옵서버 패턴
온도 센서가 화면 하나에 측정값을 보여 준다고 하자. 나중에 경보도 같은 값을 받아야 한다. 센서가 Display.Show()와 HeatAlarm.Check()를 직접 호출하면, 수신자가 늘어날 때마다 센서 코드까지 고쳐야 한다.
대신 수신자가 알림을 받을 메서드를 등록하게 한다. 센서는 등록된 메서드를 보관했다가 새 측정값을 알린다. 센서가 알아야 할 것은 등록된 메서드를 호출하는 방법이며, 그것이 화면인지 경보인지 나중에 추가된 기능인지는 몰라도 된다.
변화를 알리는 객체와 그 변화를 구독하는 객체를 나누는 구조가 옵서버(Observer) 패턴이다. Design Patterns의 네 저자를 가리키는 GoF의 분류에서는 행위 패턴에 속한다. 알리는 쪽을 주제(Subject), 받는 쪽을 옵서버라고 부르기도 한다.1
C#에서는 이벤트로 표현하기
같은 프로세스 안에서는 C# 이벤트로 이 관계를 짧게 표현할 수 있다. EventHandler<TemperatureChanged>는 보낸 객체와 TemperatureChanged 값을 받는 메서드의 형식이다. Changed는 그 메서드들이 구독할 이벤트다.2
측정값은 별도 객체로 전달한다. 수신자가 센서의 바뀔 수 있는 상태를 다시 조회하지 않아도 같은 섭씨 값을 받게 하려는 것이다. Display는 이 값을 표시하고, HeatAlarm은 경보 기준을 넘었는지 판단한다.
작은 예제부터
using System;
using System.Globalization;
var sensor = new TemperatureSensor();
var display = new Display();
var alarm = new HeatAlarm();
sensor.Changed += display.Show;
sensor.Changed += alarm.Check;
sensor.Measure(28m);
sensor.Measure(31m);
sensor.Changed -= alarm.Check;
sensor.Measure(32m);
public sealed class TemperatureChanged : EventArgs
{
public decimal Celsius { get; }
public TemperatureChanged(decimal celsius) => Celsius = celsius;
}
public sealed class TemperatureSensor
{
public event EventHandler<TemperatureChanged>? Changed;
public void Measure(decimal celsius) => Changed?.Invoke(this, new TemperatureChanged(celsius));
}
public sealed class Display
{
public void Show(object? sender, TemperatureChanged change) =>
Console.WriteLine("Display: " + change.Celsius.ToString(CultureInfo.InvariantCulture));
}
public sealed class HeatAlarm
{
public void Check(object? sender, TemperatureChanged change)
{
if (change.Celsius >= 30m)
Console.WriteLine("Alarm: " + change.Celsius.ToString(CultureInfo.InvariantCulture));
}
}Display: 28
Display: 31
Alarm: 31
Display: 32+=가 화면과 경보의 메서드를 구독자로 등록한다. 28도에서도 두 메서드는 모두 호출되지만, 경보 기준인 30도보다 낮으므로 경보는 출력하지 않는다. 31도에서는 둘 다 출력한다. -=로 경보의 구독을 해제한 뒤에는 32도를 측정해도 화면만 받는다.
Changed?.Invoke(...)는 구독자가 있을 때만 이벤트를 호출한다는 뜻이다. 구독자가 없어도 측정 요청 자체는 할 수 있다. 센서 클래스 안에는 Display 필드나 HeatAlarm 필드가 없다.
알림이 전달되는 순서
그림은 뒤의 두 측정을 보기 쉽도록 첫 번째 28도 측정을 생략했다. 시간은 아래로 흐르며, 구독을 해제한 경보는 이후 알림 경로에서 빠진다.
구독은 누가 해제하는가
발행자는 구독된 메서드에 대한 참조를 가지고 있다. 화면이 더 이상 측정값을 쓰지 않을 때 구독을 해제하지 않으면 센서가 그 화면을 계속 붙잡을 수 있다. 나중에 해제할 예정이라면 이름 있는 메서드를 쓰거나 등록한 메서드 참조인 델리게이트를 보관한다.3
이 예제는 등록 순서대로 동기 호출한다. 느린 구독자는 측정 호출을 지연시키고, 구독자가 예외를 던지면 남은 호출을 중단한 채 호출부로 전파한다. 격리나 비동기 전달이 필요하면 그 동작을 별도로 정해야 한다. 같은 메서드를 두 번 등록하면 구독도 두 개가 된다.
이벤트와 메시지 전달
이 알림은 같은 프로세스의 메모리 안에서 전달된다. 다른 프로세스까지 보내거나 재시작 뒤에도 전달하려면 전송 수단과 저장 정책이 함께 필요하다. 옵서버만으로 영속적인 메시지 전달이 구현되지는 않는다. 고정된 수신자 하나라면 직접 메서드를 호출하는 편이 간단할 수 있다.
같이 보기
전체 예제는 .NET 10 콘솔 프로그램에서 실행할 수 있다.
각주
- Gamma, Helm, Johnson, Vlissides. Design Patterns, Observer. 출판사와 패턴 목록. ↩
- Microsoft. Events. ↩
- Microsoft. 이벤트 구독과 구독 해제. ↩