목차
Observer Pattern(관찰자 패턴)
관찰자 패턴은 사건을 만드는 주체(Subject)와 사건에 반응하는 관찰자(Observer)를 분리한다. 주문 상태가 바뀌었을 때 알림 화면과 감사 로그에 알리는 경우를 생각해 보자. 주문 객체가 각 수신자의 구체 클래스를 직접 호출하면 새로운 수신자가 생길 때 주문 코드를 고쳐야 한다. 관찰자를 등록해 두면 주체는 공통 인터페이스에만 의존한다.
![]()
위 그림의 Subject는 구독 목록을 관리하고, 각 Observer는 통지를 처리한다. 다음은 이벤트의 내용을 불변 값으로 전달하는 간단한 예제다.
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
record OrderPaid(String orderId) { }
interface OrderObserver {
void onPaid(OrderPaid event);
}
final class OrderEvents {
private final List<OrderObserver> observers = new CopyOnWriteArrayList<>();
void subscribe(OrderObserver observer) {
observers.add(observer);
}
void unsubscribe(OrderObserver observer) {
observers.remove(observer);
}
void publish(OrderPaid event) {
for (OrderObserver observer : observers) {
observer.onPaid(event);
}
}
}
class Main {
public static void main(String[] args) {
OrderEvents events = new OrderEvents();
OrderObserver audit = event -> System.out.println("audit: " + event.orderId());
OrderObserver notice = event -> System.out.println("notice: " + event.orderId());
events.subscribe(audit);
events.subscribe(notice);
events.publish(new OrderPaid("O-42"));
events.unsubscribe(notice);
}
}
첫 발행은 audit: O-42와 notice: O-42를 순서대로 출력한다. 구독을 해제하면 이후 발행에서는 알림 관찰자가 호출되지 않는다. 기존 예제의 ConcerteObserverA와 ConcreteObserverB 이름 불일치 때문에 컴파일되지 않던 부분도 이처럼 동일한 타입 계약으로 정리할 수 있다.
CopyOnWriteArrayList는 구독 목록을 순회하는 중 등록·해제되어도 반복자에 구조 변경 예외가 나지 않도록 한다. 대신 구독 목록을 바꿀 때 배열 복사 비용이 든다. 구독 변경이 드물고 발행이 많은 경우에 적합한 선택이다. 이 예제의 publish는 동기 호출이다. 느린 관찰자가 있으면 발행자가 기다리고, 관찰자가 예외를 던지면 뒤 관찰자는 호출되지 않는다. 실제 시스템에서는 실패가 전체 작업을 중단해야 하는지 정하고, 필요하면 예외 격리나 비동기 큐를 설계해야 한다. 비동기 큐를 사용하면 재시도·중복 처리·순서 보장이라는 새로운 문제가 생긴다.
또한 주체가 관찰자를 강하게 참조하므로 사용이 끝난 관찰자는 구독을 해제해야 한다. 화면의 생명주기가 끝났는데도 구독이 남아 있으면 메모리 누수와 중복 알림으로 이어질 수 있다. 관찰자 패턴은 객체 사이의 통지 관계를 설명하며, 분산 메시지 브로커나 모든 이벤트 처리 방식과 같은 뜻은 아니다.