본문 바로가기
Study/디자인 패턴

[C#/Unity][디자인패턴] 옵저버 패턴(Observer Pattern)

by 스테디코디스트 2023. 12. 27.
반응형

1. 옵저버 패턴이란?

- 하나의 관찰 대상(객체)를 여러 개의 관찰자(옵저버)들이 관찰하고(일대다 구조), 객체의 상태 변화시 객체가 직접 옵저버들에게 상태 변화를 통지하고, 옵저버들은 해당 통지를 받는 구독 메커니즘을 가짐.

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

- 객체의 상태 변화를 관찰하는 관찰자(옵저버)들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴.

- public/subscribe (발행/구독) 모델로도 알려져 있음.

- 관찰자들은 수동적으로 객체에게 정보를 전달 받기만을 기다린다.

 

2. 옵저버 패턴을 사용하는 경우

- 일대다 의존성을 가져, 주로 분산 이벤트 핸들링 시스템을 구현하는 데 사용된다.

- MVC에서 모델과 뷰 사이를 느슨히 연결하기 위해 사용된다.

- 유튜버들이 자신의 구독자들에게 새로운 게시물을 알리는 경우와 같이 사용된다.

 

3. 옵저버 패턴의 장점

- Subject의 상태 변경을 주기적으로 조회하지 않고 자동으로 감지할 수 있다.

- 발행자의 코드를 변경하지 않고도 새 구독자 클래스를 도입할 수 있어 개방 폐쇄 원칙(OCP)을 준수한다.

- 런타임 시점에서 발행자와 구독 알림 관계를 맺을 수 있다.

- 상태를 변경하는 객체(Subject)와 변경을 감지하는 객체(Observer)의 관계를 느슨하게 유지할 수 있다.

 

4. 옵저버 패턴의 단점

- 구독자는 알림 순서를 제어할 수 없고, 무작위 순서로 알림을 받는다.(하드 코딩으로 구현할 수는 있겠지만, 복잡성과 결합성이 높아질 수 있다.)

- 옵저버 패턴을 자주 구성하면 구조와 동작을 알아보기 힘들어져 코드 복잡도가 증가한다.

- 다수의 옵저버 객체를 등록 이후 해지하지 않는다면 메모리 누수가 발생할 수 있다.

 

5. 코드 구현

1) 구조

-  ISubject 

: 관찰 대상자를 정의하는 인터페이스

- Concrete Subject

: 관찰 당하는 대상자/발행자/게시자

: Observer들을 리스트로 모아 합성하여 가지고 있음.

: Subject의 역할은 관찰자인 Observer들을 내부 리스트에 등록/삭제하는 인프라를 가짐.(register, remove)

: Subject가 상태를 변경하거나 어떤 동작을 실행할 때, Observer 들에게 이벤트 알림(notify)을 발행.

- IObserver

: 구독자들을 묶는 인터페이스(다형성)

- Observer

: 관찰자/구독자/알림 수신자.

: Obserber들은 Subject가 발행한 알림에 대해 현재 상태를 취득한다.

: Subject의 업데이트에 대한 전후 정보를 처리한다.

 

2) 소스코드(C#)

- 패턴 적용 전

using System;
using System.Collections.Generic;
using System.Threading;

namespace StudyCSharp
{    
    class Target
    {
        double hp; // 체력
        public double Hp { get => hp; }

        double atk; // 공격력        
        public double Atk { get => atk; }

        double def; // 방어력
        public double Def { get => def; }

        public void ChangeTarget()
        {
            Random rand = new Random();

            // 너무 빠르게 처리되어 같은 시드의 난수를 얻는 것을 방지하기 위함.
            Thread.Sleep(1);

            // 타겟의 스탯이 랜덤하게 변한다고 생각.
            hp = Math.Round(rand.NextDouble() * 1000, 2);
            atk = Math.Round(rand.NextDouble() * 1000, 2);
            def = Math.Round(rand.NextDouble() * 1000, 2);
        }
    }

    interface IUser
    {
        void display();
    }

    class User : IUser
    {
        Target _target;
        string _name;

        public User(Target target, string name)
        {
            this._target = target;
            this._name = name;
        }

        public void display()
        {
            Console.WriteLine("{0}님이 현재 타겟의 스탯을 관찰합니다.", _name);
            Console.WriteLine("체력 : " + _target.Hp);
            Console.WriteLine("공격력 : " + _target.Atk);
            Console.WriteLine("방어력 : " + _target.Def);
            Console.WriteLine();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Target target = new Target();

            List<User> users = new List<User>
            {
                new User(target, "1번 구독자"),
                new User(target, "2번 구독자"),
                new User(target, "3번 구독자")
            };

            // 새로운 타겟으로 갱신됨
            target.ChangeTarget();

            // 받은 정보 출력
            foreach (IUser user in users)
                user.display();

            Console.WriteLine("---------------------------------------------");

            // 새로운 타겟으로 갱신
            target.ChangeTarget();

            // 받은 정보 출력
            foreach (IUser user in users)
                user.display();

            Console.WriteLine("---------------------------------------------");
        }
    }
}

 

- 실행 결과

 

- 패턴 적용 후

using System;
using System.Collections.Generic;
using System.Threading;

namespace StudyCSharp
{
    interface Subject
    {
        void registerObserver(Observer o); // 구독 추가

        void removeObserver(Observer o); // 구독 취소

        void notifyObservers(); // 상태 변화를 모든 Oberser들에게 알림
    }


    class Target : Subject
    {
        // 구독자 리스트
        List<Observer> subscribers = new List<Observer>();

        double hp; // 체력
        public double Hp { get => hp; }

        double atk; // 공격력        
        public double Atk { get => atk; }

        double def; // 방어력
        public double Def { get => def; }

        public void ChangeTarget()
        {
            Random rand = new Random();

            // 너무 빠르게 처리되어 같은 시드의 난수를 얻는 것을 방지하기 위함.
            Thread.Sleep(1);

            // 타겟의 스탯이 랜덤하게 변한다고 생각.
            hp = Math.Round(rand.NextDouble() * 1000, 2);
            atk = Math.Round(rand.NextDouble() * 1000, 2);
            def = Math.Round(rand.NextDouble() * 1000, 2);

            // 구독자들에게 발행(알림)
            notifyObservers();
        }

        public void registerObserver(Observer o)
        {
            subscribers.Add(o);
        }

        public void removeObserver(Observer o)
        {
            subscribers.Remove(o);
        }

        public void notifyObservers()
        {
            foreach (Observer o in subscribers)
                o.display(this);
        }
    }

    interface Observer
    {
        void display(Target target);
    }

    class User : Observer
    {
        string _name;

        public User(string name)
        {
            this._name = name;
        }

        public void display(Target target)
        {
            Console.WriteLine("{0}님이 현재 타겟의 스탯을 관찰합니다.", _name);
            Console.WriteLine("체력 : " + target.Hp);
            Console.WriteLine("공격력 : " + target.Atk);
            Console.WriteLine("방어력 : " + target.Def);
            Console.WriteLine();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Target target = new Target();

            // 구독자 추가
            target.registerObserver(new User("1번 구독자"));
            target.registerObserver(new User("2번 구독자"));
            target.registerObserver(new User("3번 구독자"));

            // 새로운 타겟으로 갱신됨
            target.ChangeTarget();

            Console.WriteLine("---------------------------------------------");

            // 새로운 타겟으로 갱신
            target.ChangeTarget();

            Console.WriteLine("---------------------------------------------");
        }
    }
}

 

- 실행 결과


<참고 사이트>

 

(디자인 패턴) 옵저버 패턴이란 (Observer Pattern)

✋ 옵저버 패턴이란 (Observer Pattern) 옵저버 패턴 (Observer) 관찰자 이벤트가 발생하면 각 옵저버는 콜백을 받습니다. ⚡️ 옵저버란 옵저버 패턴(observer pattern)은 객체의 상태 변화를 관찰하는 옵저

zerocodings.com

 

💠 옵저버(Observer) 패턴 - 완벽 마스터하기

Observer Pattern 옵저버 패턴(Observer Pattern)은 옵저버(관찰자)들이 관찰하고 있는 대상자의 상태가 변화가 있을 때마다 대상자는 직접 목록의 각 관찰자들에게 통지하고, 관찰자들은 알림을 받아 조

inpa.tistory.com

 

 

옵서버 패턴 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 옵서버 패턴(observer pattern)은 객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체

ko.wikipedia.org