C#에서 옵저버 패턴 사용하기
Observer Pattern
객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 1:N 의존성을 정의합니다.
푸시 기반 알림이 필요한 곳에 사용
신문사 + 독자 = 옵저버패턴
의존성: 데이터의 주인은 주제이다. 옵저버는 데이터가 변경되었을 때 주제에서 갱신해 주기를 기다린다.
1:N: 상태를 저장하고 지배하는 것은 주제 객체이다.
디자인 원칙: 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.
느슨한 결합(Loose Coupling)
- 주제가 옵저버에 대해서 아는 것은 옵저버가 특정 인터페이스(Observer Interface)를 구현한다는 것 뿐이다.
- 옵저버는 언제든지 새로 추가 할 수 있다.
- 새로운 형식의 옵저버를 추가하려고 할 때도 주제를 전혀 변경할 필요가 없다.
- 주제와 옵저버는 서로 독립적으로 재 사용할 수 있다.
- 주제나 옵저버가 바뀌더라도 서로에게 영향을 미치지 않는다.
패턴 적용
- 옵저버에게 알림을 전송하는 개체인 주제객체 또는 주제객체. 주제객체는 IObservable<T> 인터페이스를 구현하는 클래스 또는 구조체입니다. 주제객체는 주제객체로부터 알림을 수신하려는 옵저버가 호출하는 단일 메서드IObservable<T>.Subscribe를 구현해야 합니다.
주제객체로부터 알림을 수신하는 개체인 옵저버. 옵저버는
IObserver<T> 인터페이스를 구현하는 클래스 또는 구조체입니다. 옵저버는 모두 주제객체에 의해 호출되는 다음 세 개의 메서드를 구현해야 합니다
- IObserver<T>.OnNext - 옵저버에게 새 정보나 현재 정보를 제공합니다.
- IObserver<T>.OnError - 오류가 발생했음을 옵저버에게 알립니다.
- IObserver<T>.OnCompleted - 주제객체가 알림 전송을 완료했음을 나타냅니다.
- 일반적으로 옵저버는 System.Collections.Generic.List<T> 개체와 같은 컨테이너 개체를 사용하여 알림을 구독한 IObserver<T> 구현에 대한 참조를 보유합니다. 이 목적으로 저장소 컨테이너를 사용하면 주제객체가 0개에서 무한대 개수까지 옵저버를 처리할 수 있습니다. 옵저버가 알림을 수신하는 순서는 정의되지 않습니다. 주제객체가 임의 메서드를 사용하여 순서를 결정할 수 있습니다.
- 알림이 완료될 때 주제객체가 옵저버를 제거할 수 있도록 하는 IDisposable 구현. 옵저버는 Subscribe 메서드로부터 IDisposable 구현에 대한 참조를 수신하므로 주제객체가 알림 전송을 완료하기 전에 IDisposable.Dispose 메서드를 호출하여 구독을 취소할 수도 있습니다.
- 주제객체가 해당 옵저버에게 전송하는 데이터를 포함하는 개체. 이 개체의 형식은 IObservable<T> 및 IObserver<T> 인터페이스의 제네릭 형식 매개 변수에 해당합니다. 이 개체는 IObservable<T> 구현과 동일할 수도 있지만 일반적으로 별도 형식입니다.
패턴 구현
객체
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 | using System;
using System.Collections.Generic;
public class BaggageInfo
{
private int flightNo;
private string origin;
private int location;
internal BaggageInfo(int flight, string from, int carousel)
{
this.flightNo = flight;
this.origin = from;
this.location = carousel;
}
public int FlightNumber {
get { return this.flightNo; }
}
public string From {
get { return this.origin; }
}
public int Carousel {
get { return this.location; }
}
} | cs |
BaggageInfo 클래스는 도착 항공편과 각 항공편의 수하물을 찾을 수 있는 컨베이어 벨트에 대한 정보를 제공합니다.
주제객체
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64 | public class BaggageHandler : IObservable<BaggageInfo>
{
private List<IObserver<BaggageInfo>> observers;
private List<BaggageInfo> flights;
public BaggageHandler()
{
observers = new List<IObserver<BaggageInfo>>();
flights = new List<BaggageInfo>();
}
public IDisposable Subscribe(IObserver<BaggageInfo> observer)
{
// Check whether observer is already registered. If not, add it
if (! observers.Contains(observer)) {
observers.Add(observer);
// Provide observer with existing data.
foreach (var item in flights)
observer.OnNext(item);
}
return new Unsubscriber<BaggageInfo>(observers, observer);
}
// Called to indicate all baggage is now unloaded.
public void BaggageStatus(int flightNo)
{
BaggageStatus(flightNo, String.Empty, 0);
}
public void BaggageStatus(int flightNo, string from, int carousel)
{
var info = new BaggageInfo(flightNo, from, carousel);
// Carousel is assigned, so add new info object to list.
if (carousel > 0 && ! flights.Contains(info)) {
flights.Add(info);
foreach (var observer in observers)
observer.OnNext(info);
}
else if (carousel == 0) {
// Baggage claim for flight is done
var flightsToRemove = new List<BaggageInfo>();
foreach (var flight in flights) {
if (info.FlightNumber == flight.FlightNumber) {
flightsToRemove.Add(flight);
foreach (var observer in observers)
observer.OnNext(info);
}
}
foreach (var flightToRemove in flightsToRemove)
flights.Remove(flightToRemove);
flightsToRemove.Clear();
}
}
public void LastBaggageClaimed()
{
foreach (var observer in observers)
observer.OnCompleted();
observers.Clear();
}
}
Colored by Color Scripter | cs |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | | internal class Unsubscriber<BaggageInfo> : IDisposable
{
private List<IObserver<BaggageInfo>> _observers;
private IObserver<BaggageInfo> _observer;
internal Unsubscriber(List<IObserver<BaggageInfo>> observers, IObserver<BaggageInfo> observer)
{
this._observers = observers;
this._observer = observer;
}
public void Dispose()
{
if (_observers.Contains(_observer))
_observers.Remove(_observer);
}
} | cs |
BaggageHandler 클래스는 도착 항공편 및 수하물을 찾을 수 있는 컨베이어 벨트에 대한 정보를 받아야 합니다. (주제 객체)
내부적으로 다음 두 개의 컬렉션을 유지 관리합니다.
- observers - 업데이트된 정보를 수신할 클라이언트 컬렉션입니다.
- flights - 항공편 및 할당된 컨베이어 벨트 컬렉션입니다.
업데이트된 정보를 수신하려는 옵저버는 BaggageHandler.Subscribe 메서드를 호출합니다. 옵저버가 이전에 알림을 구독하지 않은 경우 클라이언트의 IObserver<T> 구현에 대한 참조가 observers 컬렉션에 추가됩니다.
오버로드된 BaggageHandler.BaggageStatus 메서드를 호출하여 항공편의 수하물을 내리는 중인지 여부를 나타낼 수 있습니다.
내리는 중이면 메서드에 항공편 번호, 출발 공항 및 수하물을 내리는 중인 컨베이어 벨트가 전달됩니다.
더 이상 내리지 않는 경우 메서드에 항공편 번호만 전달됩니다.
수하물을 내리는 경우 메서드는 메서드에 전달된 BaggageInfo 정보가 flights 컬렉션에 있는지 여부를 확인합니다. 그러지 않은 경우 메서드는 정보를 추가하고 각 옵저버의 OnNext 메서드를 호출합니다.
더 이상 수하물을 내리지 않는 항공편의 경우 메서드는 항공편에 대한 정보가 flights 컬렉션에 저장되었는지 여부를 확인합니다.
저장된 경우 메서드는 각 옵저버의 OnNext 메서드를 호출하고 flights 컬렉션에서 BaggageInfo 개체를 제거합니다.
그날의 마지막 항공편이 착륙하고 해당 수하물이 처리되면 BaggageHandler.LastBaggageClaimed 메서드가 호출됩니다.
이 메서드는 각 옵저버의 OnCompleted 메서드를 호출하여 모든 알림이 완료되었음을 나타내고 observers 컬렉션을 지웁니다.
주제객체의 Subscribe 메서드는 OnCompleted 메서드가 호출되기 전에 옵저버가 알림 수신을 중지할 수 있도록 하는 IDisposable 구현을 반환합니다.
BaggageHandler.Subscribe 메서드에서 클래스가 인스턴스화되면 observers 컬렉션에 대한 참조 및 컬렉션에 추가된 옵저버에 대한 참조가 전달됩니다.
이러한 참조는 지역 변수에 할당됩니다. 개체의 Dispose 메서드가 호출되면 옵저버가 observers 컬렉션에 여전히 있는지 여부를 확인하고, 있을 경우 옵저버를 제거합니다.
옵저버
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79 | using System;
using System.Collections.Generic;
public class ArrivalsMonitor : IObserver<BaggageInfo>
{
private string name;
private List<string> flightInfos = new List<string>();
private IDisposable cancellation;
private string fmt = "{0,-20} {1,5} {2, 3}";
public ArrivalsMonitor(string name)
{
if (String.IsNullOrEmpty(name))
throw new ArgumentNullException("The observer must be assigned a name.");
this.name = name;
}
public virtual void Subscribe(BaggageHandler provider)
{
cancellation = provider.Subscribe(this);
}
public virtual void Unsubscribe()
{
cancellation.Dispose();
flightInfos.Clear();
}
public virtual void OnCompleted()
{
flightInfos.Clear();
}
// No implementation needed: Method is not called by the BaggageHandler class.
public virtual void OnError(Exception e)
{
// No implementation.
}
// Update information.
public virtual void OnNext(BaggageInfo info)
{
bool updated = false;
// Flight has unloaded its baggage; remove from the monitor.
if (info.Carousel == 0) {
var flightsToRemove = new List<string>();
string flightNo = String.Format("{0,5}", info.FlightNumber);
foreach (var flightInfo in flightInfos) {
if (flightInfo.Substring(21, 5).Equals(flightNo)) {
flightsToRemove.Add(flightInfo);
updated = true;
}
}
foreach (var flightToRemove in flightsToRemove)
flightInfos.Remove(flightToRemove);
flightsToRemove.Clear();
}
else {
// Add flight if it does not exist in the collection.
string flightInfo = String.Format(fmt, info.From, info.FlightNumber, info.Carousel);
if (! flightInfos.Contains(flightInfo)) {
flightInfos.Add(flightInfo);
updated = true;
}
}
if (updated) {
flightInfos.Sort();
Console.WriteLine("Arrivals information from {0}", this.name);
foreach (var flightInfo in flightInfos)
Console.WriteLine(flightInfo);
Console.WriteLine();
}
}
} | |
수하물 찾는 곳 정보를 표시하는 기본 클래스인 ArrivalsMonitor라는 IObserver<T> 구현을 제공합니다.
ArrivalsMonitor 클래스에는 Subscribe 및 Unsubscribe 메서드가 포함됩니다.
Subscribe 메서드를 통해 클래스는 Subscribe 호출에서 반환된 IDisposable 구현을 전용 변수에 저장할 수 있습니다.
Unsubscribe 메서드를 통해 클래스는 주제객체의 Dispose 구현을 호출하여 알림 구독을 취소할 수 있습니다.
ArrivalsMonitor에서는 OnNext, OnError 및 OnCompleted 메서드의 구현도 제공합니다.
OnNext 구현에만 상당한 양의 코드가 포함됩니다. 메서드는 도착 항공편의 출발 공항 및 수하물을 찾을 수 있는 컨베이어 벨트에 대한 정보를 유지 관리하는 private, sorted, generic List<T> 개체로 작동합니다.
BaggageHandler 클래스가 새 항공편 도착을 보고하면 OnNext 메서드 구현에서 해당 항공편에 대한 정보를 목록에 추가합니다.
BaggageHandler 클래스가 항공편의 수하물을 내렸다고 보고하면 OnNext 메서드가 목록에서 해당 항공편을 제거합니다. 변경될 때마다 목록이 정렬되고 콘솔에 표시됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71 | using System;
using System.Collections.Generic;
public class Example
{
public static void Main()
{
BaggageHandler provider = new BaggageHandler();
ArrivalsMonitor observer1 = new ArrivalsMonitor("BaggageClaimMonitor1");
ArrivalsMonitor observer2 = new ArrivalsMonitor("SecurityExit");
provider.BaggageStatus(712, "Detroit", 3);
observer1.Subscribe(provider);
provider.BaggageStatus(712, "Kalamazoo", 3);
provider.BaggageStatus(400, "New York-Kennedy", 1);
provider.BaggageStatus(712, "Detroit", 3);
observer2.Subscribe(provider);
provider.BaggageStatus(511, "San Francisco", 2);
provider.BaggageStatus(712);
observer2.Unsubscribe();
provider.BaggageStatus(400);
provider.LastBaggageClaimed();
}
}
// The example displays the following output:
// Arrivals information from BaggageClaimMonitor1
// Detroit 712 3
//
// Arrivals information from BaggageClaimMonitor1
// Detroit 712 3
// Kalamazoo 712 3
//
// Arrivals information from BaggageClaimMonitor1
// Detroit 712 3
// Kalamazoo 712 3
// New York-Kennedy 400 1
//
// Arrivals information from SecurityExit
// Detroit 712 3
//
// Arrivals information from SecurityExit
// Detroit 712 3
// Kalamazoo 712 3
//
// Arrivals information from SecurityExit
// Detroit 712 3
// Kalamazoo 712 3
// New York-Kennedy 400 1
//
// Arrivals information from BaggageClaimMonitor1
// Detroit 712 3
// Kalamazoo 712 3
// New York-Kennedy 400 1
// San Francisco 511 2
//
// Arrivals information from SecurityExit
// Detroit 712 3
// Kalamazoo 712 3
// New York-Kennedy 400 1
// San Francisco 511 2
//
// Arrivals information from BaggageClaimMonitor1
// New York-Kennedy 400 1
// San Francisco 511 2
//
// Arrivals information from SecurityExit
// New York-Kennedy 400 1
// San Francisco 511 2
//
// Arrivals information from BaggageClaimMonitor1
// San Francisco 511 2 | |
옵저버 패턴 사용시 주의점
스레딩
- 일반적으로 주제객체는 컬렉션 개체로 표시되는 옵저버 목록에 특정 옵저버를 추가하여 IObservable<T>.Subscribe 메서드를 구현하며, 옵저버 목록에서 특정 옵저버를 제거하여 IDisposable.Dispose 메서드를 구현합니다. 옵저버는 언제든지 이러한 메서드를 호출할 수 있습니다. 또한 주제객체/옵저버 계약에서는 IObserver<T>.OnCompleted 콜백 메서드 후 구독 취소 담당자를 지정하지 않으므로 주제객체와 옵저버가 모두 목록에서 같은 멤버를 제거하려고 할 수 있습니다. 이러한 가능성 때문에 Subscribe 및 Dispose 메서드는 모두 스레드로부터 안전해야 합니다. 일반적으로는 이를 위해 동시 컬렉션 또는 잠금을 사용합니다. 스레드로부터 안전하지 않은 구현은 스레드로부터 안전하지 않음을 명시적으로 문서화해야 합니다.
예외 처리
- 데이터 주제객체와 옵저버는 느슨하게 결합되므로 옵저버 디자인 패턴의 예외는 정보 제공용으로 사용됩니다. 이는 주제객체와 옵저버가 옵저버 디자인 패턴에서 예외를 처리하는 방식에 영향을 줍니다.
주제객체 - OnError 메서드 호출
- OnError 메서드는 IObserver<T>.OnNext 메서드와 같이 옵저버에 대한 정보 메시지로 사용됩니다. 그러나 OnNext메서드는 현재 또는 업데이트된 데이터를 옵저버에게 제공하는 반면 OnError 메서드는 주제객체가 유효한 데이터를 제공할 수 없음을 나타냅니다.
- 주제객체는 특정 요구 사항이 있는 경우 자체 예외를 처리해야 합니다.
- 주제객체는 옵저버가 특정 방식으로 예외를 처리한다고 예상하거나 처리하도록 요구해서는 안 됩니다.
- 주제객체는 업데이트 제공 기능을 손상시키는 예외를 처리할 때 OnError 메서드를 호출해야 합니다. 이러한 예외에 대한 정보를 옵저버에게 전달할 수 있습니다. 다른 경우에는 옵저버에게 예외에 대해 알릴 필요가 없습니다.
- 주제객체가 OnError 또는 IObserver<T>.OnCompleted 메서드를 호출한 후에는 추가 알림이 표시되지 않아야 하며 주제객체는 해당 옵저버의 구독을 취소할 수 있습니다. 그러나 옵저버는 OnError 또는 IObserver<T>.OnCompleted 알림을 받기 전이나 받은 후를 포함하여 언제든지 직접 구독을 취소할 수 있습니다. 옵저버 디자인 패턴에서는 구독 취소 담당자(주제객체 또는 옵저버)가 지정되지 않으므로 주제객체와 옵저버가 모두 구독 취소를 시도할 수 있습니다. 일반적으로 옵저버는 구독 취소 시 옵저버 컬렉션에서 제거됩니다. 단일 스레드 응용 프로그램에서는 제거를 시도하기 전에 IDisposable.Dispose 구현에서 개체 참조가 유효하며 개체가 옵저버 컬렉션의 멤버임을 확인해야 합니다. 다중 스레드 응용 프로그램에서는 System.Collections.Concurrent.BlockingCollection<T> 개체와 같은 스레드로부터 안전한 컬렉션 개체를 사용해야 합니다.
옵저버 - OnError 메서드를 구현
- 옵저버는 주제객체로부터 오류 알림을 받으면 예외를 정보로 처리해야 하며 특정 작업을 수행하지 않아도 됩니다.
- 옵저버는 주제객체로의 OnError 메서드 호출에 응답할 때 다음 모범 사례를 따라야 합니다.
- 옵저버는 OnNext 또는 OnError 등의 인터페이스 구현에서 예외를 throw해서는 안 됩니다. 옵저버가 예외를 throw하는 경우 해당 예외는 처리되지 않습니다.
- 호출 스택을 유지하려면 OnError 메서드로 전달된 Exception 개체를 throw하려는 옵저버는 예외를 throw하기 전에 래핑해야 합니다. 이렇게 하려면 표준 예외 개체를 사용해야 합니다.
그 외
- IObservable<T>.Subscribe 메서드에서 등록 취소를 시도하면 null 참조가 생성될 수 있으므로 이러한 방식은 사용하지 않는 것이 좋습니다.
- 옵저버 하나를 여러 주제객체에 연결할 수는 있지만, IObserver<T> 인스턴스를 IObservable<T> 인스턴스 하나에만 연결하는 패턴을 사용하는 것이 좋습니다.
참고 자료
- 공급자 구현 : https://msdn.microsoft.com/ko-kr/library/ff506345(v=vs.110).aspx
- 관찰자 구현 : https://msdn.microsoft.com/ko-kr/library/ff506346(v=vs.110).aspx
- 관찰자 디자인 패턴 유용한 정보 : https://msdn.microsoft.com/ko-kr/library/ff519622(v=vs.110).aspx
- 관찰자 디자인 패턴 : https://msdn.microsoft.com/ko-kr/library/ee850490(v=vs.110).aspx
- IDisposable 인터페이스 : https://msdn.microsoft.com/ko-kr/library/system.idisposable(v=vs.110).aspx
- String.Substring 메서드 (Int32, Int32) : https://msdn.microsoft.com/ko-kr/library/aka44szs(v=vs.110).aspx