❓ Observer의 의미 ❓
무언가를 관찰하는 사람? 지켜보고 있는 사람?
🔭 옵저버 패턴 🔭
위 그림에서 망원경을 들고 보고 있는 서류들도 있고 그렇지 않은 서류들도 있다. 망원경을 들고 보고 있는 서류들은 저글링을 하는 서류의 실수를 하는 것을 볼 수 있지만 그렇지 않은 서류들은 볼 수 없다.
저글링을 하는 서류가 실수한 것을 본 서류들은 각양각색의 반응을 할 것이다. 어떤 서류는 웃을 수도 있고 어떤 서류는 안타까워 할수도 있다.
즉, 관찰 중인 객체(저글링하는 서류)에 어떤 이벤트가 발생하면 여러 객체 (망원경든 서류들)에게 알리고 이를 알게된 여러 객체, 옵저버들은 어떤 행동을 취하는 것이다.
일단 먼저 직면할 수 있는 문제를 살펴보고 이를 옵저버 패턴이 어떻게 해결할 수 있는지를 알아봄으로써 옵저버 패턴에 대해 이해도를 높여보자.
😒 직면할 수 있는 문제 😒
하이마트와 하이마트에 방문하는 손님이란 객체가 있다고 가정하자.
손님은 새로나오는 맥북에 매우 관심이 있어서 꼭 사고 싶다. 매일 매장을 방문해서 맥북 재고를 확인할 수 있다. 하지만 너무 귀찮고 재고가 없으면 무의미한 짓이다.
하이마트는 매장에 신제품이 들어올 때마다 신제품 출시 문자를 모든 고객들한테 뿌릴 수 있다. 하지만 그 제품을 사길 희망하지 않았던 사람들은 짜증이 난다.
결국, 어느 한쪽은 희생을 해야하는 문제인 것이다. 어떻게 해결할 수 있을까?
😁 해결책 😁
시간이 지남에 따라 바뀔 수 있는 상태를 지닌 객체가 있다고 생각해보자. 이 객체를 Publisher라고 부르고 하이마트라고 생각하자.
옵저버 패턴은 개별 객체인 손님들은 하이마트로부터 오는 이벤트들의 알림들을 구독하거나 구독 취소할 수 있도록 구독 메커니즘
을 추가하도록 하는 패턴이다. 이를 위해선 어떤 것들이 필요할까?
1) 구독자들을 담고 있을 배열 필드
2) 구독자를 담고 있는 배열에서 추가, 제거를 위한 public한 메서드들
⇒ 이렇게 구성하면 하이마트는 알림을 보낼 때 배열 필드를 확인하고 알림 문자를 보내면 된다.
모든 구독자가 같은 인터페이스를 구현하고 하이마트에서는 오직 이 인터페이스를 통해서 구독자들과 통신해야한다. 해당 구독자라는 인터페이스는 하이마트가 알림이랑 데이터를 보낼 때 사용할 수 있는 매개변수들의 집합과 알림 메서드를 선언해야한다.
⇒ 인터페이스를 이용하지 않는다면 엄청 많은 구독자 클래스들과의 결합이 필요할 것이다.
🏗️ 구조(Structure) 🏗️
예제 코드 작성 전 먼저 예제 코드의 구조를 UML을 이용해서 보면 좋을 것 같다.
하이마트는 이벤트가 발생하는 비즈니스 로직을 구현하고 있는 객체이다.
Publisher는 위에서 하이마트 내에 존재하는 것으로 언급했는데 SRP를 생각해서 구독 메커니즘
을 담당하는 클래스로 따로 빼냈다.
구독자를 인터페이스로 만들고 구현체로 알림이 오면 차를 타고 가는 구독자와 지인에게알리는구독자로 나눴다. update가 실행되었을 때 구현체마다 취하는 기능이 달라지는 것이다. 그리고 구독자들은 update 단일 메서드로 대부분의 경우 구성되고 업데이트와 함께 어떤 이벤트의 세부 정보들을 전달할 수 있도록 하는 여러 매개변수가 있을 수 있다. 이것이 Subscriber이다.
📄 예제 코드 설명 📄
1. 하이마트
public class 하이마트 {
Publisher publisher; // 구독 메커니즘을 담당하는 클래스
public 하이마트() {
publisher = new Publisher("맥북", "갤럭시");
}
public void 맥북재고확보() {
System.out.println("하이마트에 맥북 재고가 확보되었음.");
publisher.notify("맥북", "맥북 재고 들어왔습니다.");
}
public void 갤럭시노트북재고확보() {
System.out.println("하이마트에 갤럭시 노트북 재고가 확보되었음.");
publisher.notify("갤럭시", "갤럭시노트북 재고 들어왔습니다.");
}
}
- 하이마트는 이벤트를 발생시키는 클래스이다.
- 이벤트는 맥북재고확보, 갤럭시노트북 재고확보가 존재한다.
2. Publisher
public class Publisher {
private Map<String, List<구독자>> 구독자들 = new HashMap<>();
public Publisher(String... args) {
for (String eventType : args) {
구독자들.put(eventType, new ArrayList<>());
}
}
public void 구독하기(String eventType, 구독자 subscriber) {
구독자들.get(eventType).add(subscriber);
}
public void 구독취소하기(String eventType, 구독자 subscriber) {
구독자들.get(eventType).remove(subscriber);
}
public void notify(String eventType, String 문자내용) {
System.out.println();
System.out.println(eventType + "를 구독한 구독자들에게 이를 알리기");
List<구독자> 이벤트구독자들 = 구독자들.get(eventType);
for (구독자 subscriber : 이벤트구독자들) {
subscriber.update(문자내용);
}
}
}
- Publisher는 변할 수 있는 상태인
구독자들
이란 필드를 가지고 있다.
- 생성자 선언시 외부에서 주입되는 이벤트 타입들을 이용해서 각각의 리스트를 만든다.
- 구독하기, 구독취소하기를 통해서 자신이 바라보고 싶은 이벤트를 설정 및 삭제할 수 있다.
- notify 메서드는 이벤트 발생 시 이를 구독한 구독자들의 update 메서드를 이용하여 문자내용을 보내는 메서드이고 이 과정을 콜백이라고 한다.
3. 구독자
public interface 구독자 {
void update(String 문자내용);
}
---
public class 차타고가는구독자 implements 구독자{
// 알림을 받았을 때, 어떤 행동을 취하기 위해 필요한 필드들.
private final String 차종;
public 차타고가는구독자(String 차종) {
this.차종 = 차종;
}
@Override
public void update(String 문자내용) {
System.out.println();
System.out.println(문자내용 + "라는 문자를 받고 " + 차종 + "를 타고 하이마트로 달려간다.");
}
}
---
public class 지인에게알리는구독자 implements 구독자{
private final String 지인;
public 지인에게알리는구독자(String 지인) {
this.지인 = 지인;
}
@Override
public void update(String 문자내용) {
System.out.println();
System.out.println(문자내용 + "라는 문자를 받고 " + 지인 + "에게 소식을 알린다.");
}
}
- 구독자 인터페이스를 통해서 결합도를 낮추고 각각의 구현 클래스들을 이벤트 발생을 확인했을 때 취할 행동을 update 메서드에서 정의한다.
4. main문
public class 손님 {
public static void main(String[] args) {
하이마트 himart = new 하이마트();
//하이마트에 특정 이벤트에 구독을 한 손님.
himart.publisher.구독하기("맥북", new 차타고가는구독자("소나타"));
himart.publisher.구독하기("갤럭시", new 지인에게알리는구독자("뚜영이"));
System.out.println("1. 맥북 이벤트 발생");
himart.맥북재고확보();
System.out.println("\n#######################################\n");
System.out.println("2. 갤럭시 노트북 이벤트 발생");
himart.갤럭시노트북재고확보();
}
}
- 손님이 특정 이벤트에 구독을 하면서 구독자 구현체를 이용한다.
- 이벤트 발생 시 이를 구독자에게 알리고 이에 맞는 행동을 취한다.
5. 예제 코드 결과
- 손님은 하이마트의 맥북재고에 대한 구독을 신청했다. ( 신청 시 구독자 구현 클래스를 이용하지만 Publisher에서는 구독자라는 인터페이스에 의존해서 알 필요가 없다. )
- 하이마트 맥북재고에 대한 이벤트가 발생했다. 그럼 하이마트에서는 필드로 가지고 있는 Publisher의 notify 메서드를 호출하면서 이벤트 타입과 문자내용을 넘길 것이다.
- Publisher는 이벤트 타입과 문자내용을 받고 해당 이벤트 타입에 맞는 구독자 명단을 가져와 해당 구독자들의 update 메서드를 이용하여 이벤트 발생을 알린다.
- update 메서드 내에서는 이벤트가 발생하면 각 이벤트에 따른 다른 행동들이 명시되어있고 이를 수행한다.
이처럼 Publisher가 가진구독자들이라는 상태가 변할 수 있는 필드를 이용한 구독 메커니즘을 통해서 어떤 이벤트의 발생을 구독자들에게만 알리는 행동을 정의한 패턴이 Observer 패턴이다.
💦 구현 시 고려할 부분 💦
- update 메서드 시그니처
필자가 짠 코드의 Subscriber(구독자)의 update 메서드 시그니처와 항상 같아야 하는가?
그것은 아니다. 반드시 지켜야 하는 약속이 아니고 다른 인자를 넘겨도 되는 등 상황에 맞춰서 유연하게 작성하면 될 것이다.
- notify (알림보내기) 메서드 호출
- 이벤트가 발생할 때 해당 이벤트 메서드 내에서 호출하는 방법
- 사용자(main emd)가 적절한 시기에 Notify()를 호출하는 방법
GoF(Gang of Fours)에서 두 가지 방법 중 선택하라고 한다.
💡 적용 가능한 상황 💡
- 깃허브에 푸쉬될 때마다 노션으로 알림을 주는 것
- 상대방이 카톡 메시지를 보내면 그때 “카톡”이라는 음성 메시지 출력
- MVC의 모델에서 일어나는 이벤트를 옵저버가 통보받고 뷰의 내용을 바꾸는 메서드를 작동 → 모델과 뷰 사이를 느슨히 연결시킴.
⇒ 특정 이벤트에 따른 어떠한 행동을 취하는 경우면 적용 가능하다 생각한다.
👍 장점 👍
- OCP, 하이마트의 코드를 변경하지 않고도 새로운 구독자 클래스를 도입할 수 있다. ( 마트 인터페이스가 있으면 반대로 구독자 클래스들을 변경하지 않고 새로운 마트 클래스를 도입할 수 있음. )
- 런타임에서 객체 간의 관계들을 형성할 수 있다.
👎 단점 👎
- 구독자들은 무작위로 알림을 받는다.
- 통보 체인
한 Subscriber가 통보를 받았을 때 자신이 다시 그 주체로써 다른 Subscriber에게 통보를 하는 형식의 체이닝이다. → 설계가 복잡해지고 디버깅이 어려워진다. ( 이런 경우 Mediator 패턴 도입하면 도움됨. )
- 메모리 누수
Subscriber 객체가 제때 GC에 의한 메모리 할당 해제가 되지 않는 현상이다. 통보 주체가 Subscriber의 참조값을 가지고 있어서 발생하는 것이고 이를 잘 삭제해준다면 피할 수 있다.
Reference
- 예제 코드
Uploaded by N2T