행동 관련 : 옵저버 패턴

    들어가기 전

    이 글은 백기선님의 GOF 강의를 복습하며 작성한 글입니다. 

     


    옵저버 패턴

    • GOF : 다수의 객체가 특정 객체 상태 변화를 감지하고 알림을 받는 패턴
    • Component
      • Subject
        • 여러 Observer를 가지고 있음. Observer를 등록 / 해지할 수 있는 기능 제공. 
        • Subject는 자신의 상태를 변경할 수 있는 메서드를 제공함.
        • Subject는 상태가 변경되면, 자신에게 등록된 Observer를 순회하면서 Observer가 제공하는 메서드를 모두 호출함. (알림)
      • Observer
        • 인터페이스 하나 제공함. Observer가 이벤트를 받아서 해야할 일을 정의함. 
    • 옵저버 패턴을 적용하면 Pub - Sub 패턴을 쉽게 구현할 수 있음. 
      • Polling 구조를 적용하기 어려울 때, Pub - Sub 패턴을 적용하면 좀 더 효율적으로 컴퓨터 자원을 사용할 수 있게 될 것임. 
    • Subject에는 상태가 있고, Subject의 상태를 관찰하고 있는 Observer가 존재한다. Subject의 상태가 변경되면 Observer가 상태 변화를 감지하고 특정 행위를 한다.
      • Polling, pub-sub이 가능할 것으로 보임. 

     


    Observer 패턴의 장/단점

    • 장점
      • 상태를 변경하는 객체 (publisher)와 변경을 감지하는 객체 (subscribe)의 관계를 느슨하게 유지할 수 있음. 
      • Subject의 상태 변경을 주기적으로 조회하지 않고(Polling) 자동으로 감지할 수 있음. 
      • 런타임에 옵저버를 추가하거나 제거할 수 있음.
    • 단점
      • 복잡도가 증가함.
      • 다수의 Observer를 등록한 후, 해지를 하지 않으면 Observer 인스턴스가 계속 쌓임. (메모리 누수 현상)

    단점의 마지막 부분은 다음과 같이 설명해 볼 수 있다. Observer가 해제 되지 않으면 Map 내에 Oberserver의 참조가 계속 남고 있는 상태다. 참조가 남았기 때문에 GC 대상이 안되기 때문에 메모리 누수현상이 발생할 수 있다. 따라서 가급적이면 사용이 끝난 Observer는 반드시 제거하도록 강제해야한다. 

    WeakReference를 차선으로 사용할 수도 있지만 완벽하지는 않다. 왜냐하면 WeakReference의 GC 시점이 명확하지 않기 때문이다. 

     


    Before 코드

    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("디자인패턴"));
        }
    }

    채팅 프로그램을 나타낸 코드다. 

    • User1이 보낸 메세지를 읽기 위해서 User2는 getMessage() 메서드를 호출해야한다. 즉, polling 구조로 동작중임. 
    public class ChatServer {
    
        private Map<String, List<String>> messages;
    
        public ChatServer() { this.messages = new HashMap<>(); }
    
    
        public void add(String subject, String message) {
            if (messages.containsKey(subject)) {
                messages.get(subject).add(message);
            } else {
                List<String> messageList = new ArrayList<>();
                messageList.add(message);
                messages.put(subject, messageList);
            }
        }
    
        public List<String> getMessage(String subject) { return messages.get(subject);}
    }

    ChatServer로 메세지를 보내면, Map에 저장해두고 User가 getMessage() 메서드를 호출해 읽어갈 수 있도록 구성되어 있다.

    public class User {
        private ChatServer chatServer;
        public User(ChatServer chatServer) { this.chatServer = chatServer; }
        public void sendMessage(String subject, String message) { chatServer.add(subject, message); }
        public List<String> getMessage(String subject) { return chatServer.getMessage(subject); }
    }

    User는 chatServer를 참조하고 있고, ChatServer에게 메세지를 보내고 읽어오는 역할을 한다. 

    이 때의 문제점은 무엇일까? User1이 메세지를 보냈지만 User2가 그 메세지를 읽기 위해서 계속 poll을 해야한다는 것이다. 아무 것도 없을 때 poll을 할 수도 있고, 너무 늦게 poll을 해서 많은 메세지가 쌓여있을 수도 있다.


    옵저버 패턴 적용 

    이 부분을 해결하기 위해 옵저버 패턴을 적용할 수 있다. 옵저버 패턴의 의미는 '다수의 객체가 특정 객체의 상태 변화를 감지하고 알림을 받는 패턴'이다. 

    위 코드 관점에서 살펴보면 다음과 같다. 

    • Observer : User. User는 메세지를 보내기도 하지만, ChatServer에 메세지가 저장되면 그것을 가져와서 읽어야 함. 
    • Subject : ChatServer를 Subject로 관리할 수 있음. 
      • Chatserver에는 Observer를 등록, 제거하는 API가 필요함.
      • Chatserver에는 Subject (자기 자신)의 상태를 변경할 수 있는 메서드 추가가 필요함.
        • 이 경우, 메서드를 호출하면 자신의 상태가 바뀐 것으로 판단하고 Observer에게 알림을 보내줌. (변경을 감지하고 알림을 전송받음을 구현) 
    // Subject 역할을 하자.
    public class ChatServer {
    
        private final Map<String, List<User>> subjects = new HashMap<>();
    
        public void subscribe(String subject, User user) {
            List<User> observers = this.subjects.getOrDefault(subject, new ArrayList<>());
            observers.add(user);
            subjects.put(subject, observers);
        }
    
        public void unsubscribe(String subject, User user) {
            if (this.subjects.containsKey(subject)) {
                List<User> users = this.subjects.get(subject);
                users.remove(user);
            }
        }
    
        public void sendMessage(User user, String subject, String message) {
            this.subjects.getOrDefault(subject, new ArrayList<>())
                    .forEach(u -> u.handleMessage(user, message));
        }
    }

    Subject 역할을 하는 ChatServer를 구현한다. ChatServer는 다음 기능을 추가한다.

    • subscribe() : 특정 subject에 Observer를 추가한다.
    • unsubscribe() : 특정 subject를 구독하고 있는 Observer를 제거한다. 
    • sendMessage() : Subject의 상태를 변경하는 메서드다. 이 메서드가 호출되면, Subject 클래스의 상태가 변경된 것으로 판단하고 해당되는 Observer에게 요청을 보낸다. 
    // Pub + Sub 역할을 모두 다 함.
    // 여기서는 Subscriber 인터페이스 필요없음.
    // User의 이름을 참조할 것인데, Subscribe 인터페이스 자체가 name을 가지지 않기 때문에 getName()을 인터페이스로 제공하기 애매함.
    public class User {
    
        private final String name;
        public User(String name) { this.name = name; }
        private String getName() { return this.name; }
        public void handleMessage(User user, String message) {
            System.out.printf(
                    "User: %s, Message: %s",
                    user.getName(),
                    message);
        }
    
        
    }

    User 클래스를 생성한다. User 클래스는 다음 역할을 한다.

    • publisher : Subject의 상태를 변경하는 역할을 함.
    • subscribe : Subject의 상태 변화를 감시함. 
    • handleMessage() : Subject의 상태가 변화되면 알림을 받는데, 알림을 받을 때 호출되는 메서드.
    public class Client {
        public static void main(String[] args) {
            ChatServer chatServer = new ChatServer();
    
            User user1 = new User("hello1");
            User user2 = new User("hello2");
    
            chatServer.subscribe("디자인패턴", user2);
            chatServer.sendMessage(user1, "디자인패턴", "이번엔 옵저버 패턴입니다.");
        }
    }

    옵저버 패턴으로 변경한 Client 코드는 다음과 같다. 

    이렇게 코드를 변경하고 얻어낼 수 있는 장점은 다음과 같다.

    1. 기존에는 Poll()로 메세지를 가져왔었는데, 이제는 이벤트가 발생하면 알림을 받으므로 자원을 효율적으로 사용할 수 있음. 
    2. publish - subscribe의 관계를 느슨하게 가져갈 수 있다.

    현재 코드에서는 2번이 잘 이해가 되지 않을 수도 있다. 왜냐하면 User가 publish하고 subscribe를 둘다 하기 때문이다. 그러나 이것을 Admin이 publish하고 User가 Subscribe 한다고 생각하면 좀 더 명확해진다. Admin은 ChatServer로 메세지를 보내고, User는 ChatServer를 통해서 메세지를 가져간다. 즉, Admin은 User를 몰라도 되고 User 역시 Admin을 몰라도 된다.  

     

    '디자인 패턴' 카테고리의 다른 글

    행동 관련 : State 패턴  (0) 2023.11.30
    행동 관련 : 템플릿 메서드 패턴  (0) 2023.11.30
    구조 관련 : 프록시 패턴  (0) 2023.11.26
    구조 관련 : 파사드 패턴  (0) 2023.11.25
    행동 관련 : 메멘토 패턴  (1) 2023.11.25

    댓글

    Designed by JB FACTORY