이번에도 이해를 돕기 위해 롤을 예시로 들어보자.
평화로운 라인전에서 우리가 해야 할일은 미니언 막타!! cs를 챙겨야 된다.
미니언 막타를 쳤을 때 롤 클라이언트에서 일어나는 일을 생각해보자.
미니언 막타로 옵저버 패턴 이해해보기
1. 골드가 올라간다
미니언이 죽으면서 위에 골드 몇원을 얻었는지 UI가 뜨고
동시에 하단 템창 옆에 있는 골드 표시 UI도 해당 금액이 더해진 누적 금액으로 바뀌게 된다.
2. CS가 올라간다
게임내 정보에 내 CS 정보가 업데이트 되어 Tab키를 눌러보면 CS가 1 증가해있는걸 볼 수 있다.
여기에 CS로 벌은 골드 정보도 포함시킨다 생각해보자.
3. 어쩔때는 현상금이 올라간다
압도적인 CS로 상대팀 챔피언들보다 골드가 많아지면 현상금이 붙게 되는걸 Tab키를 눌러 확인할 수 있다.
이 세가지를 토대로 클래스를 설계해보자.
Class Minion
{
public void Died() // 막타로 죽었을 때
{
Gold.Update(newGold);
CreepScore.Update(newGold);
Prize.Update(newGold);
}
}
Class Gold // 골드를 담당하는 클래스
{
public void Update(int gold)
{
// 인자로 들어온 값으로 현재 골드를 update한다.
}
}
Class CreepScore // CS를 담당하는 클래스
{
public void Update(int gold)
{
// 인자로 들어온 값으로 현재 CS정보 옆에 뜨는 골드를 update한다.
}
}
Class Prize // 현상금을 담당하는 클래스
{
public void Update(int gold)
{
// 인자로 들어온 값으로 현상금을 계산해 update한다.
}
}
좀 생략되어 있긴 하지만 이해하는데는 큰 무리가 없을거라고 생각한다.
겉으로는 괜찮아 보이지만 문제점이 존재한다.
뭐가 문제일까?
1. 동적으로 막타를 쳤을 때 실행되는 동작을 추가 / 제거 할 수 없다.
행동들은 이미 Minion 클래스의 Died 메소드에 쭈르륵 정의되어 있기 때문에 동적으로 바꿀 수 없다.
2. Minion 클래스에서는 막타를 쳤을 때 해야 할 일들 전부를 알고 있어야 한다.
동작 클래스들의 인스턴스를 가지고 있어야 하며 일일이 동작 클래스들의 update 메소드를 호출해줘야한다.
이벤트로 1대1 모드가 추가되어 CS 100개를 먼저 달성하는 쪽이 승리하도록 게임을 바꾼다고 해보자
혹은 계정 생성 이후 전체 CS를 카운트 해줘야 되는 일이 생긴다면?
Class Minion
{
public void Died() // 막타로 죽었을 때
{
Gold.Update(newGold);
CreepScore.Update(newGold);
Prize.Update(newGold);
OneVsOne.Update(newCs);
CountCS.Update(newCs);
//...
}
}
계속 이런식으로 행동들을 Minion 클래스 내부에서 미리 알고 있는 식으로 코드를 짜야한다.
Minion 클래스는 다른 동작 클래스들과 양방향으로 계속 소통 할 필요가 없으므로 불필요한 코드라고 볼 수 있다.
이러한 문제점들은 옵저버 패턴으로 해결 할 수 있다.
옵저버 패턴의 정의
옵저버 패턴은, 흔히들 신문사와 구독자사이의 관계로 예를 드는데
신문사는 신문을 발간 후 나눠줄 때 따로 사람들을 찾는게 아니라 등록된 구독자들에게만 나눠준다.
구독자들은 언제든지 구독을 끊을 수 있고, 구독자가 아닌 사람들도 언제든지 돈을 내고 구독을 신청할 수 있다.
신문사는 신문을 나눠 준 뒤 구독자들이 그 신문으로 고기를 싸든지 창문에 붙이든지 볼일을 보든지 신경쓰지 않는다.
구독이라는 약속 관계가 있으므로 그냥 나눠줄 뿐이다.
이렇게 두 객체(신문사, 구독자)끼리 느슨하게(서로에 대해 잘 모름) 결합된 형태를 Loose Coupling이라고 한다.
이게 옵저버 패턴의 전부인데, 상당히 자연스럽고 매력적이다.
옵저버 패턴으로 다시 구현해보기
interface ISubject
{
void Register(IObserver subsciber);
void Remove(IObserver subscriber);
void Notify();
}
Class Minion : ISubject
{
int newGold;
List<IObserver> subscribers = new List<IObserver();
public void Register(IObserver subscriber)
=> subscribers.Add(subscriber);
public void Remove(IObserver subscriber)
=> subscriber.Remove(subscriber);
public void Notify()
{
foreach(var subscriber in subscribers)
subscriber.Update(newGold);
}
public void Died() // 막타로 죽었을 때
{
Notify();
}
}
interface IObserver
{
void Update(int newGold);
}
Class Gold : IObserver // 미니언 신문사의 구독자이다
{
public Gold(ISubject subject)
=> subject.Register(this);
public void Update(int newGold)
{
// 인자로 들어온 값으로 현재 골드를 update한다.
}
}
Class CreepScore : IObserver // 미니언 신문사의 구독자이다
{
public CreepScore(ISubject subject)
=> subject.Register(this);
public void Update(int newGold)
{
// 인자로 들어온 값으로 현재 CS 옆의 골드를 update한다.
}
}
Class Prize : IObserver// 미니언 신문사의 구독자이다
{
public Prize(ISubject subject)
=> subject.Register(this);
public void Update(int newGold)
{
// 인자로 들어온 값으로 현상금을 계산해 update한다.
}
}
미니언 신문사에 구독하고 싶은 객체들은 Register 메소드를 통해 구독을 신청하면 된다.
단, 구독자의 조건인 돈(인터페이스)이 마련이 되어야 한다.
신문사는 구독자에 대한 정보를 알 필요도 없이, 돈이 지불되었다는것만 확인이 되면 Notify 해줌으로써
Loose-Coupling 관계도 성립하게 된다. 이렇게 옵저버 패턴을 적용할 수 있다.
옵저버 패턴을 구현하는 여러가지 방법
먼저 위의 방식은 신문사가 신문을 내보내는(PUSH) 형식이라고 생각할 수 있다.
미니언 클래스의 변수인 newGold가 미리 약속된 구독자들(int형 변수를 받는)에게 전달되는 형식이다.
반대로 PULL의 경우는 어떤 경우일까?
신문사가 신문을 배달해주는게 아닌, 신문이 발간되었다고만 말해주는 상황이다.
구독자들은 신문이 발간되었다는 소식을 듣고 직접 신문사로 찾아가 신문을 가져온다.
이를 코드로 나타내면 다음과 같다.
interface IObservable
{
void Register(IObserver subsciber);
void Remove(IObserver subscriber);
void Notify();
int GetNewGold(); // 미니언이 주는 골드를 return한다.
}
Class Minion : IObservable
{
int newGold;
List<IObserver> subscribers = new List<IObserver();
public void Register(IObserver subscriber)
=> subscribers.Add(subscriber);
public void Remove(IObserver subscriber)
=> subscriber.Remove(subscriber);
private void Notify()
{
foreach(var subscriber in subscribers)
subscriber.Update(this);
}
public int GetNewGold()
=> newGold;
public void Died() // 막타로 죽었을 때
{
Notify();
}
}
interface IObserver
{
void Update(int newGold);
}
Class Gold : IObserver // 미니언 신문사의 구독자이다
{
public Gold(ISubject subject)
=> subject.Register(this);
public void Update(ISubject subject)
{
int newGold = subject.GetNewGold();
// 인자로 들어온 값으로 현재 골드를 update한다.
}
}
Class CreepScore : IObserver // 미니언 신문사의 구독자이다
{
public CreepScore(ISubject subject)
=> subject.Register(this);
public void Update(ISubject subject)
{
int newGold = subject.GetNewGold();
// 인자로 들어온 값으로 현재 CS 옆의 골드를 update한다.
}
}
Class Prize : IObserver// 미니언 신문사의 구독자이다
{
public Prize(ISubject subject)
=> subject.Register(this);
public void Update(ISubject subject)
{
int newGold = subject.GetNewGold();
// 인자로 들어온 값으로 현상금을 계산해 update한다.
}
}
PUSH방식과 PULL방식에서 크게 다른점은 없지만 나는 개인적으로 PUSH방식을 선호한다.
옵저버 패턴의 장점과 단점
중간에 제시했던 문제들은 다 해결이 되었다.
1. 동적으로 막타를 쳤을 때 실행되는 동작을 추가 / 제거 할 수 없다.
IObserver 인터페이스를 구현만 한다면 그 누구든 동적으로 구독, 구독취소 할 수 있다.
2. Minion 클래스에서는 막타를 쳤을 때 해야 할 일들 전부를 알고 있어야 한다.
신문사는 구독자들의 정보를 더 이상 깊게 알지 못하며 그저 IObserver 인터페이스를 구현한다는 것만 알고 있다.
단점은 그럼 뭘까??
구독자들의 신문을 받는 순서가 가시적이지 않다는 점이다.
Class Minion
{
public void Died() // 막타로 죽었을 때
{
Gold.Update(newGold);
CreepScore.Update(newGold);
Prize.Update(newGold);
}
}
이 코드는 Update 순서가 절차적으로 명확하다. 따라서 순서를 못보게 되는 옵저버 패턴은 아쉽다
라고 생각할 수 있는데, 애초에 순서가 중요한것은 OOP가 추구하는 Loose-Coupling 관점에 어긋난다.
순서에 상관 없이 동일한 결과가 나와야 Loose-Coupling에 부합하는 코드라고 할 수 있다.
옵저버 패턴은 이벤트 주도 프로그래밍에서 중요한 패러다임이고
MVC패턴, Node.js의 Event-Loop에도 사용되는 보편적인 패턴이어서 이해를 하고 넘어간다면 분명 도움이 될 것이다.
'Programming > 디자인 패턴' 카테고리의 다른 글
[디자인 패턴] 5. 싱글턴 패턴 (0) | 2022.03.22 |
---|---|
[디자인 패턴] 4. 팩토리 패턴 (0) | 2022.03.17 |
[디자인 패턴] 3. 데코레이터 패턴 (0) | 2022.03.17 |
[디자인 패턴] 1. 스트래티지 패턴 (0) | 2022.03.10 |
[디자인 패턴] 디자인 패턴이란? (0) | 2022.03.10 |