행동 패턴 - 옵저버(Observer)
- 참조: https://refactoring.guru/design-patterns/observer
- 참조: GoF의 디자인 패턴
- 옵저버(Observer) 패턴
옵저버 패턴은 종속자(Dependent), 게시-구독(Publisher-Subscribe) 패턴이라고도 불리우며, 디자인 패턴을 공부해보지 않았더라도 한번쯤은 들어봤을법한 유명한 패턴이다. 옵저버 패턴은 관찰하고 있는 객체에 일어난 사건을 여러 객체들에게 알리기위한 구독 매커니즘이다.
- 문제점
Customer와 Store 객체의 타입이 있다고 가정해보자. 어떤 소비자는 매장에서 곧 출시될 특정 제품 브랜드에 관심이 많다. 이 소비자가 제품이 출시되었는지 확인하기 위해서 매일 매장을 방문할수도 있지만 이렇게 확인하는 방법은 너무 번거롭다.
아니면 매장에 신상품이 출시될때마다 모든 소비자에게 이메일을 발송하는 방법도 있다. 메일발송은 소비자가 매장에 직접 방문하지 않아도 되지만, 해당 제품에 관심이 없는 소비자에게는 짜증을 유발한다.
매장에 직접 방문해서 신상제품을 확인하는것은 너무 번거롭고, 그렇다고 해서 해당 제품에 관심이 없는 소비자에게까지 메일을 발송하는 방법도 썩 좋은 해결책은 아닌것 같다. 어떻게 이 문제를 해결해야할까?
- 해결책
종종 관심있는 상태를 가진 객체를 주제("subject") 라고 부르지만, 그 관심있는 상태를 다른 객체들에게 알려주기도 하기 때문에 게시자("publisher") 라고도 부른다. 또 게시자의 상태변화를 지켜보는 객체들을 구독자("subscribers")라고 부른다.
옵저버패턴은 게시자 클래스에 구독 매커니즘을 추가해서, 개별 객체가 게시자에서 발생하는 사건들을 구독하거나 구독해지할 수 있는 기능을 제공한다. 단어는 어렵지만 구성요소는 그렇게 복잡하진 않는데, 구독자 객체들을 참조하는 field와 구독 및 구독을 해지할 수 있는 public 메소드로 이루어져있다.
게시자에게 중요한 사건이 발생할때마다 구독자들을 조사해서 알림 메소드를 호출한다. 실제 app을 제작하다보면 어떤 게시자 클래스 하나에 이벤트를 추적하는 수십개의 구독자 클래스가 있는 경우가 있다. 일반적인 상황에서는 게시자 클래스와 구독자 클래스의 결합도가 높아봤자 좋을게 없다.
위와 같은 이유로 모든 구독자들은 같은 인터페이스를 구현해야하며, 게시자는 해당 인터페이스를 통해서만 동작해야한다. 이 인터페이스는 게시자가 알림과 함게 상황에 따른 데이터를 전달하는데 사용할 수 있는 매개변수 집합과 함께 알림 메서드를 선언해야 한다.
만약 구현해야 하는 app 이 여러 형태의 게시자를 가져야 하고, 구독자들이 이 여러 형태의 게시자들과 모두 동작해야한다면 해당 게시자들은 같은 인터페이스를 구현해야 한다. 이 인터페이스는 몇 가지 구독 메소드만 갖고있으면 된다. 해당 인터페이스는 구독자들이 게시자들의 세부 클래스들과 결합없이 게시자의 상태를 관찰할 수 있게 해준다.
- 실생활 예제
만약 뉴스나 매거진을 구독하고 있다면, 새로운 이슈를 확인하기 위해서 상점에 가지 않아도 된다. 대신 게시자는 새로운 이슈를 발행직후나 사전에 메일박스로 보낸다.
게시자는 구독자들의 목록을 관리하고 어떤 매거진에 관심이 있는지 알고 있다. 또 구독자들은 자신이 원할 때 언제든지 구독을 해지할 수 있다.
- 구조
옵저버패턴의 참여자를 알아보자.
- Publisher: 다른 객체들의 관심있는 이벤트를 발행한다. 해당 이벤트들은 게시자가 자신의 상태를 바꾸거나 특정 행동을 수행할 때 발생한다. 게시자는 새로운 구독자가 참여하거나 구독을 해지하는 기능을 제공한다. 새로운 이벤트가 발생하면 게시자는 구독자 목록을 순회하여 구독자 인터페이스안에 선언된 알림 메소드를 호출한다.
- Subscriber: Subscriber는 알림 인터페이스를 선언한다. 대부분의 경우에는 단일 update 메소드로 구성되어있다. 이 메소드는 게시자가 업데이트와 함께 세부정보를 전달할 수 있는 여러 개의 매개변수를 가질수도 있다.
- Concrete Subscribers: Concrete Subscribers 는 게시자에 의해 발생된 알림에 대한 응답으로 어떤 행동을 수행한다. 이 클래스들은 게시자가 세부 클래스와 결합도가 높아지지 않도록 모두 같은 인터페이스를 구현해야한다. 대게 구독자는 정확한 갱신을 수행하기 위해 상황에 따른 정보가 필요하다. 따라서 게시자는 알림 메소드의 인자로서 컨텍스트 데이터를 전달하기도 한다. 구독자가 모든 데이터를 직접 가져오기 위해서 게시자 자체가 인자가 되기도 한다.
- Client: Client 는 게시자와 구독자 객체들을 각각 따로 생성하고, 게시자 업데이트를 위해 구독자를 등록한다.
- 시나리오
이번 예제에서는 옵저버 패턴을 이용하여 텍스트 에디터가 자신의 상태변화를 다른 서비스 객체에 알리는 상황을 구현해보도록 하자.
구독자의 목록은 동적으로 컴파일되며, 런타임에 알림에 대한 수신을 시작하거나 중단할 수 있다. 위 구현에서 Editor 클래스는 구독자 목록을 관리하고 있지 않고, 해당 작업을 EventManager 에게 위임하고 있다. EventManager를 발전 시키면 중앙 집중형 방식의 이벤트 관리자 역할로 어떤 객체라도 게시자의 역할을 수행하게 할 수 있다.
게시자 클래스는 모든 구독자와 동일한 인터페이스를 통해서 교류할 수 있다면 프로그램에 새로운 구독자가 추가되는것을 신경쓰지 않아도 된다.
- Java 예제
우선 구독자의 인터페이스 및 구현체들부터 정의해보자. EventType 을 String 대신 enum 으로 구현해보았다. 구독자 인터페이스는 파일명을 String 으로 받는 update 메소드를 선언한다.
public enum EventType {
OPEN, SAVE;
}
public interface EventListener {
void update(String fileName);
}
구독자 인터페이스를 구현하는 2 가지 구현체 EmailAlertsListener와 LoggingListener는 아래와 같이 간단하게 작성하였다. 자신의 클래스명과 전달받은 Data를 콘솔로 출력하였다.
public class EmailAlertListener implements EventListener {
@Override
public void update(String fileName) {
System.out.println("update EventAlertListener: " + fileName);
}
}
public class LoggingListener implements EventListener {
@Override
public void update(String fileName) {
System.out.println("update LoggingListener: " + fileName);
}
}
다음으로 옵저버 패턴의 참여자들을 사용하는 Editor 클래스이다. 게시자인 Editor가 구독자들을 직접 관리를 해도 되지만, 중간에 EventManager 클래스를 따로 둠으로 인해서 비즈니스 로직에 더 집중할 수 있도록 구성한다. Editor는 파일을 열거나 저장할 수 있는데, 열거나 저장할 때 해당 행위에 맞는 이벤트 타입과 파일명을 넘겨준다.
public class Editor {
public EventManager events;
public Editor() {
super();
events = new EventManager();
}
public void openFile(String fileName) {
events.notify(EventType.OPEN, fileName);
}
public void saveFile(String fileName) {
events.notify(EventType.SAVE, fileName);
}
}
이제 옵저버 패턴에서 구독자들을 관리하여 핵심역할을 수행하는 EventManager 클래스를 살펴보자. 먼저 listeners 를 Map 으로 선언하여 각 EventType 마다 구독자들을 따로 관리할 수 있게 구성한다. 이렇게 해야 이벤트 타입별로 구독자들을 구분할 수 있다.
구독이나 구독해지 메소드인 subscribe와 unSubscribe 에서는 이벤트 타입의 List를 가져온 후, 각각의 기능을 수행한다. notify 역시 EventType 에 따라 설정된 구독자들에게만 공지를 한다.
public class EventManager {
private Map<EventType, List<EventListener>> listeners;
public EventManager() {
super();
this.listeners = new HashMap<>();
this.listeners.put(EventType.OPEN, new ArrayList<>());
this.listeners.put(EventType.SAVE, new ArrayList<>());
}
public void subscribe(EventType eventType, EventListener listener) {
List<EventListener> eventListener = listeners.get(eventType);
eventListener.add(listener);
}
public void unSubscribe(EventType eventType, EventListener listener) {
List<EventListener> eventListener = listeners.get(eventType);
eventListener.remove(listener);
}
public void notify(EventType eventType, String data) {
List<EventListener> eventListener = listeners.get(eventType);
eventListener.forEach(listener -> listener.update(data));
}
}
이제 Test를 해보자. LoggingListener는 Editor에서 파일을 열거나 저장하는 이벤트를 모두 구독한다. 반면 EmailAlertListener는 Editor가 저장하는 이벤트에 대해서만 구독한다.
파일을 열거나 저장할때 LoggingListener는 update 되지만, EmailAlertListener는 저장할 때에만 update 되는것을 알 수 있다.
그 후 EmailAlertListener는 저장 이벤트에 대한 구독을 해지한다. 이제는 Editor에서 파일을 열거나 저장하여도 EmailAlertListener에 알림이 가지 않는다.
public static void main(String[] args) {
Editor editor = new Editor();
EventListener loggingListener = new LoggingListener();
EventListener emailAlertListener = new EmailAlertListener();
editor.events.subscribe(EventType.OPEN, loggingListener);
editor.events.subscribe(EventType.SAVE, loggingListener);
editor.events.subscribe(EventType.SAVE, emailAlertListener);
editor.openFile("File 1");
editor.openFile("File 2");
editor.saveFile("File 1");
editor.saveFile("File 2");
editor.events.unSubscribe(EventType.SAVE, emailAlertListener);
editor.openFile("File 3");
editor.saveFile("File 3");
}
========================
update LoggingListener: File 1
update LoggingListener: File 2
update LoggingListener: File 1
update EventAlertListener: File 1
update LoggingListener: File 2
update EventAlertListener: File 2
update LoggingListener: File 3
update LoggingListener: File 3