옵저버 패턴
1. 정의
옵저버 패턴은 행동 디자인 패턴 중 하나로, 한 객체의 상태 변화가 다른 객체들에 의해 자동으로 감지되고 업데이트될 수 있도록 일대다 종속성을 정의합니다. 주로 분산 이벤트 처리 시스템을 구현할 때 사용됩니다. 주체(Subject)는 상태를 가지고 있으며, 옵저버(Observer)는 주체의 상태 변화를 감지하고 적절히 반응하는 객체들입니다.
2. 사용 이유와 목적
옵저버 패턴은 다음과 같은 상황에서 유용합니다:
- 다수의 객체가 특정 객체의 상태 변화를 실시간으로 알아야 하는 경우: 예를 들어, 주식 가격 변동을 여러 클라이언트에게 실시간으로 알리고 싶을 때.
- 동적인 의존성 관계가 필요한 경우: 객체들 간의 관계를 실행 중에 동적으로 설정하거나 변경해야 할 때.
- 느슨한 결합을 유지하고 싶을 때: 주체와 옵저버가 서로 직접적으로 참조하지 않고, 인터페이스를 통해 통신함으로써 모듈화를 높이고 유지보수를 쉽게 할 수 있습니다.
3. 사용 예시
using System;
using System.Collections.Generic;
// 주체 클래스
public class Subject
{
private List<IObserver> observers = new List<IObserver>();
private int state;
public int State
{
get { return state; }
set
{
state = value;
NotifyAllObservers();
}
}
public void Attach(IObserver observer)
{
observers.Add(observer);
}
public void Detach(IObserver observer)
{
observers.Remove(observer);
}
public void NotifyAllObservers()
{
foreach (var observer in observers)
{
observer.Update();
}
}
}
// 옵저버 인터페이스
public interface IObserver
{
void Update();
}
// 구체적인 옵저버 클래스
public class ConcreteObserver : IObserver
{
private string name;
private Subject subject;
public ConcreteObserver(string name, Subject subject)
{
this.name = name;
this.subject = subject;
this.subject.Attach(this);
}
public void Update()
{
Console.WriteLine($"옵저버 {name}가 알림을 받았습니다. 새로운 상태는 {subject.State}입니다.");
}
}
// 사용 예시
class Program
{
static void Main()
{
Subject subject = new Subject();
ConcreteObserver observer1 = new ConcreteObserver("옵저버1", subject);
ConcreteObserver observer2 = new ConcreteObserver("옵저버2", subject);
Console.WriteLine("첫 번째 상태 변화: 15");
subject.State = 15;
Console.WriteLine("두 번째 상태 변화: 10");
subject.State = 10;
}
}
이 예제에서 Subject 클래스는 주체로, 상태를 변경하고 모든 옵저버에게 알림을 보냅니다. IObserver 인터페이스는 옵저버들이 구현해야 하는 Update 메서드를 정의합니다. ConcreteObserver 클래스는 실제로 상태 변화를 감지하고 반응하는 구체적인 옵저버입니다.
4. 장점
- 느슨한 결합: 주체와 옵저버 간의 의존성을 최소화하여 시스템의 모듈화와 재사용성을 높입니다. 예를 들어, 옵저버가 변경되어도 주체는 영향을 받지 않습니다.
- 확장성: 새로운 옵저버를 추가할 때 주체 코드를 수정할 필요가 없습니다. 예를 들어, 주식 가격을 모니터링하는 새로운 클라이언트를 추가할 때 기존 주체 코드를 수정하지 않아도 됩니다.
- 재사용성: 주체와 옵저버를 각각 독립적으로 재사용할 수 있습니다. 예를 들어, 동일한 주체가 다양한 종류의 옵저버와 함께 사용될 수 있습니다.
- 이벤트 기반 시스템 구현: UI 이벤트(MVP/MVC), 네트워크 이벤트, 데이터 변경 이벤트 등 다양한 이벤트 기반 시스템을 쉽게 구현할 수 있습니다.
5. 단점
- 복잡성 증가: 시스템에 많은 옵저버와 주체가 존재할 경우, 상호작용의 복잡성이 증가할 수 있습니다. 예를 들어, 주체가 많은 옵저버에게 알림을 보낼 때 순서나 타이밍 문제가 발생할 수 있습니다.
- 메모리 누수: 옵저버가 주체에서 제대로 제거되지 않으면 메모리 누수가 발생할 수 있습니다. 예를 들어, 옵저버가 더 이상 필요 없을 때 주체의 리스트에서 제거되지 않으면 메모리 누수가 발생합니다.
- 예측 불가능한 동작: 옵저버들이 알림을 받는 순서가 예측 불가능할 수 있어, 의도하지 않은 부작용이 발생할 수 있습니다. 예를 들어, 한 옵저버가 다른 옵저버에 의존하는 경우 알림 순서가 중요할 수 있습니다.
- 디버깅 어려움: 이벤트가 객체에 의해 처리되기 때문에 디버깅과 로그 추적이 어려울 수 있습니다.
결론
옵저버 패턴은 C#에서 객체들 간의 종속성을 느슨하게 관리하기 위한 강력한 도구입니다. 이를 이해하고 장단점을 균형 있게 고려함으로써 유연하고 유지보수하기 쉬운 소프트웨어 시스템을 만들 수 있습니다. 예를 들어, 주식 거래 시스템, 실시간 알림 시스템, 데이터 바인딩이 필요한 UI 애플리케이션 등 다양한 분야에서 유용하게 사용될 수 있습니다.
단점 극복 방법
1. 메모리 누수 방지
메모리 누수를 방지하기 위해 약한 참조(Weak Reference)를 사용할 수 있습니다. C#에서는 WeakReference 클래스를 활용해 옵저버에 대한 참조를 약하게 만들어 주체가 옵저버를 강하게 참조하지 않도록 할 수 있습니다.
using System;
using System.Collections.Generic;
// 옵저버 인터페이스
public interface IObserver
{
void Update();
}
// 주체 클래스
public class Subject
{
private List<WeakReference<IObserver>> observers = new List<WeakReference<IObserver>>();
private int state;
public int State
{
get { return state; }
set
{
state = value;
NotifyAllObservers();
}
}
public void Attach(IObserver observer)
{
observers.Add(new WeakReference<IObserver>(observer));
}
public void Detach(IObserver observer)
{
observers.RemoveAll(weakRef =>
{
IObserver target;
return weakRef.TryGetTarget(out target) && target == observer;
});
}
public void NotifyAllObservers()
{
foreach (var weakRef in observers)
{
IObserver observer;
if (weakRef.TryGetTarget(out observer))
{
observer.Update();
}
}
}
}
// 구체적인 옵저버 클래스
public class ConcreteObserver : IObserver
{
private string name;
private Subject subject;
public ConcreteObserver(string name, Subject subject)
{
this.name = name;
this.subject = subject;
this.subject.Attach(this);
}
public void Update()
{
Console.WriteLine($"옵저버 {name}가 알림을 받았습니다. 새로운 상태는 {subject.State}입니다.");
}
}
// 사용 예시
class Program
{
static void Main()
{
Subject subject = new Subject();
ConcreteObserver observer1 = new ConcreteObserver("옵저버1", subject);
ConcreteObserver observer2 = new ConcreteObserver("옵저버2", subject);
Console.WriteLine("첫 번째 상태 변화: 15");
subject.State = 15;
Console.WriteLine("두 번째 상태 변화: 10");
subject.State = 10;
subject.Detach(observer1);
Console.WriteLine("세 번째 상태 변화: 5");
subject.State = 5;
}
}
2. 예측 가능한 알림 순서
알림 순서를 예측 가능하게 하려면 옵저버를 추가할 때 우선순위를 지정할 수 있습니다.
using System;
using System.Collections.Generic;
using System.Linq;
public interface IObserver
{
void Update();
}
public class Subject
{
private SortedList<int, List<IObserver>> observers = new SortedList<int, List<IObserver>>();
private int state;
public int State
{
get { return state; }
set
{
state = value;
NotifyAllObservers();
}
}
public void Attach(IObserver observer, int priority = 0)
{
if (!observers.ContainsKey(priority))
{
observers[priority] = new List<IObserver>();
}
observers[priority].Add(observer);
}
public void Detach(IObserver observer)
{
foreach (var observerList in observers.Values)
{
observerList.Remove(observer);
}
}
public void NotifyAllObservers()
{
foreach (var observerList in observers.OrderByDescending(x => x.Key).Select(x => x.Value))
{
foreach (var observer in observerList)
{
observer.Update();
}
}
}
}
public class ConcreteObserver : IObserver
{
private string name;
private Subject subject;
public ConcreteObserver(string name, Subject subject, int priority = 0)
{
this.name = name;
this.subject = subject;
this.subject.Attach(this, priority);
}
public void Update()
{
Console.WriteLine($"옵저버 {name}가 알림을 받았습니다. 새로운 상태는 {subject.State}입니다.");
}
}
class Program
{
static void Main()
{
Subject subject = new Subject();
ConcreteObserver observer1 = new ConcreteObserver("옵저버1", subject, 2);
ConcreteObserver observer2 = new ConcreteObserver("옵저버2", subject, 1);
Console.WriteLine("첫 번째 상태 변화: 15");
subject.State = 15;
Console.WriteLine("두 번째 상태 변화: 10");
subject.State = 10;
}
}
3. 디버깅 용이성 향상
디버깅을 쉽게 하기 위해 로깅을 추가할 수 있습니다. 이는 주체와 옵저버의 상태 변화를 추적하는 데 도움이 됩니다.
using System;
using System.Collections.Generic;
public interface IObserver
{
void Update();
}
public class Subject
{
private List<IObserver> observers = new List<IObserver>();
private int state;
public int State
{
get { return state; }
set
{
state = value;
Console.WriteLine($"주체의 상태가 {state}로 변경되었습니다.");
NotifyAllObservers();
}
}
public void Attach(IObserver observer)
{
observers.Add(observer);
Console.WriteLine($"{observer.GetType().Name}이(가) 주체에 추가되었습니다.");
}
public void Detach(IObserver observer)
{
observers.Remove(observer);
Console.WriteLine($"{observer.GetType().Name}이(가) 주체에서 제거되었습니다.");
}
public void NotifyAllObservers()
{
foreach (var observer in observers)
{
observer.Update();
}
}
}
public class ConcreteObserver : IObserver
{
private string name;
private Subject subject;
public ConcreteObserver(string name, Subject subject)
{
this.name = name;
this.subject = subject;
this.subject.Attach(this);
}
public void Update()
{
Console.WriteLine($"옵저버 {name}가 알림을 받았습니다. 새로운 상태는 {subject.State}입니다.");
}
}
class Program
{
static void Main()
{
Subject subject = new Subject();
ConcreteObserver observer1 = new ConcreteObserver("옵저버1", subject);
ConcreteObserver observer2 = new ConcreteObserver("옵저버2", subject);
Console.WriteLine("첫 번째 상태 변화: 15");
subject.State = 15;
Console.WriteLine("두 번째 상태 변화: 10");
subject.State = 10;
subject.Detach(observer1);
Console.WriteLine("세 번째 상태 변화: 5");
subject.State = 5;
}
}
이와 같이 약한 참조를 사용하거나, 우선순위를 지정하거나, 로깅을 추가하는 등의 방법을 통해 옵저버 패턴의 단점을 효과적으로 극복할 수 있습니다. 이를 통해 시스템의 안정성과 유지보수성을 높일 수 있습니다.
'개발💻 > C#' 카테고리의 다른 글
[C#] readonly와 const의 차이 (0) | 2024.10.05 |
---|---|
[C#] Coupling, Cohesion 1 (0) | 2024.08.10 |
[C#] Delegate 2 (0) | 2024.06.24 |
[C#] Delegate 1 (0) | 2024.06.08 |
[C#] Using 지시문 (2) | 2022.08.24 |