[DesignPattern] Observer Pattern

suhan0304·2024년 9월 30일

Design Pattern

목록 보기
11/16
post-thumbnail

Observer Pattern

옵저버 패턴은 옵저버들이 관찰하고 있는 대상자의 상태가 변화가 있을 때마다 대상자는 직접 각 관찰자들에게 통지하고, 관찰자들은 알림을 받아 조치를 취하는 행동 패턴이다.

옵저버 패턴은 다른 디자인 패턴들과 다르게 일대다 의존성을 가지는데, 주로 분산 이벤트 핸들링 시스템을 구현하는데 사용한다. Pub/Sub(발행/구독) 모델로도 알려져 있기도 하다.

위에서 발행, 구독과 같은 용어를 썼는데 실제 작동도 유튜브의 구독과 비슷한 원리이다. 유튜브 채널은 발행자(Subject)가 되고 구독자들은 관찰자(Observer)가 되는 구조이다. 유튜버가 영상을 업로드하면 구독자들에게 채널에 영상이 올라왔다는 연락을 받게 된다. 반면 구독을 해지하거나 아예 구독을 하지 않은 유저에게는 알림이 가지않게 된다.

실제로 관찰자라는 이름 때문에 능동적으로 대상을 관찰하는 것이라고 생각할 수 있는데, 알다시피 계속해서 어떤 객체의 변화를 탐지하는 것은 성능적으로 당연히 부담이다. 옵저버 패턴은 관찰보다는 갱신을 위한 정보를 전달받기를 기다린다고 보는것이 적절하다.


Structure

  • ISubject : 관찰 대상자를 정의하는 인터페이스
  • ConcreteSubject : 관찰 당하는 대상자 / 발행자 / 게시자
    - Observer들을 리스트로 모아 합성하여 가지고 있음
    - Subject의 역할은 관찰자인 Observer들을 내부 리스트에 등록/삭제 하는 인프라를 갖고 있다. (register, remove)
    - Subject가 상태를 변경하거나 어떤 동작을 실행할때, Observer 들에게 이벤트 알림을 발행한다.
  • IObserver : 구독자들을 묶는 인터페이스 (다형성)
  • Observer : 관찰자 / 구독자 / 알림 수신자.
    - Observer들은 Subject가 발행한 알림에 대해 현재 상태를 취득한다.
    - Subject의 업데이트에 대해 전후 정보를 처리한다.

옵저버 패턴은 여타 다른 디자인 패턴과 똑같이 상호작용할 객체를 합성을 하고 메서드 위임을 통해 구성하는 코드 패턴임은 똑같지만, 핵심은 합성한 객체를 리스트로 관리하고 리스트에 있는 관찰자 객체들에게 모두 메서드 위임을 통한 전파 행위를 한다.


How

  1. 옵저버 패턴은 한 개의 관찰 대상자와 여러 개의 관찰자로 일 대 다 관계로 구성되어 있다.
  2. 옵저버 패턴에서는 관찰 대상의 상태가 바뀌면 변경사항을 관찰자에게 알려준다.
  3. 통보를 받은 관찰자는 적절한 로직을 수행한다.
  4. 또한, 관찰자는 언제나 대상자의 그룹에서 추가 및 삭제 될 수 있다. 대상자의 관찰자 그룹에 추가되면 대상자로부터 정보를 전달받게 되고 관찰자 그룹에서 삭제되면 더 이상 정보를 받을 수 없다.

아래 구현 예시는 JAVA 버전이고 C#(유니티)는 아래 Example을 참고하면 된다.

// 관찰 대상자 / 발행자
interface ISubject {
    void registerObserver(IObserver o);
    void removeObserver(IObserver o);
    void notifyObserver();
}

class ConcreteSubject implements ISubject {
    // 관찰자들을 등록하여 담는 리스트
    List<IObserver> observers = new ArrayList<>();

    // 관찰자를 리스트에 등록
    @Override
    public void registerObserver(IObserver o) {
        observers.add(o);
        System.out.println(o + " 구독 완료");
    }

    // 관찰자를 리스트에 제거
    @Override
    public void removeObserver(IObserver o) {
        observers.remove(o);
        System.out.println(o + " 구독 취소");
    }

    // 관찰자에게 이벤트 송신
    @Override
    public void notifyObserver() {
        for(IObserver o : observers) { // 관찰자 리스트를 순회하며
            o.update(); // 위임
        }
    }
}
// 관찰자 / 구독자
interface IObserver {
    void update();
}

class ObserverA implements IObserver {
    public void update() {
        System.out.println("ObserverA 한테 이벤트 알림이 왔습니다.");
    }

    public String toString() { return "ObserverA"; }
}

class ObserverB implements IObserver {
    public void update() {
        System.out.println("ObserverB 한테 이벤트 알림이 왔습니다.");
    }

    public String toString() { return "ObserverB"; }
}
public class Client {
    public static void main(String[] args) {

        // 발행자 등록
        ISubject publisher = new ConcreteSubject();

        // 발행자를 구독할 관찰자들 리스트로 등록
        IObserver o1 = new ObserverA();
        IObserver o2 = new ObserverB();
        publisher.registerObserver(o1);
        publisher.registerObserver(o2);

        // 관찰자에게 이벤트 전파
        publisher.notifyObserver();

        // ObserverB가 구독 취소
        publisher.removeObserver(o2);

        // ObserverA 한테만 이벤트 전파
        publisher.notifyObserver();
    }
}

When

  • 한정된 시간, 특정한 경우에만 다른 객체를 관찰해야 하는 경우
  • 대상 객체의 상태가 변경될 때마다 다른 객체의 동작을 트리거해야 할 때
  • 한 객체의 상태가 변경되면 다른 객체도 변경해야 할 때 + 어떤 객체들이 변경되어야하는지 대상자는 몰라도 될때
  • MVC 패턴에 사용 (Model-View-Controller)
    - MVC의 Model과 View의 관계는 관찰자 패턴의 대상자와 관찰자 역할의 관계에 대응된다.
    • 하나의 Model에 복수의 View가 대응한다.

이러한 관찰자 패턴을 사용하면...

  • 대상자의 상태 변경을 주기적으로 조회하지 않고 자동으로 알 수 있다.
  • 대상자의 코드를 변경하지 않고 새 관찰자 클래스를 도입할 수 있어 개방 폐쇄 원칙을 준수한다.
  • 동적으로 발행자와 구독 알림 관계를 맺을 수 있다.
  • 상태를 변경하는 객체와 변경을 감지하는 객체의 관계를 느슨하게 유지할 수 있다.

But

  • 구독자는 알림 순서를 제어할 수 없고, 무작위 순서로 알림을 받음
    - 하드 코딩으로 구현할 수는 있지만, 복잡성과 결합성이 높아져 추천하지는 않는다.
  • 옵저버 패턴을 자주 구성하면 복잡도가 높아져 구조와 동작을 알아보기가 힘들어진다.
  • 다수의 옵저버 객체를 등록 후 해지하지 않는다면 메모리 누수가 발생할 수 있다.

Example

플레이어의 체력 데이터과 UI가 있다고 할 때 플레이어가 데미지를 입었을때 체력 데이터를 관찰하고 있다가 UI(체력바, 체력 텍스트, 상태창)에 반영되도록 하고자 한다고 해보자.

class Player {
    public int Health { get; private set; }

    public Player(int health) {
        Health = health;
    }

    public void TakeDamage(int damage) {
        Health -= damage;
        if (Health < 0) Health = 0;
    }
}
interface IUI {
    void Display();
}

class HealthUI : IUI {
    Player player;
    string UIName;

    public HealthUI(string name, Player player) {
        this.UIName = name;
        this.player = player;
    }

    public void Display() {
       Debug.Log($"{UIName} HP : {player.Health}");
    }
}
public class Client : MonoBehaviour
{
    void Start()
    {
        Player player = new Player(100);

        List<IUI> uis = new List<IUI>
        {
            new HealthUI("HealthBar", player),
            new HealthUI("HealthText", player),
            new HealthUI("Status", player),
        };
        
        player.TakeDamage(20);

        foreach (IUI ui in uis)
        {
            ui.Display();
        }
    }
}

당작 동작에는 문제가 없어보이지만, Client 클래스 코드에 코드가 집약적으로 구성됨을 볼 수 있다. 각 UI 요소들이 체력이 갱신될 때 데이터를 전달 받는게 아니라 플레이어 객체에 직접 조회하여 display 하는 것이기 때문이다.

Observer Pattern

옵저버 패턴을 적용시켜서 각 UI 요소들이 변화된 플레이어의 체력을 자동으로 전달받을 수 있도록 구현할 필요성이 있다. 즉, 플레이어 클래스를 발행자, 관찰 대상자로서 Subject 인터페이스를 구현하도록 하고, 플레이어의 체력 데이터 사용자들을 관찰자, 구독자로서 Observer 인터페이스를 구현하도록 구성하자.

using System;
using System.Collections.Generic;

// Subject 인터페이스
interface ISubject {
    void RegisterObserver(IObserver observer);  // 구독 추가
    void RemoveObserver(IObserver observer);    // 구독 삭제
    void NotifyObservers();                     // 상태 변경 시 모든 옵저버에게 알림
}

// Observer 인터페이스
interface IObserver {
    void Update(Player player);  // Subject의 상태가 변경되면 갱신
}

// 체력을 관리하는 Player 클래스 (Subject 역할)
class Player : ISubject {
    public int Health { get; private set; }

    private List<IObserver> observers = new List<IObserver>();

    public Player(int health) {
        Health = health;
    }

    public void TakeDamage(int damage) {
        Health -= damage;
        if (Health < 0) Health = 0;
        NotifyObservers();  // 데미지를 입을 때마다 옵저버들에게 알림
    }

    // 구독자 등록
    public void RegisterObserver(IObserver observer) {
        observers.Add(observer);
    }

    // 구독자 제거
    public void RemoveObserver(IObserver observer) {
        observers.Remove(observer);
    }

    // 구독자들에게 상태 알림
    public void NotifyObservers() {
        foreach (IObserver observer in observers) {
            observer.Update(this);  // 옵저버들에게 현재 체력 상태를 전달
        }
    }
}
using UnityEngine;

// 체력 UI를 표시하는 클래스 (Observer 역할)
class HealthUI : IObserver {
    private string UIName;

    public HealthUI(string name) {
        this.UIName = name;
    }

    // Subject의 상태 변경 시 호출되는 메서드
    public void Update(Player player) {
        Debug.Log($"{UIName}'s health: {player.Health}");
    }
}
using System.Collections.Generic;
using UnityEngine;

public class Client : MonoBehaviour
{
    void Start()
    {
        Player player = new Player(100);

        HealthUI ui1 = new HealthUI("HealthBar");
        HealthUI ui2 = new HealthUI("HealthText");
        HealthUI ui3 = new HealthUI("Status");
        
        player.RegisterObserver(ui1);
        player.RegisterObserver(ui2);
        player.RegisterObserver(ui3);
        
        player.TakeDamage(20);
        
        player.RemoveObserver(ui2);
        
        player.TakeDamage(15);
        
    }
}

클라이언트 자체적으로 별다른 명령없이 TakeDamage를 실행하니 발행자로부터 구독자들이 데이터를 전달받아서 자동적으로 처리하는 것을 볼 수 있다. 핵심은 구독자들의 관리를 발행자 객체에서 리스트로 한다는 점과 리스트에 있는 모든 구독자들의 메서드를 위임하여 실행함으로써 변화가 있을 때마다 이벤트성 알림으로 전파한다.


profile
Be Honest, Be Harder, Be Stronger

0개의 댓글