옵저버 패턴(Observer Pattern)에서는 한 객체의 상태가 바뀌면 극 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다(one-to-mnay) 의존성을 정의합니다.
위 그림의 구조를 살펴보면 옵저버 패턴은 크게 Subject와 Observer가 있습니다. Subject의 주요한 기능은 여러 옵저버들을 등록하거나 해지할 수 있는 기능을 제공하는게 Subject의 주요한 목적입니다. Client는 이러한 Subject를 이용해서 여러 Observer들을 특정 Subject에 등록을 해두고 또 Subject가 제공하는 특정한 메서드를 사용해서 Subject의 상태를 변경합니다. Subject의 상태가 변경되면 자신한테 등록되어있는 모든 Observer들을 순회하면서 Observer가 제공하는 특정한 메서드들을 모두 호출합니다. Observer의 역할은 공통된 인터페이스를 하나 제공해서 그 인터페이스에서 어떤 이벤트가 발생했을 때 Observer에 호출해줘야되는 메서드하나가 필요합니다. 그래서 그 이벤트가 담고 있는 정보자체를 Observer에 전달하면서 특정메서드를 호출시키고 그 메서드 안에서 실제로 옵저버가 해야할 일을 할 수 있게끔 해줍니다. ConcreteObserver는 Observer의 구현체로 실제 해야할 기능이 담겨있는 클래스입니다.
옵저버 패턴이 필요한 예제 코드를 살펴보겠습니다. Client에 간단한 채팅 애플리케이션이 구현되어있습니다. ChatServer를 생성해서 user1이 sendMessage로 메시지를 보내고 user2는 "디자인패턴"과 관련있는 메시지를 조회합니다. 그다음 user1이 "디자인패턴"주제에 "예제 코드 보는 중.."이라는 메시지를 보내고 다시 user2가 "디자인패턴"주제에 대한 메시지를 조회합니다. 근데 방금 user1이 보낸 메시지는 user2가 "디자인패턴"의 주제를 조회하기 전까지는 user2에 전달이 되지 않습니다. 어떤 user든 특정한 주제에 있어서 메시지를 받고 싶으면 본인이 직접 getMessage메서드를 호출해서 주기적으로 가져와야하는데 주기적으로 가져와야하는 이런 방식이 과연 효율적인지 생각해볼 수 있습니다. 중간에 메시지가 항상 존재한다면 가져오는데 메시지가 변한다면 굳이 다시 가져올 필요가 없습니다. 이렇게 polling방식보단 옵저버 디자인패턴을 적용해보겠습니다.
public class Client {
public static void main(String[] args) {
ChatServer chatServer = new ChatServer();
User user1 = new User(chatServer);
user1.sendMessage("디자인패턴", "이번엔 옵저버 패턴입니다.");
user1.sendMessage("롤드컵2021", "LCK 화이팅!");
User user2 = new User(chatServer);
System.out.println(user2.getMessage("디자인패턴"));
user1.sendMessage("디자인패턴", "예제 코드 보는 중..");
System.out.println(user2.getMessage("디자인패턴"));
}
}
위에 살펴봤던 코드를 Observer패턴을 적용해서 수정해보겠습니다. Observer역할을하는 Subscriber를 선언합니다.
public interface Subscriber {
void handleMessage(String message);
}
Subscriber의 구현체인 User를 구현합니다. 메시지를 받아서 출력하도록 handleMessage를 재정의 합니다.
public class User implements Subscriber {
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public void handleMessage(String message) {
System.out.println(message);
}
}
Subject역할인 ChatServer를 구현해서 Observer인 Subscriber들을 등록하고 해지할수 있는 register와 unregister메서드를 구현합니다. 새로운 어떤 subject에 대한 Subscriber를 등록할때 해당하는 subscriber가 있다면 리스트를 가져와서 그대로 더해주고 새로운 subject라는 리스트를 생성해서 넣어주도록 합니다.
public class ChatServer {
private Map<String, List<Subscriber>> subscribers = new HashMap<>();
public void register(String subject, Subscriber subscriber) {
if (this.subscribers.containsKey(subject)) {
this.subscribers.get(subject).add(subscriber);
} else {
List<Subscriber> list = new ArrayList<>();
list.add(subscriber);
this.subscribers.put(subject, list);
}
}
public void unregister(String subject, Subscriber subscriber) {
if (this.subscribers.containsKey(subject)) {
this.subscribers.get(subject).remove(subscriber);
}
}
}
그다음 상태를 변경할 수 있는 기능을 제공해야합니다. ChatServer에는 message를 전송하는 메서드를 제공하도록 합니다. 어떤 User가 해당 subject를 구독하고 있는 subscriber들에게 메시지를 전송하도록 하는 메서드 입니다.
public void sendMessage(User user, String subject, String message) {
if (this.subscribers.containsKey(subject)) {
String userMessage = user.getName() + ": " + message;
this.subscribers.get(subject).forEach(s -> s.handleMessage(userMessage));
}
}
이제 이 옵저버패턴을 사용하는 Client코드를 생성합니다.
public class Client {
public static void main(String[] args) {
ChatServer chatServer = new ChatServer();
User user1 = new User("joohyuk");
User user2 = new User("weekbelt");
chatServer.register("오징어게임", user1);
chatServer.register("오징어게임", user2);
chatServer.register("디자인패턴", user1);
chatServer.sendMessage(user1, "오징어게임", "아.. 이름이 기억났어.. 일남이야.. 오일남");
chatServer.sendMessage(user2, "디자인패턴", "옵저버 패턴으로 만든 채팅");
chatServer.unregister("디자인패턴", user2);
chatServer.sendMessage(user2, "디자인패턴", "옵저버 패턴 장, 단점 보는 중");
}
}
부분적으로 살펴보면 "오징어게임"주제에 user1, user2를 등록하고, "디자인패턴"주제에 user1을 등록했습니다. 그다음 user1이 "오징어게임"주제로 "아.. 이름이 기억났어.. 일남이야.. 오일남"메시지를 구독자들에게 전달하기 때문에 user1, user2가 이 문자열을 출력하게 됩니다. 그다음 user2가 "디자인패턴"주제로 "옵저버 패턴으로 만든 채팅"이 문자열로 전송하기 때문에 구독자인 user1만 문자열을 출력하게 됩니다.
chatServer.register("오징어게임", user1);
chatServer.register("오징어게임", user2);
chatServer.register("디자인패턴", user1);
chatServer.sendMessage(user1, "오징어게임", "아.. 이름이 기억났어.. 일남이야.. 오일남");
chatServer.sendMessage(user2, "디자인패턴", "옵저버 패턴으로 만든 채팅");
joohyuk: 아.. 이름이 기억났어.. 일남이야.. 오일남
joohyuk: 아.. 이름이 기억났어.. 일남이야.. 오일남
weekbelt: 옵저버 패턴으로 만든 채팅
옵저버(Observer) 패턴은 상태를 변경하는 객체(publisher)와 변경을 감지하는 객체(subscriber)의 관계를 느슨하게 유지할 수 있습니다. Subject의 상태 변경을 주기적으로 조회하지 않고 자동으로 감지할 수 있고 런타임에 옵저버를 추가하거나 제거할 수 있습니다. 하지만 복잡도가 증가하고 다수의 Observer 객체를 등록 이후 해지하지 않는다면 메모리 누수가 발생할 수도 있습니다.