이번에는 Singleton을 남용하면서 서로 참조하여 생기는 문제에 대해 얘기해보려 합니다.
1. 문제점
간단하게 Singleton을 남용해서 서로 참조하게되면 정말 스파게티코드가 되기 쉽습니다.
https://ko.wikipedia.org/wiki/%EC%8A%A4%ED%8C%8C%EA%B2%8C%ED%8B%B0_%EC%BD%94%EB%93%9C
스파게티 코드 - 위키백과, 우리 모두의 백과사전
스파게티 코드(spaghetti code)는 컴퓨터 프로그램의 소스 코드가 복잡하게 얽힌 모습을 스파게티의 면발에 비유한 표현이다. 스파게티 코드는 정상적으로 작동하지만, 사람이 코드를 읽으면서 그
ko.wikipedia.org
물론 스파게티 코드가 되어서 발생하는 문제들이 모두 따라붙게 되는데요, 제가 생각하는 문제는 크게 세가지입니다.
- 코드수정을 하기 위해 하나를 수정하려면 참조중인 모든 곳을 찾아가서 모두 수정해주어야한다.
- 한 곳에서 문제가 발생했지만, 모든 곳에서 작동하지 않는다.
- 방심하면 무한루프에 빠질 수 있다.
첫번째가 보통 우리가 아는 스파게티 코드라고 부르는 코드죠.
두번째 문제는 보통의 경우에는 크래시로 이어지는데, 크래시로 이어지지 않고 꾸역꾸역 돌아가고 있는 경우 문제점을 발견하지 못하고 버그로 이어지는 경우가 있습니다. 이런 경우 찾기도 힘든 것이 문제이고, 수정하기 위해서는 첫번째 문제처럼 모든 곳을 찾아서 수정해주어야합니다.
세번째는 코딩스타일에 따라 다르지만, 간혹 무한루프에 빠지는 코드들을 봤습니다.
public class ItemInventory : SingletonTemplate<ItemInventory>
{
Dictionary<long, ItemData> m_DicItems = new Dictionary<long, ItemData>();
public IEnumerable<ItemData> GetItemList() => m_DicItems.Values;
public void AddItem(ItemData item)
{
if (m_DicItems.ContainsKey(item.UID) == true)
return;
m_DicItems.Add(item.UID, item);
UIManager.Instance.GetUI<UIItemInventory>().UpdateList();
EffectManager.Instance.PlayEffect("GetItem_01", PlayerManager.Instance.Position);
PlayerManager.Instance.UpdatePlayerSkin();
}
public void RemoveItem(ItemData item)
{
if (m_DicItems.ContainsKey(item.UID) == false)
return;
m_DicItems.Remove(item.UID);
UIManager.Instance.GetUI<UIItemInventory>().UpdateList();
EffectManager.Instance.PlayEffect("DelItem_01", PlayerManager.Instance.Position);
PlayerManager.Instance.UpdatePlayerSkin();
}
}
public class PlayerManager : SingletonTemplate<PlayerManager>
{
public Vector3 Position => Vector3.zero;
public int SkinID => 0;
public void UpdatePlayerSkin()
{
var skinData = SkinManager.Instance.GetSkinData(SkinID);
if (skinData != null)
{
//TODO
}
}
}
정말 고통스러운 마음으로 샘플코드를 만들어왔습니다. 일부러 문제가 생길 수 있는 코드를 만드는 것도 쉽지 않네요.
각 Singleton이 어떤 용도인지는 중요한게 아니니 따로 설명하지 않겠습니다만, 대충 이름으로 추측가능하도록 쉬운 이름으로 만들어두었습니다.
우선 이 코드 전체를 보여드리고 하나씩 문제가 되는 부분을 고쳐보도록 합시다.
2. 불필요한 Singleton제거
우선은 위 코드에서 PlayerManager가 참조중인 SkinManager입니다.
PlayerManager는 플레이어를 관리하는 Singleton이고, SkinManager는 플레이어가 보유한 스킨들 관리하는 Singleton입니다.
PlayerManager와 SkinManager의 용도를 보고 느끼신게 있으신가요?
PlayerManager와 SkinManager가 분리될 필요가 없었습니다. 오히려 분리되었을 경우, Player가 변경되어 PlayerManager는 갱시되는데 SkinManager는 갱싱되지 않아 버그로 이어질 수 있습니다.
public class PlayerManager : SingletonTemplate<PlayerManager>
{
public Vector3 Position => Vector3.zero;
public int EquippedSkinID => 0;
public Dictionary<int, SkinData> m_DicSkins = new Dictionary<int, SkinData>();
public void UpdatePlayerSkin()
{
SkinData skinData = null;
if (m_DicSkins.TryGetValue(EquipedSkinID, out skinData) == true)
{
//TODO
}
}
}
우선은 쓸모없는 SkinManager를 제거하고 PlayerManager로 옮겨왔습니다. 또한 모호한 이름의 SkinID를 EquippedSkinID로 수정했습니다.
이렇게 불필요한 Singleton이 많아지는 것 보다 계층구조를 잘 짜서 구성해두는게 좀 더 효율적입니다.
만약에 코드양이 많아서 분리해야한다면 Singleton을 분리하는 것이 아니라, Singleton내부에 또다른 클래스별로 분리하여서 PlayerManager.Instance.Object.Position, PlayerManager.Instance.Skin.EquippedSkin 이렇게 참조하는 것도 방법이며 partial로 스크립트를 분리하는 것도 방법입니다.
3. Singleton 내부에서 다른 Singleton 참조하지 않도록 하기
꽤나 많은 사람들이 Singleton내부에서 Singleton을 호출하는 방식을 사용중입니다.
물론, 이렇게 해야하는 경우도 있지만 사실 이렇게까지 할 필요가 없는데 하는 경우가 꽤 있다는게 문제입니다.
저는 제 나름대로의 규칙을 정해서 참하여 스파게티처럼 얽히지 않도록 하고있습니다.
- 컨텐츠쪽에 가까운 Singleton이 시스템쪽에 가까운 Singleton을 참조
- 최대한 참조는 Script에서 Singleton을 참조
이번에는 제 나름대로의 규칙으로 진행했으며, 이 것이 꼭 해답은 아니며 협업하는 분들과 논의하여 좀 더 올바른 방향으로 지행하시면 됩니다.
여러분들에게 이런 방법도 있다고 참고만 해드리는 용도라는 것을 명심해주시길 바랍니다.
public class ItemInventory : SingletonTemplate<ItemInventory>
{
Dictionary<long, ItemData> m_DicItems = new Dictionary<long, ItemData>();
public System.Action OnAddItemEvent;
public System.Action OnRemoveItemEvent;
public IEnumerable<ItemData> GetItemList() => m_DicItems.Values;
public void AddItem(ItemData item)
{
if (m_DicItems.ContainsKey(item.UID) == true)
return;
m_DicItems.Add(item.UID, item);
OnAddItemEvent?.Invoke();
}
public void RemoveItem(ItemData item)
{
if (m_DicItems.ContainsKey(item.UID) == false)
return;
m_DicItems.Remove(item.UID);
OnRemoveItemEvent?.Invoke();
}
}
저는 Singleton이 Singleton을 참조하는 문제를 해결하기 위해 deletate를 사용했고 C#에서 제공하는 Action deletage를 사용했습니다.
개인적으로는 UniRx를 사용하는 것을 추천드리지만, UniRx를 모르는 분들도 많이 보실걸 감안한 것도 있고 UniRx설명만으로 다 잡아먹을 것 같아 링크만 남겨두겠습니다.
https://github.com/neuecc/UniRx
GitHub - neuecc/UniRx: Reactive Extensions for Unity
Reactive Extensions for Unity. Contribute to neuecc/UniRx development by creating an account on GitHub.
github.com
public class UIItemInventory : MonoBehaviour
{
private void OnEnable()
{
ItemInventory.Instance.OnAddItemEvent += OnAddItem;
ItemInventory.Instance.OnRemoveItemEvent += OnRemoveItem;
UpdateList();
}
private void OnDisable()
{
ItemInventory.Instance.OnAddItemEvent -= OnAddItem;
ItemInventory.Instance.OnRemoveItemEvent -= OnRemoveItem;
}
void OnAddItem()
{
if (this == null) return;
UpdateList();
}
void OnRemoveItem()
{
if (this == null) return;
UpdateList();
}
public void UpdateList() { }
}
public class PlayerController : MonoBehaviour
{
private void OnEnable()
{
ItemInventory.Instance.OnAddItemEvent += OnAddItem;
ItemInventory.Instance.OnRemoveItemEvent += OnRemoveItem;
PlayerManager.Instance.UpdatePlayerSkin();
}
private void OnDisable()
{
ItemInventory.Instance.OnAddItemEvent -= OnAddItem;
ItemInventory.Instance.OnRemoveItemEvent -= OnRemoveItem;
}
void OnAddItem()
{
if (this == null) return;
EffectManager.Instance.PlayEffect("GetItem_01", transform.position);
}
void OnRemoveItem()
{
if (this == null) return;
EffectManager.Instance.PlayEffect("DelItem_01", transform.position);
}
}
실제 Script 코드에서는 이렇게 사용하시면 됩니다.
this == null 코드는 오브젝트가 파괴되었는데 콜백을 받는 경우가 간혹 있어서 예외처리하는 코드를 추가했습니다. (가끔 이러는데 원인을 잘 모르겠습니다.. 혹시 아는 분은 덧글 달아주세요...)
이 방식대로 참조하면 Singleton은 깔끔하게 유지되고, 지저분한건 Script가 모두 가져가게 되는 것이죠.
4. 마무리
결국 제 생각을 적어보았지만, 프로젝트에 따라 같이 일하시는 분들에 따라 방식이 정해져있지는 않습니다.
이런 방식도 있다는 것을 알고 프로젝트 및 같이 일하는 분들과 논의하고 맞춰서 진행하시면 될 것 같습니다.
조금이라도 도움이 되셨길 바라고, 이번편을 마지막으로 "Unity에서의 Singleton"편은 마무지막입니다. 다음에는 다른 내용으로 글을 써보겠습니다.
긴 글 읽어주셔서 감사합니다.
'Unity Tips' 카테고리의 다른 글
Unity에서 Enum.Parse함수 GC 발생 줄이기 (0) | 2022.05.01 |
---|---|
Unity에서 계층이 있는 데이터 저장 (JSON vs XML vs Scriptable Object) (0) | 2022.04.30 |
Unity에서 C++코드(cpp) Native Plug-ins 사용하기 (0) | 2022.03.27 |
Unity에서의 Singleton 2편 - MonoBehaviour Singleton의 문제점 (0) | 2022.03.06 |
Unity에서의 Singleton 1편 - 싱글턴 클래스 만들기 (5) | 2022.02.20 |