본문 바로가기
Study

디자인 패턴, Observer Pattern (관찰자 패턴)

by Client. DJ 2022. 1. 29.
반응형

옵저버 패턴이란?

대상의 특정 행위가 일어나면 관찰자가 구독 중인 객체에게 브로드캐스팅(방송)하여 알려주는 패턴입니다.

관찰자가 대상을 주시하고 있다가, 특정 행동이 보이면 구독하고 있는 객체들에게 브로드케스팅을 한다.

특징

- 어떠한 한 행위에 대한 보고를 객체마다 일일이 참조해주지 않고 관찰자에게 구독하고 있으면 된다.

- "Delegate event chain + 단일 인스턴스(Singleton Pattern)" 형태이다.

예제

NotificationCenter.cs (관찰자)

public static class NotificationCenter
{
    public delegate void Subscribe(string text);
    private static event Subscribe _subscribes = null;

    /// <summary>
    /// 구독 리스트
    /// </summary>
    public static event Subscribe Subscribes
    {
        add
        {
            _subscribes -= value;
            _subscribes += value;
        }
        remove
        {
            _subscribes -= value;
        }
    }

    /// <summary>
    /// 알림
    /// </summary>
    /// <param name="title"></param>
    /// <param name="text"></param>
    public static void Broadcast(string text)
    {
        _subscribes?.Invoke(text);
    }
}

Character.cs

public abstract class Character
{
    public float Hp { get; protected set; } = 100f;
    public float Defense { get; protected set; } = 0f;
    public float Power { get; protected set; } = 0f;
    
    public void Attack(Character target)
    {
        target.DamageProcess(this.Power);
    }

    public void DamageProcess(float power)
    {
        var damage = power - this.Defense;
        if (damage < 0f) damage = 0f;
        this.Hp -= damage;

        // 방송
        if (this.Hp > 0f)
        {
            NotificationCenter.Broadcast($"{this.GetType()}는 '{damage}' 데미지를 받았다. (현재 체력 : {this.Hp})");
        }
        else
        {
            NotificationCenter.Broadcast($"{this.GetType()}는 사망했다.");
        }
    }

    public bool IsDead()
    {
        return this.Hp <= 0f;
    }
}

Character 클래스를 상속받은 Player.cs, Monster.cs

public class Player : Character
{
    public Player()
    {
        this.Hp = 1500f;
        this.Defense = 40f;
        this.Power = 100f;
    }
}

public class Monster : Character
{
    public Monster()
    {
        this.Hp = 1000f;
        this.Defense = 50f;
        this.Power = 50f;
    }
}

테스트 케이스

대결 시작

using System;
using System.Collections.Generic;
class Program
{
    private static List<string> records = new List<string>();
    static void Main(string[] args)
    {
        // 구독 (이벤트 발생마다 텍스트 기록)
        NotificationCenter.Subscribes += text => records.Add(text);

        // 캐릭터 생성
        Player player = new Player();
        Monster monster = new Monster();

        // 대결 시작
        while (!player.IsDead() && !monster.IsDead())
        {
            if (!player.IsDead())
                player.Attack(monster);

            if (!monster.IsDead())
                monster.Attack(player);
        }

        // 대결 결과
        for (int i = 0; i < records.Count; i++)
        {
            Console.WriteLine(records[i]);
        }
    }
}

결과

..중략
Monster는 '50' 데미지를 받았다. (현재 체력 : 400)
Player는 '10' 데미지를 받았다. (현재 체력 : 1380)
Monster는 '50' 데미지를 받았다. (현재 체력 : 350)
Player는 '10' 데미지를 받았다. (현재 체력 : 1370)
Monster는 '50' 데미지를 받았다. (현재 체력 : 300)
Player는 '10' 데미지를 받았다. (현재 체력 : 1360)
Monster는 '50' 데미지를 받았다. (현재 체력 : 250)
Player는 '10' 데미지를 받았다. (현재 체력 : 1350)
Monster는 '50' 데미지를 받았다. (현재 체력 : 200)
Player는 '10' 데미지를 받았다. (현재 체력 : 1340)
Monster는 '50' 데미지를 받았다. (현재 체력 : 150)
Player는 '10' 데미지를 받았다. (현재 체력 : 1330)
Monster는 '50' 데미지를 받았다. (현재 체력 : 100)
Player는 '10' 데미지를 받았다. (현재 체력 : 1320)
Monster는 '50' 데미지를 받았다. (현재 체력 : 50)
Player는 '10' 데미지를 받았다. (현재 체력 : 1310)
Monster는 사망했다.

예제 설명

이해를 돕기 위한 이미지

Player vs Monster 구도로 대결을 하였습니다. 이 둘은 공격을 주고 받을 때마다, NotificationCenter에 알려줍니다. 공격 소식을 구독 중인 records.Add(text) 행위를 함으로써 기록을 저장할 수 있습니다.

 

Publisher(제공자) : Player, Monster 클래스

Observer(관찰자) : NotificationCenter 클래스

Subscribe(구독) : 기록 행위를 하는 대리자 delegate (string text) { records.Add(text); }

패턴 사용 목적

한 번의 행위로 여러 결과 행위가 필요한 경우에 사용하면 유리합니다.

(ex. 몬스터 사망 => 사망 이펙트, 결과창, 기록, 보수 지급, 다음 스테이지 이동 처리, 다른 유저들에게 업적 달성 알림 처리 등 동시 처리)

주의

- 무분별한 구독 남용시, 디버깅하기가 어려워지며 이는 예외 상황을 초래할 수 있습니다. 옵저버 패턴은 싱글턴 패턴에 버금가는 편리함이 있기 때문에 너무 의존적이지 않고, 꼭 필요한 곳에서 사용하는 것이 좋겠습니다.

- 순서 보장이 되지 않습니다. 구독하고 있는 메소드의 순서가 보장되지 않음을 유의해야 합니다. 하나의 프로젝트에서 여러 작업자들이 사용한다면 순서 보장이 될까요? 우리의 프로그래밍은 절차지향입니다. :)

반응형

댓글