코딩하는 개굴이

[디자인 패턴] 옵저버/관찰자 패턴 (Observer Pattern) 구현하기 본문

기본기 채우기/디자인 패턴

[디자인 패턴] 옵저버/관찰자 패턴 (Observer Pattern) 구현하기

개굴이모자 2022. 7. 2. 13:56
반응형

해당 내용은 Inflearn 의 코딩으로 학습하는 GoF의 디자인 패턴 강좌를 기반으로 정리되었음을 알립니다.
해당 내용은 주로 JAVA 기반으로 코드 설명이 진행됨을 알립니다.

행동 관련 디자인 패턴

Observer Pattern (옵저버 디자인 패턴)

다수의 객체가 특정 객체의 상태 변화를 감지하고 알림을 받는 패턴이다.
Observer Pattern 은 아래 형태로 구현한다.

image

Subject는 여러 옵저버들을 등록하거나 해지할 수 있는 기능을 수행한다. Cleint는 Subject를 이용해 여러 Observer를 등록하고, Subject가 제공하는 여러 메서드를 이용해 Subject의 상태들을 변경할 수 있다. 이때, Subject 는 자신에게 등록되어있는 모든 Observer들을 순회하며 옵저버가 제공하는 특정 메서드들을 호출해준다.

공통된 인터페이스를 제공하며, 옵저버의 호출해야하는 메서드를 호출하며 이벤트와 관련된 정보 혹은 이벤트 자체를 전달하여 옵저버가 해야하는 일을 할 수 있도록 한다.

Concrete Observer 는 인터페이스들의 여러 구현체들로 실제 해야하는 일들을 구현하게된다. update() 는 어떤 데이터를 이용해 호출하라는 자체적인 규약이 된다.

구체적인 모양은 변경되어도 된다. 해당 패턴들의 목적에 부합하면 되므로, Subject 등의 부분에서 인터페이스로 구현되어도 된다.

Observer Pattern의 필요성

유저가 Subject에 맞게, 메시지를 보내면 해당 Subject에 맞는 유저가 메시지를 받는 내용을 설계해본다고 가정하자.

방법은 여러가지 있겠지만, 아래처럼 Tight coupling으로 구현할 수 있을 것이다.

  • User는 ChatServer 객체 를 받아, 그것을 이용해 sendMessage/getMessage를 한다.
  • ChatServer는 특정 subject에 맞게 message들을 add하고 getMessage를 할 수 있다.
  • Client는 ChatServer 객체를 하나 생성해 User 객체들을 생성 시, 이를 넘긴다.

자세한 내용은 아래 코드를 참고하자.

//User.java
package me.whiteship.designpatterns._03_behavioral_patterns._19_observer._01_before;

import java.util.List;

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);
    }
}

///Client.java
package me.whiteship.designpatterns._03_behavioral_patterns._19_observer._01_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("디자인패턴"));
    }
}

//ChatServer.java
package me.whiteship.designpatterns._03_behavioral_patterns._19_observer._01_before;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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는 받은 메시지들을 확인하려면 일정 시간마다 getMessage들을 각 User 별로 다시 호출하는 polling 을 수행해야한다.

또한, 런타임에 유저가 해제되는 등의 상황에서는 해당 메시지를 대응하기가 어렵다.

또한, User 가 아닌 다른 타입(예를들면 Guest 등)의 객체가 메시지들을 받을 필요성이 생긴다면 이를 처리하기 번거롭다.

기본적인 Observer Patterrn

해당 케이스를 Observer Pattern을 이용해 다시 설계해보자.

특정 User 가 선택한 subject 에 맞게 Subscribe를 하고, ChatServer에 User가 message를 sendMessage를 하게 되면, 그의 subject에 맞는 Subscriber 가 message를 받을 수 있게 아래의 구조에 맞추어 초기 설계를 해보자.

  • Subscriber 는 handleMessage를 가진 interface이다.
    • Subscriber 는 Observer이다.
package com.gaegul.observer;

public interface Subscriber { //message를 handle 하는 Observer 생성
    void handleMessage(String message);
}
  • User는 Subscriber를 상속하고 name 을 받는다.
    • User는 Concrete Observer이다.
package com.gaegul.observer;

public class User implements Subscriber { //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);
    }
}
  • ChatServer는 Subscriber 들을 register, unregister 를 할 수 있게 하며, sendMessage 를 할 수 있게 한다.
    • ChatServer 는 Subject이다.
package com.gaegul.observer;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ChatServer { //여러 observer들을 subject별로 등록하고 해제할 수 있어야한다.

    private Map<String, List<Subscriber>> subscribers = new HashMap<>();

    public void register(String subject, Subscriber subscriber) {
        //특정 subject에 대해 구독자 리스트가 이미 있을 경우, 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);
        }
    }

    public void sendMessage(User user, String subject, String message) {
        if (subscribers.containsKey(subject)) {
            String userMessage = user.getName() + " : " + message;
            this.subscribers.get(subject).forEach(s -> s.handleMessage(userMessage));
        }
    }
}
//Client.java
package com.gaegul.observer;

public class Client {

    public static void main(String[] args) {
        ChatServer chatServer = new ChatServer();

        User user1 = new User("Doraemon");
        User user2 = new User("Dorami");
        chatServer.register("Robot", user1);
        chatServer.register("Robot", user2);

        chatServer.register("Tool", user1);

        chatServer.sendMessage(user1, "Robot", "I am cat type robot!");
        chatServer.sendMessage(user2, "Tool", "This is Anywhere door");
    }
}

위의 내용을 보면 Oberser 패턴의 장단점이 보일 것이다.

  • 장점
    • Subject(상태를 변경하는 객체)와 Observer(변경을 감지하는 객체)간의 관계를 느슨하게 유지(Loose Coupling)할 수 있다.
      • Loose Coupling의 장점 : 코드를 작성하기 쉽고, 코드를 변경하기 쉽고, 재사용하기 좋으며, 테스트하기 용이하다.
    • Subject의 상태 변경을 주기적으로 조회하지 않고 자동으로 감지할 수 있다.
      • ChatServer 에서 주기적으로 새로운 메시지가 보내졌는지 polling 을 하지 않아도 데이터를 새로 받아올 수 있도록 한다.
    • 런타임중에 새로운 옵저버들을 등록/해제할 수 있다.
      • Client 에서 중간에 unregister 를 하고 sendMessage를 보내더라도 의도한대로 동작한다.
  • 단점
    • 코드가 복잡해진다.
      • 객체를 등록/해제하는 과정들을 필요
    • 객체를 제대로 해제하지 않을 경우, ChatServer 에 쌓이게되고 Garbage Collector 의 대상이 되지 않게 된다. (Map 에 담아두었기 때문에 참조가 남아있기 때문에 가비지 컬렉터의 대상이 되지 않고 메모리 누수가 생긴다.)
      • 적절한 순간에 객체를 해제하고 참조를 없애주어야한다.
      • 명시적으로 해제하기 어려운 상황에서는 Weak Reference 를 적용한 Hash Map 을 사용하면 객체가 해제되면 자동으로 사라지게된다. (해결책은 아니므로, 이에 기대고 설계하는 것을 권장하지 않는다.)

정리

간단하게 말하면, Observer 패턴은 Observer 를 상속하는 Concrete Observer 를 Subject 내부에서 list 로 지니고 있어서, Client 가 사용 시에는 해당 list 를 돌며 해당하는 Observer 들에 이벤트들을 호출한다.
Subject 는 Observer들을 등록/해제하며 관리하여 동적인 변화들에 유동적으로 대응할 수 있도록 한다.

반응형
Comments