관찰자 디자인 패턴을 사용하면 구독자가 공급자에 등록하고 공급자로부터 알림을 받을 수 있습니다. 푸시 기반 알림이 필요한 모든 시나리오에 적합합니다. 이 패턴은 공급자 ( 주체 또는 관찰 가능 개체라고도 함) 및 0개, 하나 이상의 관찰자를 정의합니다. 관찰자는 공급자에 등록하고 미리 정의된 조건, 이벤트 또는 상태 변경이 발생할 때마다 공급자는 대리자를 호출하여 모든 관찰자에게 자동으로 알깁니다. 이 메서드 호출에서 공급자는 관찰자에게 현재 상태 정보를 제공할 수도 있습니다. .NET에서 관찰자 디자인 패턴은 제네릭 System.IObservable<T> 및 System.IObserver<T> 인터페이스를 구현하여 적용됩니다. 제네릭 형식 매개 변수는 알림 정보를 제공하는 형식을 나타냅니다.
패턴을 적용하는 경우
관찰자 디자인 패턴은 데이터 원본(비즈니스 논리) 계층과 사용자 인터페이스(표시) 계층과 같은 서로 다른 두 구성 요소 또는 애플리케이션 계층 간의 깨끗한 분리를 지원하기 때문에 분산 푸시 기반 알림에 적합합니다. 공급자가 콜백을 사용하여 클라이언트에 현재 정보를 제공할 때마다 패턴을 구현할 수 있습니다.
패턴을 구현하려면 다음 세부 정보를 제공해야 합니다.
관찰자에게 알림을 보내는 개체인 공급자 또는 주체입니다. 공급자는 인터페이스를 구현하는 클래스 또는 구조체입니다 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 알림이 완료되면 공급자가 관찰자를 제거할 수 있도록 하는 구현입니다. 관찰자는 IDisposable 메서드를 통해 Subscribe 구현에 대한 참조를 받으므로, 공급자가 알림을 보내는 것을 완료하기 전에 IDisposable.Dispose 메서드를 호출하여 구독을 취소할 수도 있습니다.
공급자가 관찰자에게 보내는 데이터를 포함하는 개체입니다. 이 개체의 형식은 IObservable<T> 및 IObserver<T> 인터페이스의 제네릭 형식 매개 변수에 해당합니다. 이 개체는 IObservable<T> 구현과 같을 수도 있지만, 일반적으로는 별도의 타입입니다.
비고
관찰자 디자인 패턴을 구현하는 것 외에도 IObservable<T> 및 IObserver<T> 인터페이스를 사용하여 구축된 라이브러리를 살펴보는 데 관심이 있을 수 있습니다. 예를 들어 Rx(.NET용 Reactive Extensions) 는 비동기 프로그래밍을 지원하는 일련의 확장 메서드와 LINQ 표준 시퀀스 연산자로 구성됩니다.
대안을 고려해야 하는 경우
IObservable<T>
/
IObserver<T> 인터페이스는 푸시 기반 알림 시나리오에 적합하지만 .NET 더 적합할 수 있는 다른 패턴을 제공합니다.
- 표준 .NET 이벤트 — 단일 애플리케이션 내의 간단한 알림 시나리오의 경우 events는 보다 idiomatic이고 구현하기 쉽습니다.
-
IAsyncEnumerable<T>- 소비자가 속도를 제어하는 비동기 풀 기반 시퀀스의 경우 비동기 스트림을 사용합니다. -
System.Threading.Channels— 백프레셔 및 비동기 지원이 필요한 생산자-소비자 패턴에는 System.Threading.Channels를 사용하세요. -
Reactive Extensions(Rx.NET) — 복잡한 이벤트 조합, 필터링 및 변환에는
IObservable<T>를 직접 구현하는 대신System.Reactive패키지를 사용합니다.
.NET에서 IObservable<T>의 가장 두드러진 용도는 DiagnosticListener이며, 이를 통해 프레임워크 및 라이브러리 작성자는 소비자가 구독하는 구조화된 진단 이벤트를 내보낼 수 있습니다.
패턴 구현
다음 예제에서는 관찰자 디자인 패턴을 사용하여 공항 수하물 찾는 정보 시스템을 구현합니다. 클래스는 BaggageInfo 도착 항공편 및 각각의 항공편에서 수하물을 픽업할 수 있는 수하물 회전대에 대한 정보를 제공합니다. 다음 예제에 나와 있습니다.
namespace Observables.Example;
public readonly record struct BaggageInfo(
int FlightNumber,
string From,
int Carousel);
Namespace Example
Public Structure BaggageInfo
Implements IEquatable(Of BaggageInfo)
Public ReadOnly Property FlightNumber As Integer
Public ReadOnly Property From As String
Public ReadOnly Property Carousel As Integer
Public Sub New(flightNumber As Integer, from As String, carousel As Integer)
Me.FlightNumber = flightNumber
Me.From = from
Me.Carousel = carousel
End Sub
Public Overloads Function Equals(other As BaggageInfo) As Boolean Implements IEquatable(Of BaggageInfo).Equals
Return FlightNumber = other.FlightNumber AndAlso
From = other.From AndAlso
Carousel = other.Carousel
End Function
Public Overrides Function Equals(obj As Object) As Boolean
If TypeOf obj Is BaggageInfo Then
Return Equals(DirectCast(obj, BaggageInfo))
End If
Return False
End Function
Public Overrides Function GetHashCode() As Integer
Return HashCode.Combine(FlightNumber, From, Carousel)
End Function
Public Shared Operator =(left As BaggageInfo, right As BaggageInfo) As Boolean
Return left.Equals(right)
End Operator
Public Shared Operator <>(left As BaggageInfo, right As BaggageInfo) As Boolean
Return Not left.Equals(right)
End Operator
End Structure
End Namespace
BaggageHandler 클래스는 도착 항공편 및 수하물 회수대에 대한 정보를 수신할 책임이 있습니다. 내부적으로 다음 두 컬렉션을 유지 관리합니다.
-
_observers: 업데이트된 정보를 관찰하는 클라이언트의 컬렉션입니다. -
_flights: 항공편과 할당된 수하물 컨베이어 벨트의 모음입니다.
클래스의 BaggageHandler 소스 코드는 다음 예제에 나와 있습니다.
namespace Observables.Example;
public sealed class BaggageHandler : IObservable<BaggageInfo>
{
private readonly Lock _lock = new();
private readonly HashSet<IObserver<BaggageInfo>> _observers = [];
private readonly HashSet<BaggageInfo> _flights = [];
public IDisposable Subscribe(IObserver<BaggageInfo> observer)
{
BaggageInfo[] snapshot;
lock (_lock)
{
// Check whether observer is already registered. If not, add it.
if (!_observers.Add(observer))
{
return new Unsubscriber<BaggageInfo>(_lock, _observers, observer);
}
// Snapshot existing data while holding the lock.
snapshot = [.. _flights];
}
// Provide observer with existing data outside the lock.
foreach (BaggageInfo item in snapshot)
{
observer.OnNext(item);
}
return new Unsubscriber<BaggageInfo>(_lock, _observers, observer);
}
// Called to indicate all baggage is now unloaded.
public void BaggageStatus(int flightNumber) =>
BaggageStatus(flightNumber, string.Empty, 0);
public void BaggageStatus(int flightNumber, string from, int carousel)
{
var info = new BaggageInfo(flightNumber, from, carousel);
IObserver<BaggageInfo>[] snapshot;
// Carousel is assigned, so add new info object to list.
if (carousel > 0)
{
lock (_lock)
{
if (!_flights.Add(info))
{
return;
}
snapshot = [.. _observers];
}
foreach (IObserver<BaggageInfo> observer in snapshot)
{
observer.OnNext(info);
}
}
else if (carousel is 0)
{
// Baggage claim for flight is done.
lock (_lock)
{
if (_flights.RemoveWhere(
flight => flight.FlightNumber == info.FlightNumber) == 0)
{
return;
}
snapshot = [.. _observers];
}
foreach (IObserver<BaggageInfo> observer in snapshot)
{
observer.OnNext(info);
}
}
}
public void LastBaggageClaimed()
{
IObserver<BaggageInfo>[] snapshot;
lock (_lock)
{
snapshot = [.. _observers];
_observers.Clear();
}
foreach (IObserver<BaggageInfo> observer in snapshot)
{
observer.OnCompleted();
}
}
}
Namespace Example
Public NotInheritable Class BaggageHandler
Implements IObservable(Of BaggageInfo)
Private ReadOnly _lock As New Object()
Private ReadOnly _observers As New HashSet(Of IObserver(Of BaggageInfo))()
Private ReadOnly _flights As New HashSet(Of BaggageInfo)()
Public Function Subscribe(observer As IObserver(Of BaggageInfo)) As IDisposable Implements IObservable(Of BaggageInfo).Subscribe
Dim snapshot As BaggageInfo()
SyncLock _lock
' Check whether observer is already registered. If not, add it.
If Not _observers.Add(observer) Then
Return New Unsubscriber(Of BaggageInfo)(_lock, _observers, observer)
End If
' Snapshot existing data while holding the lock.
snapshot = _flights.ToArray()
End SyncLock
' Provide observer with existing data outside the lock.
For Each item As BaggageInfo In snapshot
observer.OnNext(item)
Next
Return New Unsubscriber(Of BaggageInfo)(_lock, _observers, observer)
End Function
' Called to indicate all baggage is now unloaded.
Public Sub BaggageStatus(flightNumber As Integer)
BaggageStatus(flightNumber, String.Empty, 0)
End Sub
Public Sub BaggageStatus(flightNumber As Integer, from As String, carousel As Integer)
Dim info As New BaggageInfo(flightNumber, from, carousel)
Dim snapshot As IObserver(Of BaggageInfo)()
' Carousel is assigned, so add new info object to list.
If carousel > 0 Then
SyncLock _lock
If Not _flights.Add(info) Then
Return
End If
snapshot = _observers.ToArray()
End SyncLock
For Each observer As IObserver(Of BaggageInfo) In snapshot
observer.OnNext(info)
Next
ElseIf carousel = 0 Then
' Baggage claim for flight is done.
SyncLock _lock
If _flights.RemoveWhere(
Function(flight) flight.FlightNumber = info.FlightNumber) = 0 Then
Return
End If
snapshot = _observers.ToArray()
End SyncLock
For Each observer As IObserver(Of BaggageInfo) In snapshot
observer.OnNext(info)
Next
End If
End Sub
Public Sub LastBaggageClaimed()
Dim snapshot As IObserver(Of BaggageInfo)()
SyncLock _lock
snapshot = _observers.ToArray()
_observers.Clear()
End SyncLock
For Each observer As IObserver(Of BaggageInfo) In snapshot
observer.OnCompleted()
Next
End Sub
End Class
End Namespace
업데이트된 정보를 수신하려는 클라이언트는 메서드를 호출합니다 BaggageHandler.Subscribe . 클라이언트가 이전에 알림을 구독하지 않은 경우 클라이언트의 IObserver<T> 구현에 대한 참조가 컬렉션에 _observers 추가됩니다.
오버로드된 메서드를 BaggageHandler.BaggageStatus 호출하여 항공편의 수하물이 언로드 중이거나 더 이상 언로드되지 않음을 나타낼 수 있습니다. 첫 번째 경우, 메서드는 항공편 번호와 항공편이 출발한 공항, 수하물이 내려지는 회전목마를 전달합니다. 두 번째 경우 메서드는 플라이트 번호만 전달됩니다. 언로드되는 수하물을 위해, 메서드는 메서드에 전달된 BaggageInfo 정보가 _flights 컬렉션에 존재하는지 확인합니다. 그렇지 않은 경우 메서드는 정보를 추가하고 각 관찰자의 OnNext 메서드를 호출합니다. 수하물이 더 이상 언로드되지 않는 항공편의 경우, 이 메서드는 해당 항공편의 정보가 컬렉션에 저장 _flights 되어 있는지 여부를 확인합니다. 이 경우 메서드는 각 관찰자의 OnNext 메서드를 호출한 후 BaggageInfo 객체를 _flights 컬렉션에서 제거합니다.
하루의 마지막 비행이 착륙하고 수하물이 처리되면 메서드가 BaggageHandler.LastBaggageClaimed 호출됩니다. 이 메서드는 각 관찰자의 OnCompleted 메서드를 호출하여 모든 알림이 완료되었음을 나타내고 컬렉션을 지웁니다 _observers .
공급자의 Subscribe 메서드는 관찰자가 IDisposable 메서드가 호출되기 전에 알림 수신을 중지할 수 있도록 하는 OnCompleted 구현을 반환합니다. 이 Unsubscriber 클래스의 소스 코드는 다음 예제에 나와 있습니다.
BaggageHandler.Subscribe 메서드에서 클래스가 인스턴스화되면 _lock 개체에 대한 참조, _observers 컬렉션, 그리고 해당 컬렉션에 추가되는 관찰자에 대한 참조가 전달됩니다. 이러한 참조는 지역 변수에 할당됩니다. 개체의 Dispose 메서드가 호출되면 잠금 상태에서 _observers 컬렉션에서 관찰자를 제거합니다.
namespace Observables.Example;
internal sealed class Unsubscriber<T> : IDisposable
{
private readonly Lock _lock;
private readonly ISet<IObserver<T>> _observers;
private readonly IObserver<T> _observer;
internal Unsubscriber(
Lock @lock,
ISet<IObserver<T>> observers,
IObserver<T> observer) => (_lock, _observers, _observer) = (@lock, observers, observer);
public void Dispose()
{
lock (_lock)
{
_observers.Remove(_observer);
}
}
}
Namespace Example
Friend NotInheritable Class Unsubscriber(Of T)
Implements IDisposable
Private ReadOnly _lock As Object
Private ReadOnly _observers As ISet(Of IObserver(Of T))
Private ReadOnly _observer As IObserver(Of T)
Friend Sub New(lock As Object, observers As ISet(Of IObserver(Of T)), observer As IObserver(Of T))
_lock = lock
_observers = observers
_observer = observer
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
SyncLock _lock
_observers.Remove(_observer)
End SyncLock
End Sub
End Class
End Namespace
다음 예제에서는 수하물 클레임 정보를 표시하는 기본 클래스인 IObserver<T> 구현인 ArrivalsMonitor을 제공합니다. 정보는 원래 도시의 이름으로 사전순으로 표시됩니다. 메서드 ArrivalsMonitor는 Visual Basic에서는 overridable으로, C#에서는 virtual로 표시되므로 파생 클래스에서 재정의할 수 있습니다.
namespace Observables.Example;
public class ArrivalsMonitor : IObserver<BaggageInfo>
{
private readonly string _name;
private readonly Lock _lock = new();
private readonly List<string> _flights = [];
private readonly string _format = "{0,-20} {1,5} {2, 3}";
private IDisposable? _cancellation;
public ArrivalsMonitor(string name)
{
ArgumentException.ThrowIfNullOrEmpty(name);
_name = name;
}
public virtual void Subscribe(BaggageHandler provider) =>
_cancellation = provider.Subscribe(this);
public virtual void Unsubscribe()
{
Interlocked.Exchange(ref _cancellation, null)?.Dispose();
lock (_lock)
{
_flights.Clear();
}
}
public virtual void OnCompleted()
{
lock (_lock)
{
_flights.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;
lock (_lock)
{
// Flight has unloaded its baggage; remove from the monitor.
if (info.Carousel is 0)
{
string flightNumber = $"{info.FlightNumber,5}";
for (int index = _flights.Count - 1; index >= 0; index--)
{
string flightInfo = _flights[index];
if (flightInfo.Substring(21, 5).Equals(flightNumber))
{
updated = true;
_flights.RemoveAt(index);
}
}
}
else
{
// Add flight if it doesn't exist in the collection.
string flightInfo = string.Format(_format, info.From, info.FlightNumber, info.Carousel);
if (_flights.Contains(flightInfo) is false)
{
_flights.Add(flightInfo);
updated = true;
}
}
if (updated)
{
_flights.Sort();
Console.WriteLine($"Arrivals information from {_name}");
foreach (string flightInfo in _flights)
{
Console.WriteLine(flightInfo);
}
Console.WriteLine();
}
}
}
}
Imports System.Threading
Namespace Example
Public Class ArrivalsMonitor
Implements IObserver(Of BaggageInfo)
Private ReadOnly _name As String
Private ReadOnly _lock As New Object()
Private ReadOnly _flights As New List(Of String)()
Private ReadOnly _format As String = "{0,-20} {1,5} {2, 3}"
Private _cancellation As IDisposable
Public Sub New(name As String)
If String.IsNullOrEmpty(name) Then
Throw New ArgumentException("Value cannot be null or empty.", NameOf(name))
End If
_name = name
End Sub
Public Overridable Sub Subscribe(provider As BaggageHandler)
_cancellation = provider.Subscribe(Me)
End Sub
Public Overridable Sub Unsubscribe()
Dim previous = Interlocked.Exchange(_cancellation, Nothing)
previous?.Dispose()
SyncLock _lock
_flights.Clear()
End SyncLock
End Sub
Public Overridable Sub OnCompleted() Implements IObserver(Of BaggageInfo).OnCompleted
SyncLock _lock
_flights.Clear()
End SyncLock
End Sub
' No implementation needed: Method is not called by the BaggageHandler class.
Public Overridable Sub OnError([error] As Exception) Implements IObserver(Of BaggageInfo).OnError
' No implementation.
End Sub
' Update information.
Public Overridable Sub OnNext(info As BaggageInfo) Implements IObserver(Of BaggageInfo).OnNext
Dim updated As Boolean = False
SyncLock _lock
' Flight has unloaded its baggage; remove from the monitor.
If info.Carousel = 0 Then
Dim flightNumber As String = String.Format("{0,5}", info.FlightNumber)
For index As Integer = _flights.Count - 1 To 0 Step -1
Dim flightInfo As String = _flights(index)
If flightInfo.Substring(21, 5).Equals(flightNumber) Then
updated = True
_flights.RemoveAt(index)
End If
Next
Else
' Add flight if it doesn't exist in the collection.
Dim flightInfo As String = String.Format(_format, info.From, info.FlightNumber, info.Carousel)
If Not _flights.Contains(flightInfo) Then
_flights.Add(flightInfo)
updated = True
End If
End If
If updated Then
_flights.Sort()
Console.WriteLine($"Arrivals information from {_name}")
For Each flightInfo As String In _flights
Console.WriteLine(flightInfo)
Next
Console.WriteLine()
End If
End SyncLock
End Sub
End Class
End Namespace
클래스에는 ArrivalsMonitor 및 Subscribe 메서드가 포함됩니다.
Subscribe 메서드를 사용하면, 클래스는 IDisposable 호출에서 반환된 구현을 프라이빗 변수에 저장할 Subscribe 수 있습니다. 이 Unsubscribe 메서드를 사용하면 공급자의 Dispose 구현을 호출하여 클래스가 알림에서 구독을 취소할 수 있습니다.
ArrivalsMonitor는 OnNext, OnError, 및 OnCompleted 메서드의 구현도 제공합니다. 구현에 OnNext 만 상당한 양의 코드가 포함됩니다. 이 방법은 도착 항공편의 출발지 공항과 수하물을 받을 수 있는 회전식 컨베이어에 대한 정보를 유지하는 개인 정렬된 일반 List<T> 객체와 함께 작동합니다. 클래스가 BaggageHandler 새 플라이트 도착을 보고하는 경우 메서드 구현은 OnNext 해당 플라이트 정보를 목록에 추가합니다. 클래스가 BaggageHandler 항공편의 수하물이 언로드되었다고 OnNext 보고하는 경우, 이 메서드는 해당 항공편을 목록에서 제거합니다. 변경될 때마다 목록이 정렬되어 콘솔에 표시됩니다.
다음 예제에서는 BaggageHandler 클래스와 ArrivalsMonitor 클래스의 두 인스턴스를 인스턴스화하는 애플리케이션 진입점을 포함하고, BaggageHandler.BaggageStatus 메서드를 사용하여 도착 항공편에 대한 정보를 추가하고 제거합니다. 각 경우에 관찰자는 업데이트를 받고 수하물 클레임 정보를 올바르게 표시합니다.
using Observables.Example;
BaggageHandler provider = new();
ArrivalsMonitor observer1 = new("BaggageClaimMonitor1");
ArrivalsMonitor observer2 = new("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();
Imports Observables.Example
Imports System.Threading
Module Program
Sub Main(args As String())
Dim provider As New BaggageHandler()
Dim observer1 As New ArrivalsMonitor("BaggageClaimMonitor1")
Dim observer2 As 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()
End Sub
End Module
관련 콘텐츠
.NET