[20251224-막간] Observer Pattern

SmartBear·2025년 12월 24일
post-thumbnail

관찰자 (디자인) 패턴

관찰자(디자인)패턴이란?

수업시간에 배운 Delegate를 실 사용하는 디자인 중 "관찰자"가 포함된 패턴으로 이해된다. 즉,


이런 느낌이라 볼 수 있다.

관찰자 패턴은 크게 관찰자주체로 나눌 수 있다

  • 주체(Subject): 상태를 관리하며 상태 변화시 옵저버들에게 알람을 보내는 역할.
  • 관찰자(Observer): 주체의 상태 변화를 관찰. 변경사항 수신. Subscriber라고도 한다. (구독하기 때문에?)

즉, 관찰자Listener이다. 서버-클라 모델에서 서버를 생각하면 보다 쉽게 이해 할 수 있다.
관찰자주체의 상태에 의해 수동적으로 반응하는 개념이다.

🤔 보다보니...

사실 이것은 디자인이다. 뭔가 특별한 클래스나 함수를 쓰는 것은 아니다.
언어에서 제공하는 어떤 기능을 사용하여 할 것인지는 내가 정하기 나름이다.
Delegate를 배우며 이미 이러한 내용에 대한 언급을 강사님이 하셨지만, 직접적으로 관찰자 디자인이라는 표현은 쓰지 않으셨다.
아니, 오히려 Delegate를 이해하려면 관찰자 디자인적으로 설명하는 것이 이해가 빠를 것 같아 그렇게 이야기 하신게 아닌가 싶다.

구현시 주의 사항

  • 먼저, Subject 에는 알람을 보낼 Subscriber 들을 관리할 필요가 있다. 이것 이외에는 Subscriber에서 구현되는 내용에 대해서는 일체 독립적이어야 한다. Subscriber 또한 Subject를 직접적으로 수정해야 하는 일이 발생시켜서는 안된다.
  • Subscriber는 필시 subscriberunsubscriber (구독과 구취) 를 구현해주어야 한다. (솔직히 하다보면 그렇게 되기 마련 이었다..)
  • 사용하지 않게된 Subscriber는 없애주자.
    • GC 를 돌리면 없어진다. 바로는 사라지지 않으니 주의.

코드 예시

Program.cs

시나리오를 진행한 스크립트.

  • 시나리오 설명
  • 우편함에 우편이나 소포가 들어올때마다 핸드폰과 PC 에 알람을 보내는 프로그램.
  • 우편함은 하던일 하고 있고, 핸드폰과 PC 는 알람을 받고 있음.
  • 우편 및 소포를 받음
  • 몇몇의 우편 및 소포를 꺼냄
  • 너무 시끄러워서 PC 알람은 꺼버림
  • 다시 몇몇의 우편 및 소포를 꺼냄
  • 중간중간 뭐가 있는지 전체 리스트 확인
using System;
namespace ObserverPatternTest
{
    class Program
    {
        static void Main(string[] args)
        {            
        MailBox mailBox = new MailBox();
        Subscriber subscriber01 = new Subscriber("핸드폰", mailBox);
        Subscriber subscriber02 = new Subscriber("PC", mailBox);
        
        mailBox.ShowAll();

        mailBox.AddMail(new Post("지로영수증", "썼으니 돈 내야지? by 정부"));
        mailBox.AddMail(new Box("개사료", 5));
        mailBox.AddMail(new Post("위반과태료", "XXX 동네에서 불법 주차 하였습니다.\n과태로: 10만원"));
        mailBox.AddMail(new Box("2L생수 6개", 12));
        mailBox.AddMail(new Post("안내장", "고백 공격은 그 누구도 용서치 않을 것 입니다."));
        mailBox.AddMail(new Box("레고", 2));
        mailBox.AddMail(new Post("아침 인사", "안녕? 굳세고 좋은 아침. 내이름은 왈드"));
        mailBox.AddMail(new Box("책", 3));
        
        mailBox.ShowAll();
        
        mailBox.RemoveMail(1);
        
        mailBox.ShowAll();
        
        Console.WriteLine($"!!! {subscriber02.Name}의 알람을 그만 받겠다 !!!");
        subscriber02.Unsubscribe();
        subscriber02 = null;
        GC.Collect();
        mailBox.RemoveMail(2);
        mailBox.RemoveMail(0);
        mailBox.ShowAll();
        }
    }
}

MailBox.cs

Subject인 주체를 구현한 내용. 즉, Mail 관련 데이터와 MailBox 에 대한 구현체.

// MailBox.cs

public enum MailType
{
    Post,
    Box
}

interface IMailInformation
{
    void Show();
}

public class Mail
{
    public MailType mailType
    {
        get;
        protected set;
    }
    public string Subject { get; set; }
    public Mail(string subject)
    {
        Subject = subject;
    }
}

public class Post : Mail, IMailInformation
{
   
    public string Body { get; set; }
    public Post(string subject, string body) : base(subject)
    {
        mailType = MailType.Post;
        Body = body;
    }

    public void Show()
    {
        Console.WriteLine("-------------------");
        Console.WriteLine(Subject);
        Console.WriteLine("===================");
        Console.WriteLine(Body);
        Console.WriteLine("--------------------");
    }
}

public class Box : Mail, IMailInformation
{
    public int Weight { get; set; }
    public Box(string subject, int weight) : base(subject)
    {
        mailType = MailType.Box;
        Weight = weight;
    }

    public void Show()
    {
        Console.WriteLine("-------------------");
        Console.WriteLine(Subject);
        Console.WriteLine("===================");
        Console.WriteLine($"소포 무게: {Weight}");
        Console.WriteLine("--------------------");
    }
}

public class MailBox
{
    public delegate void SubscriberDel<T>(string action, T mail);
    
    public Mail[] Mails =  new Mail[10];
    private int Index = 0;
    public event SubscriberDel<Mail> Subscribers;

    public int AddMail(Mail mail)
    {
        Mails[Index] = mail;
        Index++;
        Subscribers?.Invoke("추가", mail);  // 트리거 발생!
        return Index - 1;
    }

    public void RemoveMail(int index)
    {
        Mail removedMail = Mails[index];
        Mails[index] = null;
        for (int i = index + 1; i < Mails.Length; i++) {
            
            if (Mails[i] == null) break;
            Mails[i - 1] = Mails[i];
        }
        Index--;
        Mails[Index] = null;
        Subscribers?.Invoke("삭제", removedMail);  // 트리거 발생!
    }

    public void ShowAll()
    {
        Console.WriteLine("===========================");
        if (Mails[0] == null)
        {
            Console.WriteLine("우편함이 비어 있습니다.");
        }
        else
        {
            Console.WriteLine("우편함 목록은 아래와 같습니다.");
            Console.WriteLine("---------------------------");
        }
        for (int i = 0; i < Mails.Length; i++)
        {
            if (Mails[i] == null) break;
            Console.WriteLine($"{i}:[{Mails[i].mailType}]: {Mails[i].Subject}");
        }
        Console.WriteLine("===========================");
    }
}

Subscriber.cs

관찰자(Subscriber) 를 구현한 내용.
간단히 알람을 보낸다고 구현한 내용이다.
Box 와 Post 가 서로 다른 출력물을 나게 하였다.

// Subscriber.cs
interface ISubscriber
{
    void Subscribe();
    void Unsubscribe();
}

public class Subscriber : ISubscriber
{
    private MailBox _mailBox;
    public string Name;
    public Subscriber(string name, MailBox mailbox)
    {
        Name = name;
        _mailBox = mailbox;
        Subscribe(); // 일부러 기능을 보여주기 위해 method 로 분리하고 생성자에 포함했다.
    }
    public void Subscribe()
    {  // 구독
        _mailBox.Subscribers += GetNoti;
    }
    public void Unsubscribe()  
    {  // 구취
        _mailBox.Subscribers -= GetNoti;
    }
    public void GetNoti<T>(string action, T mail) where T : Mail
    {   // 알람을 받았을 때의 행동
        if (mail is Box)
        {
            ShowBox(action, mail as Box);
        }
        else if (mail is Post)
        {
            ShowPost(action, mail as Post);
        }
    }

    public void ShowBox(string action, Box box)
    {
        Console.WriteLine($">>>\n{Name}(이)가 알립니다.");
        Console.WriteLine($"--- 아래 항목이 {action} 되었습니다. ---");
        Console.WriteLine($"{box.Subject} | {box.Weight} kg\n<<<");
    }

    public void ShowPost(string action, Post post)
    {
        Console.WriteLine($">>>\n{Name}(이)가 알립니다.");
        Console.WriteLine($"--- 아래 항목이 {action} 되었습니다. ---");
        Console.WriteLine($"{post.Subject}\n{post.Body}\n<<<");
    }
}

출력 결과

===========================
우편함이 비어 있습니다.
===========================
>>>
핸드폰(이)가 알립니다.
--- 아래 항목이 추가 되었습니다. ---
지로영수증
썼으니 돈 내야지? by 정부
<<<
>>>
PC(이)가 알립니다.
--- 아래 항목이 추가 되었습니다. ---
지로영수증
썼으니 돈 내야지? by 정부
<<<
>>>
핸드폰(이)가 알립니다.
--- 아래 항목이 추가 되었습니다. ---
개사료 | 5 kg
<<<
>>>
PC(이)가 알립니다.
--- 아래 항목이 추가 되었습니다. ---
개사료 | 5 kg
<<<
>>>
핸드폰(이)가 알립니다.
--- 아래 항목이 추가 되었습니다. ---
위반과태료
XXX 동네에서 불법 주차 하였습니다.
과태로: 10만원
<<<
>>>
PC(이)가 알립니다.
--- 아래 항목이 추가 되었습니다. ---
위반과태료
XXX 동네에서 불법 주차 하였습니다.
과태로: 10만원
<<<
>>>
핸드폰(이)가 알립니다.
--- 아래 항목이 추가 되었습니다. ---
2L생수 6개 | 12 kg
<<<
>>>
PC(이)가 알립니다.
--- 아래 항목이 추가 되었습니다. ---
2L생수 6개 | 12 kg
<<<
>>>
핸드폰(이)가 알립니다.
--- 아래 항목이 추가 되었습니다. ---
안내장
고백 공격은 그 누구도 용서치 않을 것 입니다.
<<<
>>>
PC(이)가 알립니다.
--- 아래 항목이 추가 되었습니다. ---
안내장
고백 공격은 그 누구도 용서치 않을 것 입니다.
<<<
>>>
핸드폰(이)가 알립니다.
--- 아래 항목이 추가 되었습니다. ---
레고 | 2 kg
<<<
>>>
PC(이)가 알립니다.
--- 아래 항목이 추가 되었습니다. ---
레고 | 2 kg
<<<
>>>
핸드폰(이)가 알립니다.
--- 아래 항목이 추가 되었습니다. ---
아침 인사
안녕? 굳세고 좋은 아침. 내이름은 왈드
<<<
>>>
PC(이)가 알립니다.
--- 아래 항목이 추가 되었습니다. ---
아침 인사
안녕? 굳세고 좋은 아침. 내이름은 왈드
<<<
>>>
핸드폰(이)가 알립니다.
--- 아래 항목이 추가 되었습니다. ---
책 | 3 kg
<<<
>>>
PC(이)가 알립니다.
--- 아래 항목이 추가 되었습니다. ---
책 | 3 kg
<<<
===========================
우편함 목록은 아래와 같습니다.
---------------------------
0:[Post]: 지로영수증
1:[Box]: 개사료
2:[Post]: 위반과태료
3:[Box]: 2L생수 6개
4:[Post]: 안내장
5:[Box]: 레고
6:[Post]: 아침 인사
7:[Box]: 책
===========================
>>>
핸드폰(이)가 알립니다.
--- 아래 항목이 삭제 되었습니다. ---
개사료 | 5 kg
<<<
>>>
PC(이)가 알립니다.
--- 아래 항목이 삭제 되었습니다. ---
개사료 | 5 kg
<<<
===========================
우편함 목록은 아래와 같습니다.
---------------------------
0:[Post]: 지로영수증
1:[Post]: 위반과태료
2:[Box]: 2L생수 6개
3:[Post]: 안내장
4:[Box]: 레고
5:[Post]: 아침 인사
6:[Box]: 책
===========================
!!! PC의 알람을 그만 받겠다 !!!
>>>
핸드폰(이)가 알립니다.
--- 아래 항목이 삭제 되었습니다. ---
2L생수 6개 | 12 kg
<<<
>>>
핸드폰(이)가 알립니다.
--- 아래 항목이 삭제 되었습니다. ---
지로영수증
썼으니 돈 내야지? by 정부
<<<
===========================
우편함 목록은 아래와 같습니다.
---------------------------
0:[Post]: 위반과태료
1:[Post]: 안내장
2:[Box]: 레고
3:[Post]: 아침 인사
4:[Box]: 책
===========================

그럼 왜 굳이 Delegate 를 쓸까?

사실, 일반 Class 로 구현 및 생성자나 함수로 받아 직접 배열로 관리하거나 하는 방법도 있긴 하다. 하지만 그렇게 되면 한쪽 코드를 수정할 때 다른 쪽 코드를 수정해야 하는 케이스도 자주 발생하게 된다. 또한 관찰자에 대한 관리도 많이 복잡해 진다.
Delegate 는 이러한 독립성과 복잡성을 쉽게 해결해 주는 강력한 기능이다.


그럼에도 굳이 안 쓸 이유가..? 😆

profile
Python Dev with Infra -> Game Programmer

0개의 댓글