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("---------------------------------------------");
}
}
}
- 실행 결과
<참고 사이트>