디자인패턴 3번째 시간의 주인공은 바로 옵저버 패턴 Observer Pattern 입니다! 주로 웹이나 앱에서 회원 전용 메일 시스템 혹은 키보드나 마우스에서 발생하는 입력 이벤트 처리 시스템에서 적극적으로 활용되고 있는만큼 유명한 디자인 패턴이기 때문에 이번 기회에 잘 배워봅시다.^^
Subject-<정보를 제공하는 객체>와 Observer-<정보를 제공받는 객체> 간에 1 : n 의존 관계를 정의하고, Subject를 구성하는 객체들에서 변경 사항이 감지되면, 등록되어 있는 Observer의 상태가 최신으로 갱신할 수 있도록 설계된 Design Pattern입니다.

Observer Pattern을 설계할때 곰곰히 생각해보면 Subject가 Observer에게 최신 데이터를 전달해야 하므로 ConcreteSubject에서 최신데이터를 정의하거나, 가지고 있어야 하므로 위의 클래스 다이어그램은 자연스럽게 느껴지실 겁니다.
그런데 얘초부터 정보를 공급받을 ConcreateObserver에 state를 가지게 하고, 최신 state를 요청하는 onDemand 방식의 구조로 만들면 안되는 걸까요?
현재 옵저버 패턴이 활용되고 있는 시스템이 대표적으로 회원 전용 메일시스템이라고 말씀드렸죠. 보통 웹사이트에선 제공하는 최신 정보나 새로운 소식 등등을 메일을 통해 회원가입을 한 클라이언트들에게 전달됩니다. 그런데 클라이언트에게 직접 최신 정보를 가지고 올 수 있게 만들면, 사용성이 굉장히 떨어집니다. 간단히 말하면 귀찮아집니다. 이유는 최신 정보가 있는지 없는지 확인하기 위해선 클라이언트가 일일이 요청을 보내야 알 수 있게 됩니다.
이건 마치 아침에 일찍 일어나기 위해 알림벨을 맞췄더니, 일어나기 전에 알람벨이 몇 시에 울릴지 확인해야되는 상황과 똑같습니다. <현재 시각> 자체가 새로운 정보이기 때문입니다...!
이제 Observer Pattern이 왜 필요한지 간단히 설명할 수 있습니다. Observer들은 분명히 최근 데이터를 필요로 하지만, 평소엔 관심이 없기 때문입니다. 그래서 Subject와 Obserever의 개념을 분리하여 구현하고, Subject에서 Observer에게 상태 변경을 알리는 구조가 필요한 것입니다.
더 자세히 이해하기 위해 유명한 사례를 하나 가져왔습니다.
유니티 게임 프로젝트의 InputManager 컴포넌트 (유니티는 C#을 쓰지만, 필자는 자바를 사용해 구현해보겠다.)
- 게임을 플레이하기 위해선 키보드나 마우스, 게임 패드와 같이 특정 입력 장치가 필요합니다. 입력 장치로부터 발생하는 다양한 입력값들은 게임 프로젝트 내 플레이어가 조종가능한 객체들 또한 가지각색입니다. 대표적으로 조종가능한 케릭터들이나, 케릭터가 탑승한 자동차나 오토바이 같은 탈 것들도 있고, 설정창과 같은 UI 객체들 또한, 조종받을 수 있는 객체들입니다.
- 따라서, Input values는 매우 빈번하게 상태가 변경되며, 수많은 객체들에게 이 사항을 알려야되고, 객체들마다 해석되는 방식도 제각각입니다. 이를 문제를 해결하기 위해서 Observer Pattern이 효과적입니다.
위의 글을 바탕으로 클래스 다이어그램을 제작해보면 아래와 같다.

[Observer]
public interface Observer {
void update(int value);
}
[Subject]
public interface Subject {
void registerObserver(Observer o);
void unRegisterObserver(Observer o);
void notifyObservers(int value);
}
[InputManager]
import java.util.ArrayList;
import java.util.Scanner;
public class InputManager implements Subject {
private ArrayList<Observer> observers = new ArrayList<>();
public InputManager() {}
@Override
public void registerObserver(Observer o)
{
if (observers != null) observers.add(o);
}
@Override
public void unRegisterObserver(Observer o)
{
if (observers != null) observers.remove(o);
}
@Override
public void notifyObservers(int value)
{
if (observers != null && !observers.isEmpty())
{
for (Observer o: observers)
{
o.update(value);
}
}
}
public void newInputEvent()
{
Scanner scanner = new Scanner(System.in);
notifyObservers(scanner.nextInt());
}
}
[Controller]
public abstract class Controller {
public abstract void control(int value);
}
[PlayerController] 나머지 VehicleController, UiController도 비슷함
public class PlayerController extends Controller implements Observer
{
@Override
public void control(int value)
{
System.out.println("Player Controller execute order: " + value);
}
@Override
public void update(int value)
{
control(value);
}
}
[Main]
public class Main {
static InputManager _input = new InputManager();
static PlayerController playerController = new PlayerController();
static VehicleController vehicleController = new VehicleController();
static UiController uiController = new UiController();
public static void main(String[] args) {
Observer[] controllers = {
playerController,
vehicleController,
uiController
};
// Controller 를 InputManager 에 등록
for (Observer controller: controllers)
{
_input.registerObserver(controller);
}
_input.newInputEvent();
// InputManager 에 등록된 Controller 중 UiController 등록해제
_input.unRegisterObserver(uiController);
_input.newInputEvent();
}
}
실행 결과
보시다시피 이제 Subject에 등록된 Observer들이 최신 state도 자동적으로 반영되고 언제든지 등록을 해제할 수도 있으며, 최신 정보를 어떻게 다룰것인지 각 Controller마다 따로따로 정의할 수 있습니다. 이제 케릭터, 탈 것, Ui패널마다 따로따로 입력 키들을 맵핑시켜 알맞은 동작을 충돌없이 구현할 수 있고, 한번에 하나씩만 등록 상태로 전환시킨 후 독립적으로 호출할 수 있습니다.
Observer Pattern은 유명한 디자인 패턴이라고 말씀드렸었죠? 사실 java.util 패키지에서 Observer(관찰자)과 Observable(관찰 받는 대상 즉, 위에서 Subject와 같은 위치)를 찾아볼 수 있습니다. 아래가 Observable을 사용했을때 클래스 다이어그램입니다.
C# 언어에선 delegate, Unity Engine에선 System.Action 와 같이 라이브러리에서 Observer Pattern을 응용한 다중 매서드 호출도 널리 사용되고 있습니다.
그럼 java 공식 라이브러 에서 이미 구현되어 있는데, 위처럼 복잡한 설계를 따라갈 필요가 있나 의문이 들겁니다.
그 외 deprecated의 다양한 원인으로 오라클 공식 문서에서 설명해주고 있으니 아래 링크를 접속해 참고해주시길 바랍니다.