Share via


관찰자 디자인 패턴

관찰자 디자인 패턴은 구독자가 공급자에 등록하고 공급자로부터 알림을 받을 수 있게 합니다. 이는 푸시 기반 알림이 필요한 시나리오에 적합합니다. 이 패턴은 ‘공급자’(‘주체’ 또는 ‘관찰 가능 대상’이라고도 함) 및 0개 이상의 ‘관찰자’를 정의합니다. 관찰자는 공급자에 등록하고 미리 정의된 조건, 이벤트 또는 상태 변경이 발생할 때마다 공급자는 대리자를 호출하여 모든 관찰자에게 자동으로 알립니다. 이 메서드 호출에서 공급자는 관찰자에게 현재 상태 정보를 제공할 수도 있습니다. .NET에서는 제네릭 System.IObservable<T>System.IObserver<T> 인터페이스를 구현하여 관찰자 디자인 패턴을 적용합니다. 제네릭 형식 매개 변수는 알림 정보를 제공하는 형식을 나타냅니다.

패턴을 적용하는 경우

관찰자 디자인 패턴은 데이터 소스(비즈니스 논리) 계층 및 사용자 인터페이스(표시) 계층과 같은 두 가지 구성 요소 또는 애플리케이션 계층의 명확한 구분을 지원하기 때문에 분산된 푸시 기반 알림에 적합합니다. 공급자가 콜백을 사용하여 해당 클라이언트에 현재 정보를 제공할 때마다 패턴을 구현할 수 있습니다.

패턴을 구현하려면 다음 세부 정보를 제공해야 합니다.

  • 관찰자에게 알림을 전송하는 개체인 공급자 또는 주체. 공급자는 IObservable<T> 인터페이스를 구현하는 클래스 또는 구조체입니다. 공급자는 공급자로부터 알림을 수신하려는 관찰자가 호출하는 단일 메서드 IObservable<T>.Subscribe를 구현해야 합니다.

  • 공급자로부터 알림을 수신하는 개체인 관찰자. 관찰자는 IObserver<T> 인터페이스를 구현하는 클래스 또는 구조체입니다. 관찰자는 모두 공급자에 의해 호출되는 다음 세 개의 메서드를 구현해야 합니다.

  • 공급자가 관찰자를 추적할 수 있게 해주는 메커니즘. 일반적으로 공급자는 System.Collections.Generic.List<T> 개체와 같은 컨테이너 개체를 사용하여 알림을 구독한 IObserver<T> 구현에 대한 참조를 보유합니다. 이 목적으로 스토리지 컨테이너를 사용하면 공급자가 0개에서 무한대 개수까지 관찰자를 처리할 수 있습니다. 관찰자가 알림을 수신하는 순서는 정의되지 않습니다. 공급자가 임의 메서드를 사용하여 순서를 결정할 수 있습니다.

  • 알림이 완료될 때 공급자가 관찰자를 제거할 수 있도록 하는 IDisposable 구현. 관찰자는 Subscribe 메서드로부터 IDisposable 구현에 대한 참조를 수신하므로 공급자가 알림 전송을 완료하기 전에 IDisposable.Dispose 메서드를 호출하여 구독을 취소할 수도 있습니다.

  • 공급자가 해당 관찰자에게 전송하는 데이터를 포함하는 개체. 이 개체의 형식은 IObservable<T>IObserver<T> 인터페이스의 제네릭 형식 매개 변수에 해당합니다. 이 개체는 IObservable<T> 구현과 동일할 수도 있지만 일반적으로 별도 형식입니다.

참고 항목

관찰자 디자인 패턴 구현 외에도 IObservable<T>IObserver<T> 인터페이스를 사용하여 빌드된 라이브러리 탐색에 관심이 있을 수 있습니다. 예를 들어 .NET용 사후 확장(Rx)은 비동기 프로그래밍을 지원하기 위해 일련의 확장 메서드와 LINQ 표준 시퀀스 연산자로 구성됩니다.

패턴 구현

다음 예제에서는 관찰자 디자인 패턴을 사용하여 공항의 수하물 찾는 곳 정보 시스템을 구현합니다. BaggageInfo 클래스는 도착 항공편과 각 항공편의 수하물을 찾을 수 있는 컨베이어 벨트에 대한 정보를 제공합니다. 다음 예에 나와 있습니다.

namespace Observables.Example;

public readonly record struct BaggageInfo(
    int FlightNumber,
    string From,
    int Carousel);
Public Class BaggageInfo
    Private flightNo As Integer
    Private origin As String
    Private location As Integer

    Friend Sub New(ByVal flight As Integer, ByVal from As String, ByVal carousel As Integer)
        Me.flightNo = flight
        Me.origin = from
        Me.location = carousel
    End Sub

    Public ReadOnly Property FlightNumber As Integer
        Get
            Return Me.flightNo
        End Get
    End Property

    Public ReadOnly Property From As String
        Get
            Return Me.origin
        End Get
    End Property

    Public ReadOnly Property Carousel As Integer
        Get
            Return Me.location
        End Get
    End Property
End Class

BaggageHandler 클래스는 도착 항공편 및 수하물을 찾을 수 있는 컨베이어 벨트에 대한 정보를 받아야 합니다. 내부적으로 다음 두 개의 컬렉션을 유지 관리합니다.

  • _observers: 업데이트된 정보를 관찰하는 클라이언트 컬렉션입니다.
  • _flights: 항공편 및 할당된 컨베이어 벨트 컬렉션입니다.

BaggageHandler 클래스의 소스 코드는 다음 예제에 나와 있습니다.

namespace Observables.Example;

public sealed class BaggageHandler : IObservable<BaggageInfo>
{
    private readonly HashSet<IObserver<BaggageInfo>> _observers = new();
    private readonly HashSet<BaggageInfo> _flights = new();

    public IDisposable Subscribe(IObserver<BaggageInfo> observer)
    {
        // Check whether observer is already registered. If not, add it.
        if (_observers.Add(observer))
        {
            // Provide observer with existing data.
            foreach (BaggageInfo item in _flights)
            {
                observer.OnNext(item);
            }
        }

        return new Unsubscriber<BaggageInfo>(_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);

        // Carousel is assigned, so add new info object to list.
        if (carousel > 0 && _flights.Add(info))
        {
            foreach (IObserver<BaggageInfo> observer in _observers)
            {
                observer.OnNext(info);
            }
        }
        else if (carousel is 0)
        {
            // Baggage claim for flight is done.
            if (_flights.RemoveWhere(
                flight => flight.FlightNumber == info.FlightNumber) > 0)
            {
                foreach (IObserver<BaggageInfo> observer in _observers)
                {
                    observer.OnNext(info);
                }
            }
        }
    }

    public void LastBaggageClaimed()
    {
        foreach (IObserver<BaggageInfo> observer in _observers)
        {
            observer.OnCompleted();
        }

        _observers.Clear();
    }
}
Public Class BaggageHandler : Implements IObservable(Of BaggageInfo)

    Private observers As List(Of IObserver(Of BaggageInfo))
    Private flights As List(Of BaggageInfo)

    Public Sub New()
        observers = New List(Of IObserver(Of BaggageInfo))
        flights = New List(Of BaggageInfo)
    End Sub

    Public Function Subscribe(ByVal observer As IObserver(Of BaggageInfo)) As IDisposable _
                    Implements IObservable(Of BaggageInfo).Subscribe
        ' Check whether observer is already registered. If not, add it
        If Not observers.Contains(observer) Then
            observers.Add(observer)
            ' Provide observer with existing data.
            For Each item In flights
                observer.OnNext(item)
            Next
        End If
        Return New Unsubscriber(Of BaggageInfo)(observers, observer)
    End Function

    ' Called to indicate all baggage is now unloaded.
    Public Sub BaggageStatus(ByVal flightNo As Integer)
        BaggageStatus(flightNo, String.Empty, 0)
    End Sub

    Public Sub BaggageStatus(ByVal flightNo As Integer, ByVal from As String, ByVal carousel As Integer)
        Dim info As New BaggageInfo(flightNo, from, carousel)

        ' Carousel is assigned, so add new info object to list.
        If carousel > 0 And Not flights.Contains(info) Then
            flights.Add(info)
            For Each observer In observers
                observer.OnNext(info)
            Next
        ElseIf carousel = 0 Then
            ' Baggage claim for flight is done
            Dim flightsToRemove As New List(Of BaggageInfo)
            For Each flight In flights
                If info.FlightNumber = flight.FlightNumber Then
                    flightsToRemove.Add(flight)
                    For Each observer In observers
                        observer.OnNext(info)
                    Next
                End If
            Next
            For Each flightToRemove In flightsToRemove
                flights.Remove(flightToRemove)
            Next
            flightsToRemove.Clear()
        End If
    End Sub

    Public Sub LastBaggageClaimed()
        For Each observer In observers
            observer.OnCompleted()
        Next
        observers.Clear()
    End Sub
End Class

업데이트된 정보를 수신하려는 클라이언트는 BaggageHandler.Subscribe 메서드를 호출합니다. 클라이언트가 이전에 알림을 구독하지 않은 경우 클라이언트의 IObserver<T> 구현에 대한 참조가 _observers 컬렉션에 추가됩니다.

오버로드된 BaggageHandler.BaggageStatus 메서드를 호출하여 항공편의 수하물을 내리는 중인지 여부를 나타낼 수 있습니다. 내리는 중이면 메서드에 항공편 번호, 출발 공항 및 수하물을 내리는 중인 컨베이어 벨트가 전달됩니다. 더 이상 내리지 않는 경우 메서드에 항공편 번호만 전달됩니다. 수하물을 내리는 경우 메서드는 메서드에 전달된 BaggageInfo 정보가 _flights 컬렉션에 있는지 여부를 확인합니다. 그렇지 않은 경우 메서드는 정보를 추가하고 각 관찰자의 OnNext 메서드를 호출합니다. 더 이상 수하물을 내리지 않는 항공편의 경우 메서드는 항공편에 대한 정보가 _flights 컬렉션에 저장되었는지 여부를 확인합니다. 저장된 경우 메서드는 각 관찰자의 OnNext 메서드를 호출하고 _flights 컬렉션에서 BaggageInfo 개체를 제거합니다.

그날의 마지막 항공편이 착륙하고 해당 수하물이 처리되면 BaggageHandler.LastBaggageClaimed 메서드가 호출됩니다. 이 메서드는 각 관찰자의 OnCompleted 메서드를 호출하여 모든 알림이 완료되었음을 나타내고 _observers 컬렉션을 지웁니다.

공급자의 Subscribe 메서드는 OnCompleted 메서드가 호출되기 전에 관찰자가 알림 수신을 중지할 수 있도록 하는 IDisposable 구현을 반환합니다. Unsubscriber(Of BaggageInfo) 클래스의 소스 코드는 다음 예제에 나와 있습니다. BaggageHandler.Subscribe 메서드에서 클래스가 인스턴스화되면 _observers 컬렉션에 대한 참조 및 컬렉션에 추가된 관찰자에 대한 참조가 전달됩니다. 이러한 참조는 지역 변수에 할당됩니다. 개체의 Dispose 메서드가 호출되면 관찰자가 _observers 컬렉션에 여전히 있는지 여부를 확인하고, 있을 경우 관찰자를 제거합니다.

namespace Observables.Example;

internal sealed class Unsubscriber<BaggageInfo> : IDisposable
{
    private readonly ISet<IObserver<BaggageInfo>> _observers;
    private readonly IObserver<BaggageInfo> _observer;

    internal Unsubscriber(
        ISet<IObserver<BaggageInfo>> observers,
        IObserver<BaggageInfo> observer) => (_observers, _observer) = (observers, observer);

    public void Dispose() => _observers.Remove(_observer);
}
Friend Class Unsubscriber(Of BaggageInfo) : Implements IDisposable
    Private _observers As List(Of IObserver(Of BaggageInfo))
    Private _observer As IObserver(Of BaggageInfo)

    Friend Sub New(ByVal observers As List(Of IObserver(Of BaggageInfo)), ByVal observer As IObserver(Of BaggageInfo))
        Me._observers = observers
        Me._observer = observer
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        If _observers.Contains(_observer) Then
            _observers.Remove(_observer)
        End If
    End Sub
End Class

다음 예제에서는 수하물 찾는 곳 정보를 표시하는 기본 클래스인 ArrivalsMonitor라는 IObserver<T> 구현을 제공합니다. 정보는 출발 도시 이름별 사전순으로 표시됩니다. ArrivalsMonitor의 메서드는 overridable(Visual Basic의 경우) 또는 virtual(C#의 경우)로 표시되므로 파생 클래스에서 재정의될 수 있습니다.

namespace Observables.Example;

public class ArrivalsMonitor : IObserver<BaggageInfo>
{
    private readonly string _name;
    private readonly List<string> _flights = new();
    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()
    {
        _cancellation?.Dispose();
        _flights.Clear();
    }

    public virtual void OnCompleted() => _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;

        // Flight has unloaded its baggage; remove from the monitor.
        if (info.Carousel is 0)
        {
            string flightNumber = string.Format("{0,5}", info.FlightNumber);
            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();
        }
    }
}
Public Class ArrivalsMonitor : Implements IObserver(Of BaggageInfo)
    Private name As String
    Private flightInfos As New List(Of String)
    Private cancellation As IDisposable
    Private fmt As String = "{0,-20} {1,5}  {2, 3}"

    Public Sub New(ByVal name As String)
        If String.IsNullOrEmpty(name) Then Throw New ArgumentNullException("The observer must be assigned a name.")

        Me.name = name
    End Sub

    Public Overridable Sub Subscribe(ByVal provider As BaggageHandler)
        cancellation = provider.Subscribe(Me)
    End Sub

    Public Overridable Sub Unsubscribe()
        cancellation.Dispose()
        flightInfos.Clear()
    End Sub

    Public Overridable Sub OnCompleted() Implements System.IObserver(Of BaggageInfo).OnCompleted
        flightInfos.Clear()
    End Sub

    ' No implementation needed: Method is not called by the BaggageHandler class.
    Public Overridable Sub OnError(ByVal e As System.Exception) Implements System.IObserver(Of BaggageInfo).OnError
        ' No implementation.
    End Sub

    ' Update information.
    Public Overridable Sub OnNext(ByVal info As BaggageInfo) Implements System.IObserver(Of BaggageInfo).OnNext
        Dim updated As Boolean = False

        ' Flight has unloaded its baggage; remove from the monitor.
        If info.Carousel = 0 Then
            Dim flightsToRemove As New List(Of String)
            Dim flightNo As String = String.Format("{0,5}", info.FlightNumber)
            For Each flightInfo In flightInfos
                If flightInfo.Substring(21, 5).Equals(flightNo) Then
                    flightsToRemove.Add(flightInfo)
                    updated = True
                End If
            Next
            For Each flightToRemove In flightsToRemove
                flightInfos.Remove(flightToRemove)
            Next
            flightsToRemove.Clear()
        Else
            ' Add flight if it does not exist in the collection.
            Dim flightInfo As String = String.Format(fmt, info.From, info.FlightNumber, info.Carousel)
            If Not flightInfos.Contains(flightInfo) Then
                flightInfos.Add(flightInfo)
                updated = True
            End If
        End If
        If updated Then
            flightInfos.Sort()
            Console.WriteLine("Arrivals information from {0}", Me.name)
            For Each flightInfo In flightInfos
                Console.WriteLine(flightInfo)
            Next
            Console.WriteLine()
        End If
    End Sub
End Class

ArrivalsMonitor 클래스에는 SubscribeUnsubscribe 메서드가 포함됩니다. Subscribe 메서드를 통해 클래스는 Subscribe 호출에서 반환된 IDisposable 구현을 전용 변수에 저장할 수 있습니다. Unsubscribe 메서드를 통해 클래스는 공급자의 Dispose 구현을 호출하여 알림 구독을 취소할 수 있습니다. ArrivalsMonitor에서는 OnNext, OnErrorOnCompleted 메서드의 구현도 제공합니다. OnNext 구현에만 상당한 양의 코드가 포함됩니다. 메서드는 도착 항공편의 출발 공항 및 수하물을 찾을 수 있는 컨베이어 벨트에 대한 정보를 유지 관리하는 private, sorted, generic 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();

// Sample 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
Module Example
    Public Sub Main()
        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
' 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
제목 설명
관찰자 디자인 패턴 유용한 정보 관찰자 디자인 패턴을 구현하는 애플리케이션을 개발할 때 채택할 모범 사례를 설명합니다.
방법: 공급자 구현 온도 모니터링 애플리케이션에 대한 공급자의 단계별 구현을 제공합니다.
방법: 관찰자 구현 온도 모니터링 애플리케이션에 대한 관찰자의 단계별 구현을 제공합니다.