From fc20f032cbce1577380c558adeb396032dc8201c Mon Sep 17 00:00:00 2001 From: cjw72 Date: Wed, 16 Mar 2022 22:02:37 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=EC=98=B5=EC=A0=80=EB=B2=84=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=20-=20=EC=98=88=EC=A0=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../summary/example-code.md" | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 "\355\226\211\353\217\231/10\354\243\274\354\260\250-\354\230\265\354\240\200\353\262\204/summary/example-code.md" diff --git "a/\355\226\211\353\217\231/10\354\243\274\354\260\250-\354\230\265\354\240\200\353\262\204/summary/example-code.md" "b/\355\226\211\353\217\231/10\354\243\274\354\260\250-\354\230\265\354\240\200\353\262\204/summary/example-code.md" new file mode 100644 index 0000000..0d4b050 --- /dev/null +++ "b/\355\226\211\353\217\231/10\354\243\274\354\260\250-\354\230\265\354\240\200\353\262\204/summary/example-code.md" @@ -0,0 +1,232 @@ +# 옵저버 패턴 + +**채팅 서비스**를 만들 때 다음과 같은 `폴링(polling)` 방식을 적용한다해보자 + +- 유저가 특정 `토픽`에 `메세지`를 전달후 저장 +- 유저가 주기적으로 서버에 원하는 `토픽`의 `전체 메세지`를 요청 + +위 방식의 예제 코드를 보자. + +```java +//채팅 서버 클래스 +public class ChatServer { + //특정 토픽의 메세지 리스트 + private Map> messages = new HashMap<>(); + + //토픽에 메세지 추가 + public void addMessage(String topic, String message) { + //토픽 존재시 + if (this.messages.containsKey(topic)) { + //토픽에 메세지 추가 + this.messages.get(topic).add(message); + } else { + List messageList = new ArrayList<>(); + //메세지 추가 + messageList.add(message); + //토픽 추가 + this.messages.put(topic, messageList); + } + } + + //토픽 메세지 얻기 + public List getMessage(String topic) { + return this.messages.get(topic); + } +} +``` + +```java +//채팅을 이용하는 클라이언트 클래스 +public class Client { + private final ChatServer chatServer; + private final String name; + + //채팅 서버와 Client 이름 받기 + public Client(ChatServer chatServer, String name) { + this.chatServer = chatServer; + this.name = name; + } + + //토픽에 메세지 전송 + public void sendMessage(String topic, String message) { + chatServer.addMessage(topic, message); + } + + //해당 토픽 메세지 출력 + public void printMessage(String topic) { + List messages = this.getMessage(topic); + if(Objects.isNull(messages)) return; + //메세지 출력 + System.out.printf("======== 이름: %s 토픽: %s =======\n", this.name, topic); + messages.forEach(System.out::println); + } + + //해당 토픽 메세지 얻기 + private List getMessage(String topic) { + return this.chatServer.getMessage(topic); + } +} +``` + +```java +public class Main { + public static void main(String[] args) { + //채팅 서버 생성 + ChatServer chatServer = new ChatServer(); + //클라이언트 생성 + Client client1 = new Client(chatServer, "홍길동"); + Client client2 = new Client(chatServer, "아무개"); + + //사람인이라는 토픽에 메세지 전송 + client1.sendMessage("사람인", "안녕하세요"); + client2.sendMessage("사람인", "반갑습니다"); + //클라이언트가 서버로부터 메세지 출력 호출 + client1.printMessage("사람인"); + client2.printMessage("사람인"); + + //점핏이라는 토픽에 메세지 전송 + client1.sendMessage("점핏", "잘부탁드립니다"); + //클라이언트가 서버로부터 메세지 출력 호출 + client2.printMessage("점핏"); + } +} + +결과 +======== 이름: 홍길동 토픽: 사람인 ======= +안녕하세요 +반갑습니다 +======== 이름: 아무개 토픽: 사람인 ======= +안녕하세요 +반갑습니다 +======== 이름: 아무개 토픽: 점핏 ======= +잘부탁드립니다 +``` + +예제에서 `클라이언트`는 메세지를 출력하고 싶을 때마다 `채팅 서버`에 요청을 보낸다. + +이 방식의 문제는 전달된 메세지는 없지만 `클라이언트`가 주기적으로 서버에 메세지를 요청하면 불필요한 작업이 늘어나 리소스가 낭비된다는 점이다. + +`옵저버 패턴`을 사용하면 메세지 전달을 감지하여 `클라이언트`가 불필요한 작업을 없앨 수 있다. + +패턴을 적용한 예제를 보자 + +**요구사항**은 다음과 같다 + +- 유저는 특정 `토픽`을 구독 가능 +- 유저가 구독한 `토픽`에 `메세지`를 전송시 `토픽`을 구독한 다른 유저는 `메세지` 확인 가능 + +![Untitled](https://user-images.githubusercontent.com/32676275/158595711-7b63d418-90ae-44dd-b9d3-3c2ea4262f23.png) + +```java +/** + * 구독자 인터페이스 + * ** Observer ** + */ +public interface Subscriber { + //전달받은 메세지 처리 메소드 + void handleMessage(String message); +} +``` + +```java +/** + * Subscriber 인터페이스 구현체 + * ** ConcreteObserver ** + */ +public class User implements Subscriber{ + //유저 이름 + private final String name; + + //생성자 + public User(String name) { + this.name = name; + } + + //메세지 처리 + @Override + public void handleMessage(String message) { + System.out.printf("%s --- %s 수신\n",message,this.name); + } + //Getter +} +``` + +```java +/** + * 채팅 서버 클래스 + * ** Subject ** + */ +public class ChatServer { + //각 토픽에 구독자들을 저장 + public Map> subscribers = new HashMap<>(); + + //구독 등록 + public void register(String topic, Subscriber subscriber) { + //토픽 존재시 구독자 추가 + if(this.subscribers.containsKey(topic)) { + this.subscribers.get(topic).add(subscriber); + return; + } + //존재하지 않을시 토픽 생성후 추가 + List newSubscribers = new ArrayList<>(); + newSubscribers.add(subscriber); + this.subscribers.put(topic, newSubscribers); + } + + //구독 취소 + public void unregister(String topic, Subscriber subscriber) { + //토픽 존재시 토픽의 구독자 제거 + if(this.subscribers.containsKey(topic)) { + this.subscribers.get(topic).remove(subscriber); + } + } + + //토픽의 구독자들에게 메세지 전송 + public void sendMessage(User user, String topic, String message) { + // 해당 토픽이 존재시 + if (this.subscribers.containsKey(topic)) { + String userMessage = user.getName() + ": " + message; + // 토픽 구독자들에게 메세지 전달 + this.subscribers.get(topic).forEach(s -> s.handleMessage(userMessage)); + } + } +} +``` + +```java +public class Main { + public static void main(String[] args) { + ChatServer chatServer = new ChatServer(); + User user1 = new User("홍길동"); + User user2 = new User("아무개"); + + System.out.println("==========각 토픽에 구독자가 1명일 때=========="); + chatServer.register("개발", user1); //구독 + chatServer.register("기획", user2); //구독 + chatServer.sendMessage(user1, "개발", "개발 토픽에 메세지 전달"); //메시지전송 + chatServer.sendMessage(user2, "기획", "기획 토픽에 메세지 전달"); //메시지전송 + + System.out.println("==========구독자 추가후 메세지 전달=========="); + chatServer.register("기획", user1); //구독 + chatServer.sendMessage(user1, "기획", "기획 구독후 메세지 전달"); //메시지전송 + + System.out.println("==========구독 취소후 메세지 전달=========="); + chatServer.unregister("기획", user2); //구독 취소 + chatServer.sendMessage(user2, "기획", "기획 구독 취소후 메세지 전달"); //메시지 전송 + } +} + +결과 +==========각 토픽에 구독자가 1명일 때========== +홍길동: 개발 토픽에 메세지 전달 --- 홍길동 수신 +아무개: 기획 토픽에 메세지 전달 --- 아무개 수신 +==========구독자 추가후 메세지 전달========== +홍길동: 기획 구독후 메세지 전달 --- 아무개 수신 +홍길동: 기획 구독후 메세지 전달 --- 홍길동 수신 +==========구독 취소후 메세지 전달========== +아무개: 기획 구독 취소후 메세지 전달 --- 홍길동 수신 +``` + +`옵저버 패턴`을 적용하면 `ChatServer(Subject)` 에서 `User(Observer)` 리스트를 가지고 있는 구조가 된다. + +`ChatServer.sendMessage()`메소드 호출시 `User(Observer).handleMessage()` 메소드를 호출함으로써 **유저는 메세지가 전달되는 시점에 처리할 수 있는 구조가 된다.** \ No newline at end of file From 7426593dcd52610487898ce855061f28f025c972 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=B0=BD=EC=84=AD?= Date: Mon, 21 Mar 2022 23:08:21 +0900 Subject: [PATCH 2/3] =?UTF-8?q?Add)=20=EC=98=B5=EC=A0=80=EB=B2=84=20?= =?UTF-8?q?=ED=8C=A8=ED=84=B4=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...0\353\262\204 \355\214\250\355\204\264.md" | 277 ++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 "\355\226\211\353\217\231/10\354\243\274\354\260\250-\354\230\265\354\240\200\353\262\204/summary/\354\230\265\354\240\200\353\262\204 \355\214\250\355\204\264.md" diff --git "a/\355\226\211\353\217\231/10\354\243\274\354\260\250-\354\230\265\354\240\200\353\262\204/summary/\354\230\265\354\240\200\353\262\204 \355\214\250\355\204\264.md" "b/\355\226\211\353\217\231/10\354\243\274\354\260\250-\354\230\265\354\240\200\353\262\204/summary/\354\230\265\354\240\200\353\262\204 \355\214\250\355\204\264.md" new file mode 100644 index 0000000..d4de914 --- /dev/null +++ "b/\355\226\211\353\217\231/10\354\243\274\354\260\250-\354\230\265\354\240\200\353\262\204/summary/\354\230\265\354\240\200\353\262\204 \355\214\250\355\204\264.md" @@ -0,0 +1,277 @@ +# 옵저버 패턴 + +> 다수의 객체가 **특정 객체 상태 변화를 감지하고 알림**을 받는 패턴 + +![](https://refactoring.guru/images/patterns/diagrams/observer/structure.png?id=365b7e2b8fbecc8948f34b9f8f16f33c) + +- **Publisher** : 여러 구독자를 등록/해지할 수 있는 기능을 제공한다. 구독자들의 상태를 변경하거나, 행동을 직접적으로 실행시킬 수 있는 권리를 가진다. + +- **Subscriber**: 이벤트 발생시 구독자에서 호출하는 메소드를 추가한다. + +- **Concrete Subscribers**: 실제로 구독자가 해야할일을 구현한다. + +즉, 발행-구독 형식의 패턴이 구현된다. + +## 옵저버 패턴은 언제 사용되는가? + +- 외부에서 발생한 이벤트에 대한 응답을 전달 해주고 싶을때 (이벤트 기반 프로그래밍) + +- 객체 속성 값이 변하는 경우 응답주고 싶은 경우 + +- 채팅 서비스 + +## 옵저버 패턴 예제 코드 + +**채팅 서비스**를 만들 때 다음과 같은 `폴링(polling)` 방식을 적용한다해보자 + +- 유저가 특정 `토픽`에 `메세지`를 전달후 저장 +- 유저가 주기적으로 서버에 원하는 `토픽`의 `전체 메세지`를 요청 + +위 방식의 예제 코드를 보자. + +```java +//채팅 서버 클래스 +public class ChatServer { + //특정 토픽의 메세지 리스트 + private Map> messages = new HashMap<>(); + + //토픽에 메세지 추가 + public void addMessage(String topic, String message) { + //토픽 존재시 + if (this.messages.containsKey(topic)) { + //토픽에 메세지 추가 + this.messages.get(topic).add(message); + } else { + List messageList = new ArrayList<>(); + //메세지 추가 + messageList.add(message); + //토픽 추가 + this.messages.put(topic, messageList); + } + } + + //토픽 메세지 얻기 + public List getMessage(String topic) { + return this.messages.get(topic); + } +} +``` + +```java +//채팅을 이용하는 클라이언트 클래스 +public class Client { + private final ChatServer chatServer; + private final String name; + + //채팅 서버와 Client 이름 받기 + public Client(ChatServer chatServer, String name) { + this.chatServer = chatServer; + this.name = name; + } + + //토픽에 메세지 전송 + public void sendMessage(String topic, String message) { + chatServer.addMessage(topic, message); + } + + //해당 토픽 메세지 출력 + public void printMessage(String topic) { + List messages = this.getMessage(topic); + if(Objects.isNull(messages)) return; + //메세지 출력 + System.out.printf("======== 이름: %s 토픽: %s =======\n", this.name, topic); + messages.forEach(System.out::println); + } + + //해당 토픽 메세지 얻기 + private List getMessage(String topic) { + return this.chatServer.getMessage(topic); + } +} +``` + +```java +public class Main { + public static void main(String[] args) { + //채팅 서버 생성 + ChatServer chatServer = new ChatServer(); + //클라이언트 생성 + Client client1 = new Client(chatServer, "홍길동"); + Client client2 = new Client(chatServer, "아무개"); + + //사람인이라는 토픽에 메세지 전송 + client1.sendMessage("사람인", "안녕하세요"); + client2.sendMessage("사람인", "반갑습니다"); + //클라이언트가 서버로부터 메세지 출력 호출 + client1.printMessage("사람인"); + client2.printMessage("사람인"); + + //점핏이라는 토픽에 메세지 전송 + client1.sendMessage("점핏", "잘부탁드립니다"); + //클라이언트가 서버로부터 메세지 출력 호출 + client2.printMessage("점핏"); + } +} + +결과 +======== 이름: 홍길동 토픽: 사람인 ======= +안녕하세요 +반갑습니다 +======== 이름: 아무개 토픽: 사람인 ======= +안녕하세요 +반갑습니다 +======== 이름: 아무개 토픽: 점핏 ======= +잘부탁드립니다 +``` + +예제에서 `클라이언트`는 메세지를 출력하고 싶을 때마다 `채팅 서버`에 요청을 보낸다. + +이 방식의 문제는 전달된 메세지는 없지만 `클라이언트`가 주기적으로 서버에 메세지를 요청하면 불필요한 작업이 늘어나 리소스가 낭비된다는 점이다. + +`옵저버 패턴`을 사용하면 메세지 전달을 감지하여 `클라이언트`가 불필요한 작업을 없앨 수 있다. + +패턴을 적용한 예제를 보자 + +**요구사항**은 다음과 같다 + +- 유저는 특정 `토픽`을 구독 가능 +- 유저가 구독한 `토픽`에 `메세지`를 전송시 `토픽`을 구독한 다른 유저는 `메세지` 확인 가능 + +![Untitled](https://user-images.githubusercontent.com/32676275/158595711-7b63d418-90ae-44dd-b9d3-3c2ea4262f23.png) + +```java +/** + * 구독자 인터페이스 + * ** Observer ** + */ +public interface Subscriber { + //전달받은 메세지 처리 메소드 + void handleMessage(String message); +} +``` + +```java +/** + * Subscriber 인터페이스 구현체 + * ** ConcreteObserver ** + */ +public class User implements Subscriber{ + //유저 이름 + private final String name; + + //생성자 + public User(String name) { + this.name = name; + } + + //메세지 처리 + @Override + public void handleMessage(String message) { + System.out.printf("%s --- %s 수신\n",message,this.name); + } + //Getter +} +``` + +```java +/** + * 채팅 서버 클래스 + * ** Subject ** + */ +public class ChatServer { + //각 토픽에 구독자들을 저장 + public Map> subscribers = new HashMap<>(); + + //구독 등록 + public void register(String topic, Subscriber subscriber) { + //토픽 존재시 구독자 추가 + if(this.subscribers.containsKey(topic)) { + this.subscribers.get(topic).add(subscriber); + return; + } + //존재하지 않을시 토픽 생성후 추가 + List newSubscribers = new ArrayList<>(); + newSubscribers.add(subscriber); + this.subscribers.put(topic, newSubscribers); + } + + //구독 취소 + public void unregister(String topic, Subscriber subscriber) { + //토픽 존재시 토픽의 구독자 제거 + if(this.subscribers.containsKey(topic)) { + this.subscribers.get(topic).remove(subscriber); + } + } + + //토픽의 구독자들에게 메세지 전송 + public void sendMessage(User user, String topic, String message) { + // 해당 토픽이 존재시 + if (this.subscribers.containsKey(topic)) { + String userMessage = user.getName() + ": " + message; + // 토픽 구독자들에게 메세지 전달 + this.subscribers.get(topic).forEach(s -> s.handleMessage(userMessage)); + } + } +} +``` + +```java +public class Main { + public static void main(String[] args) { + ChatServer chatServer = new ChatServer(); + User user1 = new User("홍길동"); + User user2 = new User("아무개"); + + System.out.println("==========각 토픽에 구독자가 1명일 때=========="); + chatServer.register("개발", user1); //구독 + chatServer.register("기획", user2); //구독 + chatServer.sendMessage(user1, "개발", "개발 토픽에 메세지 전달"); //메시지전송 + chatServer.sendMessage(user2, "기획", "기획 토픽에 메세지 전달"); //메시지전송 + + System.out.println("==========구독자 추가후 메세지 전달=========="); + chatServer.register("기획", user1); //구독 + chatServer.sendMessage(user1, "기획", "기획 구독후 메세지 전달"); //메시지전송 + + System.out.println("==========구독 취소후 메세지 전달=========="); + chatServer.unregister("기획", user2); //구독 취소 + chatServer.sendMessage(user2, "기획", "기획 구독 취소후 메세지 전달"); //메시지 전송 + } +} + +결과 +==========각 토픽에 구독자가 1명일 때========== +홍길동: 개발 토픽에 메세지 전달 --- 홍길동 수신 +아무개: 기획 토픽에 메세지 전달 --- 아무개 수신 +==========구독자 추가후 메세지 전달========== +홍길동: 기획 구독후 메세지 전달 --- 아무개 수신 +홍길동: 기획 구독후 메세지 전달 --- 홍길동 수신 +==========구독 취소후 메세지 전달========== +아무개: 기획 구독 취소후 메세지 전달 --- 홍길동 수신 +``` + +`옵저버 패턴`을 적용하면 `ChatServer(Subject)` 에서 `User(Observer)` 리스트를 가지고 있는 구조가 된다. + +`ChatServer.sendMessage()`메소드 호출시 `User(Observer).handleMessage()` 메소드를 호출함으로써 **유저는 메세지가 전달되는 시점에 처리할 수 있는 구조가 된다.** + +## 패턴의 장/단점 + +✅ 장점: + +- OCP(개방/폐쇄원칙) 새로운 구독자 클래스를 발행자의 코드 변경 없이 추가 가능. 그리고 그 반대로 발행자 인터페이스가 있다는 가정하에 역도 성립한다. +- 런타임안에 객체간 관계를 성립할 수있다. + +🚨 단점: + +- 구독자들은 랜덤으로 알람이 간다. (순서가 중요하면 사용 불가) + +## 비슷한 패턴 + +- **책임연쇄패턴, 커맨드패턴, 중제자, 옵저버**는 요청에 대한 발송자와 수신자 연결 방식이 다양하다. + + - **책임연쇄패턴**은 잠재적인 리시버중 하나가 처리될때까지 요청을 동적인 리시버체인에 따라서 순차적인 처리를 한다. + + - **커맨드 패턴**은 수신자와 송신자 사이의 단방향의 연결을 설립한다. + + - **중제자 패턴**은 송신자와 수신자를 직접적인 통신은 배제하고 중제자 객체를 통한 간접적인 소통 가능하도록 강제한다. + + - **옵저버 패턴**은 수신자의 전달받은 요청으로부터 동적으로 구독하거나 비구독하는 방식으로 구성한다 From 52a417f62bc56de708d85ca8b6291045d5d89637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=B0=BD=EC=84=AD?= Date: Mon, 21 Mar 2022 23:13:37 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix)=20=EB=82=B4=EC=9A=A9=20=EC=95=BD?= =?UTF-8?q?=EA=B0=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...230\265\354\240\200\353\262\204 \355\214\250\355\204\264.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/\355\226\211\353\217\231/10\354\243\274\354\260\250-\354\230\265\354\240\200\353\262\204/summary/\354\230\265\354\240\200\353\262\204 \355\214\250\355\204\264.md" "b/\355\226\211\353\217\231/10\354\243\274\354\260\250-\354\230\265\354\240\200\353\262\204/summary/\354\230\265\354\240\200\353\262\204 \355\214\250\355\204\264.md" index d4de914..0183134 100644 --- "a/\355\226\211\353\217\231/10\354\243\274\354\260\250-\354\230\265\354\240\200\353\262\204/summary/\354\230\265\354\240\200\353\262\204 \355\214\250\355\204\264.md" +++ "b/\355\226\211\353\217\231/10\354\243\274\354\260\250-\354\230\265\354\240\200\353\262\204/summary/\354\230\265\354\240\200\353\262\204 \355\214\250\355\204\264.md" @@ -258,7 +258,7 @@ public class Main { ✅ 장점: - OCP(개방/폐쇄원칙) 새로운 구독자 클래스를 발행자의 코드 변경 없이 추가 가능. 그리고 그 반대로 발행자 인터페이스가 있다는 가정하에 역도 성립한다. -- 런타임안에 객체간 관계를 성립할 수있다. +- 런타임 안에서 객체를 추가하거나 제거하는 방식이 가능해진다. 🚨 단점: