옵저버 패턴 - Observer Pattern

오태형·2024년 10월 14일

디자인패턴

목록 보기
3/3

디자인패턴 3번째 시간의 주인공은 바로 옵저버 패턴 Observer Pattern 입니다! 주로 웹이나 앱에서 회원 전용 메일 시스템 혹은 키보드나 마우스에서 발생하는 입력 이벤트 처리 시스템에서 적극적으로 활용되고 있는만큼 유명한 디자인 패턴이기 때문에 이번 기회에 잘 배워봅시다.^^

Observer Pattern이 무엇인지 한 줄로 요약하자면

Subject-<정보를 제공하는 객체>와 Observer-<정보를 제공받는 객체> 간에 1 : n 의존 관계를 정의하고, Subject를 구성하는 객체들에서 변경 사항이 감지되면, 등록되어 있는 Observer의 상태가 최신으로 갱신할 수 있도록 설계된 Design Pattern입니다.

  • Observer Pattern의 클래스 다이어그램

Subject: Interface

  • registerObserver -> 연관시킬 Observer 객체를 등록
  • unRegisterObserver -> 연관된 Observer 객체를 등록 취소
  • notifyObserver -> 연관된 모든 Observer에게 Subject 객체의 상태 변화를 알림

Observer: Interface

  • update -> 연결된 Subject 객체에 맞춰 최신 상태로 Observer를 업데이트함

Observer Pattern을 설계할때 곰곰히 생각해보면 Subject가 Observer에게 최신 데이터를 전달해야 하므로 ConcreteSubject에서 최신데이터를 정의하거나, 가지고 있어야 하므로 위의 클래스 다이어그램은 자연스럽게 느껴지실 겁니다.

그런데 얘초부터 정보를 공급받을 ConcreateObserver에 state를 가지게 하고, 최신 state를 요청하는 onDemand 방식의 구조로 만들면 안되는 걸까요?


Observer Pattern이 가진 이점

현재 옵저버 패턴이 활용되고 있는 시스템이 대표적으로 회원 전용 메일시스템이라고 말씀드렸죠. 보통 웹사이트에선 제공하는 최신 정보나 새로운 소식 등등을 메일을 통해 회원가입을 한 클라이언트들에게 전달됩니다. 그런데 클라이언트에게 직접 최신 정보를 가지고 올 수 있게 만들면, 사용성이 굉장히 떨어집니다. 간단히 말하면 귀찮아집니다. 이유는 최신 정보가 있는지 없는지 확인하기 위해선 클라이언트가 일일이 요청을 보내야 알 수 있게 됩니다.

이건 마치 아침에 일찍 일어나기 위해 알림벨을 맞췄더니, 일어나기 전에 알람벨이 몇 시에 울릴지 확인해야되는 상황과 똑같습니다. <현재 시각> 자체가 새로운 정보이기 때문입니다...!

이제 Observer Pattern이 왜 필요한지 간단히 설명할 수 있습니다. Observer들은 분명히 최근 데이터를 필요로 하지만, 평소엔 관심이 없기 때문입니다. 그래서 Subject와 Obserever의 개념을 분리하여 구현하고, Subject에서 Observer에게 상태 변경을 알리는 구조가 필요한 것입니다.


Observer Pattern 구현

더 자세히 이해하기 위해 유명한 사례를 하나 가져왔습니다.

유니티 게임 프로젝트의 InputManager 컴포넌트 (유니티는 C#을 쓰지만, 필자는 자바를 사용해 구현해보겠다.)

  • 게임을 플레이하기 위해선 키보드나 마우스, 게임 패드와 같이 특정 입력 장치가 필요합니다. 입력 장치로부터 발생하는 다양한 입력값들은 게임 프로젝트 내 플레이어가 조종가능한 객체들 또한 가지각색입니다. 대표적으로 조종가능한 케릭터들이나, 케릭터가 탑승한 자동차나 오토바이 같은 탈 것들도 있고, 설정창과 같은 UI 객체들 또한, 조종받을 수 있는 객체들입니다.
  • 따라서, Input values는 매우 빈번하게 상태가 변경되며, 수많은 객체들에게 이 사항을 알려야되고, 객체들마다 해석되는 방식도 제각각입니다. 이를 문제를 해결하기 위해서 Observer Pattern이 효과적입니다.

위의 글을 바탕으로 클래스 다이어그램을 제작해보면 아래와 같다.

  • Controlloer {{abstarct}} : Controller의 상위 추상클래스입니다. ...Controller 들은 일반적으로 control(Object input): void 조작할 수 있다는 것을 임의하고 있습니다.
  • InputManager -> newInputEvent() : 이 곳에서 새로운 InputEvent를 감지하고 notifyObservers(Object)를 호출합니다. 다만, 마우스나 키보드와 같은 입력값들은 이벤트들로 본래 처리되나, 예제 코드이므로 java.util.Scanner의 Scanner.nextInt()로 숫자를 입력받는 것으로 대체했습니다. (아래 코드에서 int value가 바로 그것입니다!)
그래서 실제코드를 보면 이렇게 됩니다.

[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 사례

Observer Pattern은 유명한 디자인 패턴이라고 말씀드렸었죠? 사실 java.util 패키지에서 Observer(관찰자)과 Observable(관찰 받는 대상 즉, 위에서 Subject와 같은 위치)를 찾아볼 수 있습니다. 아래가 Observable을 사용했을때 클래스 다이어그램입니다.

C# 언어에선 delegate, Unity Engine에선 System.Action 와 같이 라이브러리에서 Observer Pattern을 응용한 다중 매서드 호출도 널리 사용되고 있습니다.

Observer Pattern의 단점

그럼 java 공식 라이브러 에서 이미 구현되어 있는데, 위처럼 복잡한 설계를 따라갈 필요가 있나 의문이 들겁니다.

  1. 문제는 Observable이 인터페이스가 아니라 클래스로 구현되어 있습니다. 그리고 자바는 다중상속을 허용하지 않습니다. 따라서 다른 클래스에 상속받아야하는 클래스는 Observer로 상속받을 수 없습니다. 구현 방식이 제한적일 수 밖에 없다는 이야기입니다.
  2. setChanged() 함수가 protected로 되어 있어 있습니다. 어차피 Observable에서 상속 받아야 쓸 수 있으므로 문제가 될 것은 아니지만, 상속보다는 구성을 사용한다는 디자인 원칙을 위배합니다. 그래서 Java SE 9부터 java.util.Observable/java.util.Observer는 deprecated 되었습니다.(사용을 권장하지 않음)
  3. Observable의 notify는 사실... 호출 순서를 보장해주지 않습니다. 저의 예시코드처럼 ArrayList를 사용한 foreach구문을 통해 순서를 보장해주지않는 이상, 런타임 중엔 호출 순서를 모르고, 1:1 상태변경 매칭은 어렵습니다. 오브젝트들끼리 상호작용이 철저하게 설계되어야하는 프로젝트에선 가장 큰 문제가 될 것입니다.

그 외 deprecated의 다양한 원인으로 오라클 공식 문서에서 설명해주고 있으니 아래 링크를 접속해 참고해주시길 바랍니다.

Ref

profile
덜익은 개발자

0개의 댓글