오늘 수업에서는 다양한 기본적인 디자인 패턴을 공부 하였다. 하나씩 나열해가며 정리 해 보도록 하자.
디자인 패턴은 우리가 어느 상황에 직면했을 경우, 해결할 수 있는 한가지 방법일 뿐이다. 이게 정형화 되서 말이 디자인 패턴처럼 어렵게 나온거지, 나도 모르는 사이에 쓰던 디자인 패턴이 다양하였다. (추후에 하나하나 설명하면서 언급 할 예정이다.)
즉 알면 업무를 할 시 해결 방안의 한가지 제시책일 뿐이지, 반드시 따라야 하는것은 아니다. 억지로 패턴을 만들려 하지말고 상황에서 이 디자인 패턴을 쓰면 좋겠다라고 생각하면 좋다.
다만 기본적으로 알아야 할 점들은, 이 아이디어들이 정형화 되서, 용어들을 알고 있어야, 협업할 시 유리하다는 것이다. 어찌보면 현업에서 사용 할 방식들이니 미리 참고하고 들어가도록 하자.
추가적으로 보통 디자인 패턴을 사용할 시 Interface를 많이 사용하늗데, 이유는 여러 패턴들을 혼용하거나, 여러개를 같이 사용할 수 있으며, 확장성이 좋기 때문에 Interface지향적인 개발을 하는 것이다.
우리가 자주 쓰던 static을 사용하는 싱글톤들... 이것도 어찌보면 디자인 패턴 중 하나이다. 특성을 살펴보도록 하자.
해당 객체는 반드시 하나만 존재한다
싱글톤의 핵심 개념이자, 사실상 쓰는 이유이다. 하나만 존재하기 때문에, 접근도 간단하다. 다만 싱글톤을 사용할 시 주의할 점에 대해 언급해 보도록 하자.
싱글톤을 남용하면 꼬인다...?
어찌보면 하나에서 다 처리하려 하면, 여러군데에서 모두 참조하다보니, 꼬일 수 밖에 없다. 애초에 어디서 사용되었는지 파악하기도 가면 갈수록 힘들어질 것이고, 값이 이상하게 바뀌어 버리는 현상도 발생 할 수 있다.
이래서 주로 Manager같은 여러 시스템에서 동시에 사용할 것들만, 남겨서 싱글톤으로 만들어 주도록 하자.
이 점에서 Player에 대한 이야기가 있는데, Player또한 하나이지만, 인스턴스들이 여러개 존재 하기 때문에, Player을 싱글톤으로 사용하는것은 나쁜 습관이라고 하니 주의하도록 하자.
싱글톤은 하나... 아니 반드시 하나 존재해야한다.
우리가 싱글톤은 하나만 존재해야하고, 반드시 하나는 존재해야 한다. 뒤에 있는 문장이 치명적일 수 있을것이다. 반드시 하나는 존재 해 주어야 하기 때문에, 테스트를 하기 위해서 돌렸더니 해당 싱글톤이 없어서 테스트가 터지는 경우들도 존재한다. 물론 모든 씬에 넣어줄 수도 있겠지만, 다른 방식을 코드로 처리할 수 있다.
// 싱글톤 클래스
public class Singleton
{
private static _instance
public static instance
{
get
{
if(_instance = null)
_instance = new Singleton()
return _instance
}
}
}
해당 코드는 싱글톤 참조 시에, 싱글톤이 비어있을 때 새로 하나 만들어주는 방식을 사용한다. MonoBehaviour이 아닐때의 예시이지만, MonoBehaviour을 상속할때도 비슷하게 작업 해주면 된다. (GameObject생성 하고 AddComponent로 싱글톤 넣어주기)
팩토리를 써본적은 없는것 같지만, 말 그대로 공장이다. 밖에서 팩토리에게 "이거 만들어줘" 하면 팩토리가 알아서 만드는 형식이다.
물론 팩토리를 안쓰고 각각의 위치에서 만들 수 있을 것이다. 하지만 나중에 고칠때를 생각 해 보도록 하자. 모든 곳에 가서 다 고쳐줘야 하는 불편함이 생길 수 밖에 없다. 이를 방지하기 위해 생긴것이 팩토리라고 보면 된다. (팩토리에서만 생산하고, 이 팩토리만 고치면 나오는것도 다 고쳐진게 나온다.)
팩토리를 만들때는 여러 방식이 있는데, 여기서부터 인터페이스를 들어가 보도록 하자.
interface IDrink
{
void ConsumeDrink();
}
class Soda : IDrink
{
void ConsumeDrink() => Console.WriteLine("소다 시원해");
}
class DrinkFactory
{
public IDrink MakeDrink(ID id)
{
/...
if (id == "Soda") return new Soda();
/...
}
}
간단한 예시이다. DrinkFactory라는 음료수를 만드는 공장이 있다고 보자. 여기서 인터페이스로 음료의 특성을 가진 친구들을 만들어 줄것인데, public으로 지정 해 주어, ID를 받아 음료를 만들어 준다.
이제 Soda라는 ID를 팩토리에 넣어주면, 소다가 나오는거다! 나중에 소다에 향신료를 추가하든 레시피가 바뀌어도, Factory안에서 수정만 해 준다면 밖에서는 소다가 나온다는것만 알면 되서, 주문만 넣어주면 되는 것이다.
옵저버는 감시자이다. 어찌보면 하나의 상황이 발생 했을 시, 이를 다른곳에 전달 해 주기 위해 존재한다. 어찌보면 해당 상황에 행동해야 할 다른 위치가 많을 때 사용하면 좋은것이다.
옵저버 또한 인터페이스로 구성되는데, 한번 만들어 보도록 하자.
//Interface
public Interface IWantSoda
{
void OnSodaGenerated();
}
//Subject
public class Shop
{
/...
private List<IWantSoda> observers = new();
public void AddObserver(IWantSoda observer)
{
if (!observers.Contains(observer))
observers.Add(observer);
}
public void RemoveObserver(IWnatSoda observer)
{
if (observers.Contains(observer))
observers.Remove(observer);
}
/...
void NotifyObservers()
{
foreach( var observer in observers)
observer.OnSodaGenerated();
}
}
//Observer
public class Students : IWantSoda
{
/... Start에 추가
public void OnSodaGenerated()
{
/...
}
/... 사라질때 해제
}
코드가 길어져서 대충 적어보았다. 우선 상점이라는 Subject가 있다. 사건이 발생하는 위치라고 보면 된다. 이제 상점에서 탄산이 들어왔다는 상황을 생각 해 보자. Shop애서는 미리 탄산을 먹고 싶은 학생들의 정보를 받아두었다. (이는 학생 클래스에서 학생이 Start에서 해 줘야 한다. - 코드 생략)
이제 Shop에 탄산이 들어왔으니, 우리는 학생들을 순회하면서 카톡을 날릴 것이라고 생각하면 된다. 탄산이 들어온 시점에 foreach문으로 학생들에 있는 OnSodaGenerated를 실행시켜 줄것이다. 어찌보면 Shop에서 단체 카톡으로 날리는 것이기 때문에 관리도 쉽고, 학생들은 Shop에서 탄산이 들어왔다는 정보가 왔으니 각각의 행동(탄산을 위해 매점으로 뛰어!)을 할 것이다. 이것이 옵저버 패턴이라고 보면 된다.
Mediator이라고 불리는 중재자는 사실 전에 한번 써 본적이 있다. 전 프로젝트의 퀘스트 시스템에 Event Bus Pattern이 중재자와 옵저버를 합친 버전이라고 하는데, 일단 이번에도 상황을 정리하고 예시를 보도록 하자.
이제 코딩을 하다보면 관계가 복잡해지는데, 이 결합도를 줄이기 위해서 필요한 개념이라고 보면 된다.
예시 상황을 한번 보자.
만약 이제 여러 학생들이 있는데 보니까 상점에 탄산이 들어왔다고는 하는데, 내가 상점주인이랑 사이가 안좋을 수도 있을것이고, 사람이 몰리는것도 귀찮다. 그럴때 약간 심부름꾼을 시켜 중간에서 처리를 해 주는 것이 중재자의 역활이다. 이러면 심부름꾼이 관리를 다 할것이다. (상점은 심부름꾼에게만 문자를 전할거고, 이제 심부름꾼이 소다를 먹고싶어하는 사람들에게 소다를 전달 할 것이다.)
학생 <-> 심부름꾼 <-> 상점
약간 이런 개념으로 잡고 가면 된다. 이러면 학생과 상점간의 연결고리가 끊기니, 좀더 유동적으로 작업이 가능해진다.
이제 이 중재자에, observer을 더한것이 Event Bus Pattern이라고 한다. 전에 한번 본적 있지만, 다른 점이 있어 다시 한번 정리하자. 이번에도 예제로 설명한다
- 상점에 탄산이 들어옴
- 상점에서 보고있던 옵저버가 심부름꾼에게 탄산이 들어왔다고 연락이 함
- 심부름꾼은 학생들에게 음료수가 들어왔다고 전해준다.
- 이제 학생들은 음료수를 마시는 행동을 한다.
이런 과정을 거치는 것이다. 어찌보면 말로만 들으면 옵저버가 들어가서 더 복잡할수도 있게 보이지만, 코드상에서는 자동화가 되고 확장성도 좋아지는 현상이다.
상태는 우리 FSM(Finate State Machine) 을 생각 해 보면 좋고, 유니티에서는 Animator같이 봐도 된다. 다만 실제 상태 패턴과 FSM은 다르다고 한다. 하나의 상태를 가지고 있고, 이에 따라 동작하는것은 같다.
다만 상태 패턴은 해당 내용을 클래스 로 만들어서 저장한다. 이 점이 가장 큰 차이로 보이는데, 전에 작업한 C# 콘솔 RPG의 씬 전환같이 생각하면 된다.
FSM보다는 훨씬 간결해진다. 각각의 Class가 Interface를 상속받고, 이 Interface에 있는 내용이 해당 State Class에 들어갈때 실행 해 준다 보면 된다.
물론 FSM 과 상태 패턴을 같이 사용하는 경우도 많다고 한다.
커맨드는 명령을 객체로 만드는 것이다. 우리 ctrl + z 생각하면 된다.
이제 이 명령들을 저장하거나 나중에 실행이 필요 할 수 있어서, Stack 혹은 Queue중 맞는 상황에 있는 자료구조를 사용하여 저장한다고 보면 된다.
이제 이건 What에 해당하는 내용이다. 무슨 행동을 했는지가 중요한 내용이다.
전략은 해당 명령이 무슨 행동을 할지 정의하는 것이다. 간단하게 스킬이 가장 좋은 예시가 될 수 있다. Interface로 무슨 행동을 할지만 정하고, 내부에 있는 내용은 스킬마다 다른것을 생각 해 보면 쉽다.
아제 이건 Command와 비슷하지만 다르게 How에 해당하는 내용이다. 행동이 어떻게 구성되어있는가가 중요하다.
설명이 너무 어지럽다. 내가 정리를 너무 못한거 같기도 하고.
근데 아무래도 디자인패터는 모르면서 쓰는경우도 많기도 하고, 알면 좋은거긴 해서 내 머리속에만 저장 되면 도움이 된다.
디자인 패턴이 이 이외에 다양한 것들이 있는데, ChatGpt같은 곳에 물어봐도 좋다고 하신다.