Observer Pattern이란?
옵저버 패턴(Observer Pattern)은 옵저버(관찰자)들이 관찰하고 있는 대상자의 상태가 변화가 있을 때마다 대상자는 직접 목록의 각 관찰자들에게 통지하고, 관찰자들은 알림을 받아 조치를 취하는 행동 패턴이다.
옵저버 패턴은 여타 다른 디자인 패턴들과 다르게 일대다(one-to-many) 의존성을 가지는데, 주로 분산 이벤트 핸들링 시스템을 구현하는 데 사용된다. Pub/Sub(발행/구독) 모델로도 알려져 있기도 하다.
이 패턴을 이해하는 데 있어 유튜브로 비유해 보면 쉽다.
유튜브 채널은 발행자(Subject)가 되고 구독자들은 관찰자(Observer)가 되는 구조로 보면 된다.
유튜버가 영상을 올리면 구독자들은 영상이 올라왔다는 변화를 감지하여 알림이 오게 된다.
옵저버 패턴 구조
- ISubject : 관찰 대상자를 정의하는 인터페이스
- ConcreteSubject : 관찰당하는 대상자 / 발행자 / 게시자
- Observer들을 리스트(List, Map, Set.. 등)로 모아 합성(composition)하여 가지고 있음
- Subject의 역할은 관찰자인 Observer들을 내부 리스트에 등록/삭제하는 인프라를 갖고 있다. (register, remove)
- Subject가 상태를 변경하거나 어떤 동작을 실행할때, Observer 들에게 이벤트 알림(notify)을 발행한다.
- IObserver : 구독자들을 묶는 인터페이스 (다형성)
- Observer : 관찰자 / 구독자 / 알림 수신자.
- Observer들은 Subject가 발행한 알림에 대해 현재 상태를 취득한다.
- Subject의 업데이트에 대해 전후 정보를 처리한다.
옵저버 패턴은 여타 다른 디자인 패턴과 똑같이 상호작용할 객체를 합성(composition)을 하고 메서드 위임을 통해 구성하는 패턴임은 똑같지만, 핵심은 합성한 객체를 리스트로 관리하고 리스트에 있는 관찰자 객체들에게 모두 메서드 위임을 위한 전파 행위를 한다는 점을 기억하면 된다.
유튜브로 살펴보는 옵저버 패턴
옵저버패턴의 예시로 유튜브채널과 구독자의 관계를 구현하려고 한다. 전체적인 과정은 이렇다.
채널에 구독자들이 구독을 한다. -> 채널에서 영상을 업로드한다. -> 업로드 사실을 구독자들에게 알린다.
채널에 영상이 업로드되면 채널에 구독을 한 구독자들에게 업로드 알람이 갈 것이다. 따라서 1대다 관계를 구현해야 한다.
옵저버패턴이 없다면
Channel.java
public class Channel {
String videoName;
LocalDateTime uploadDateTime;
public void upLoadVideo(String videoName) {
this.videoName = videoName;
this.uploadDateTime = LocalDateTime.now();
}
}
Subscriber.java
public interface Subscriber {
void display();
}
KoreanSubscriber.java
public class KoreanSubscriber implements Subscriber {
String name;
Channel channel;
public KoreanSubscriber(String name, Channel channel) {
this.name = name;
this.channel = channel;
}
@Override
public void display() {
System.out.printf("%s님이 좋아할만한 영상이 업로드 되었습니다.%n", name);
System.out.printf("%s (%s)%n", channel.videoName, channel.uploadDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")));
}
}
Client.java
public class Client {
public static void main(String[] args) {
Channel channel = new Channel();
List<Subscriber> subscribers = new ArrayList<>();
subscribers.add(new KoreanSubscriber("홍길동", channel));
subscribers.add(new KoreanSubscriber("임꺽정", channel));
subscribers.add(new KoreanSubscriber("김철수", channel));
//비디오가 업로드됨
channel.upLoadVideo("요리 잘하는법!");
//알람
for (Subscriber subscriber : subscribers) {
subscriber.display();
}
channel.upLoadVideo("게임 잘하는법");
// ...
}
}
홍길동님이 좋아할만한 영상이 업로드 되었습니다.
요리 잘하는법! (2023-05-17 21:41)
임꺽정님이 좋아할만한 영상이 업로드 되었습니다.
요리 잘하는법! (2023-05-17 21:41)
김철수님이 좋아할만한 영상이 업로드 되었습니다.
요리 잘하는법! (2023-05-17 21:41)
동작에 아무 문제없어 보이지만, 너무 Client 클래스에 코드가 집약적으로 구성됨을 볼 수 있다.
Channel을 구독한 Subscriber들을 관리하는 프로세스를 클라이언트에서 관리하고 있으며, 영상이 업로드될 때 데이터를 전달받아 display 하는 것이 아닌 Subscriber 클래스 내에 있는 api 객체에 자신이 직접 조회하여 display 하는 것이기 때문이다.
옵저버 패턴 적용시키기
구독자들을 관리하는 프로세스를 클라이언트가 아닌 Channel 클래스에서 관리하도록 설정하고, 영상이 업로드가 된다면 자동으로 업로드 사실을 전달받을 수 있도록 일종의 전파 및 동작 행위를 Channel 클래스에서 구현할 필요성이 있다.
Subject.java
public interface Subject {
void registerObserver(Observer observer); // 구독 추가
void removeObserver(Observer observer); // 구독 삭제
void notifyObservers(); // Subject 객체의 상태 변경시 이를 모든 옵저버에게 알람
}
Channel.java
public class Channel implements Subject{
String videoName;
LocalDateTime uploadDateTime;
List<Observer> subscribers = new ArrayList<>();
public void upLoadVideo(String videoName) {
this.videoName = videoName;
this.uploadDateTime = LocalDateTime.now();
notifyObservers();
}
@Override
public void registerObserver(Observer observer) {
subscribers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
subscribers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer subscriber : subscribers) {
subscriber.display(this);
}
}
}
Observer.java
public interface Observer {
void display(Channel channel);
}
KoreanSubscriber.java
public class KoreanSubscriber implements Observer{
String name;
public KoreanSubscriber(String name) {
this.name = name;
}
@Override
public void display(Channel channel) {
System.out.printf("%s님이 좋아할만한 영상이 업로드 되었습니다.%n", name);
System.out.printf("%s (%s)%n", channel.videoName, channel.uploadDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")));
}
}
Client.java
public class Client {
public static void main(String[] args) {
Channel channel = new Channel();
channel.registerObserver(new KoreanSubscriber("홍길동"));
channel.registerObserver(new KoreanSubscriber("임꺽정"));
channel.registerObserver(new KoreanSubscriber("김철수"));
//비디오가 업로드됨
channel.upLoadVideo("요리 잘하는법!");
//알아서 전파되어 출력
//비디오가 업로드됨
channel.upLoadVideo("게임 잘하는법!");
}
}
홍길동님이 좋아할만한 영상이 업로드 되었습니다.
요리 잘하는법! (2023-05-17 22:27)
임꺽정님이 좋아할만한 영상이 업로드 되었습니다.
요리 잘하는법! (2023-05-17 22:27)
김철수님이 좋아할만한 영상이 업로드 되었습니다.
요리 잘하는법! (2023-05-17 22:27)
홍길동님이 좋아할만한 영상이 업로드 되었습니다.
게임 잘하는법! (2023-05-17 22:27)
임꺽정님이 좋아할만한 영상이 업로드 되었습니다.
게임 잘하는법! (2023-05-17 22:27)
김철수님이 좋아할만한 영상이 업로드 되었습니다.
게임 잘하는법! (2023-05-17 22:27)
클라이언트에서 별다른 명령 없이 알아서 자동으로 발행자로부터 구독자들이 데이터를 전달받아 적절히 자동으로 처리하는 것을 볼 수 있다. 핵심은 구독자들의 관리를 발행자 객체에서 리스트로 한다는 점과 리스트에 있는 모든 구독자들의 메서드를 위임하여 실행함으로써 변화가 있을 때마다 이벤트성 알림으로 전파한다는 컨셉으로써 옵저버 패턴을 이해하면 된다.
옵저버 패턴 특징
패턴 사용 시기
- 앱이 한정된 시간, 특정한 케이스에만 다른 객체를 관찰해야 하는 경우
- 대상 객체의 상태가 변경될 때마다 다른 객체의 동작을 트리거해야 할 때
- 한 객체의 상태가 변경되면 다른 객체도 변경해야 할 때. 그런데 어떤 객체들이 변경되어야 하는지 몰라도 될 때
- MVC 패턴에서도 사용된
- MVC의 Model과 View의 관계는 Observer 패턴의 Subject 역할과 Observer 역할의 관계에 대응된다.
- 하나의 Model에 복수의 View가 대응한다.
패턴 장점
- Subject의 상태 변경을 주기적으로 조회하지 않고 자동으로 감지할 수 있다.
- 발행자의 코드를 변경하지 않고도 새 구독자 클래스를 도입할 수 있어 OCP를 준수한다.
- 런타임 시점에서에 발행자와 구독 알림 관계를 맺을 수 있다.
- 상태를 변경하는 객체(Subject)와 변경을 감지하는 객체(Observer)의 관계를 느슨하게 유지할 수 있다. (느슨한 결함)
패턴 단점
- 구독자는 알림 순서를 제어할 수 없고, 무작위 순서로 알림을 받음
- 하드 코딩으로 구현할 수는 있겠지만, 복잡성과 결합성만 높아지기 때문에 추천되지는 않는 방법이다.
- 옵저버 패턴을 자주 구성하면 구조와 동작을 알아보기 힘들어져 코드 복잡도가 증가한다.
- 다수의 옵저버 객체를 등록 이후 해치지 않는다면 메모리 누수가 발생할 수도 있다.
참고 자료 :
'백엔드 > Java' 카테고리의 다른 글
[Java] JVM의 Class Loader (0) | 2023.05.23 |
---|---|
[Java] 상속(Inheritance)과 합성(Composition) (1) | 2023.05.19 |
[Java] 전략 패턴(Strategy Pattern) (0) | 2023.05.17 |
[Java] 템플릿 메서드 패턴(Template Method Pattern) (0) | 2023.05.17 |
[Java] 스트림(Stream) (0) | 2023.05.11 |