Design Pattern

Observer

다미르 2022. 3. 2. 23:53

소셜 미디어(facebook, twitter)등을 사용해본 경험이 있나요?

팔로우한 사람이 게시글을 등록할 때마다 자동으로 알람이 오는 것은 어떻게 만드는 걸까요?

 

다른 예시를 들어보자면, Javascript에서 이벤트에 대한 처리는 주로 아래와 같이 구현을 합니다.

어떻게 작동하는 걸까요?

window.onload = function() {
	// do something on load
};

이러한 질문들에 답을 줄 수 있는 디자인 패턴 두 번째, 옵저버(observer) 패턴에 대해서 알아보고자 합니다.

 

* observer ?

- a person who watches or notices something

- a person who follows events, especially political ones, closely and comments pulicly on them

- 즉, 무엇인가를 지속적으로 관찰하는 사람(혹은 것)을 뜻합니다


1. Definition

observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. It is also referred to as the publish-subscribe pattern.

- 1대다 관계의 객체들 사이에서 활용

- 주체(subject) 객체 상태의 변화가 있을 때, 의존관계에 있는 모든 객체들(observer)에게 자동적으로 알림

- 발행/구독(publish-subscribe) 패턴으로도 불림

https://howtodoinjava.com/design-patterns/behavioral/observer-design-pattern/

 

2. Structure

State는 임의로 정의한 Enum 객체이다

- 주체(Subject) : 옵저버를 관리(register(), remove())하고, 상태(State) 변경 시 옵저버들에게 알림(notifyToAll())

- 옵저버(Observer) : 주체(Subject)로부터 알림이 오면, 해당 상태를 변경(update)

- 옵저버가 직접 주체 객체의 상태(State)에 접근하지 않음(push 방식)

 

*) Implementation

- 주체(Subject)

public class Subject {

    private List<Observer> observers = new ArrayList<Observer>();

    private State state = State.READY;

    public State getState() {
        return this.state;
    }

    public void setState(State state) {
        this.state = state;
        notifyToAll();
    }

    public void notifyToAll() {
        observers.forEach(o -> o.update(getState()));
    };

    public void register(Observer o) {
        this.observers.add(o);
    };

    public void remove(Observer o) {
        this.observers.remove(o);
    };
}

- Observer

// Observer 상위 interface
public interface Observer {
    public void update(State state);
}

// Observer 구현체A
public class ObserverA implements Observer {

    private State state;

    @Override
    public void update(State state) {
        this.state = state;
        System.out.println("state of ObserverA : " + this.state);
    }
}

// Observer 구현체B
public class ObserverB implements Observer {

    private State state;

    @Override
    public void update(State state) {
        this.state = state;
        System.out.println("state of ObserverB : " + this.state);
    }
}

- Client

public class Client {
    public static void main(String[] args) {

        // 주체 생성
        Subject subject = new Subject();
        System.out.println("initial state of subject : " + subject.getState());

        // 옵저버 생성
        Observer observerA = new ObserverA();
        Observer observerB = new ObserverB();

        // 주체에 등록
        subject.register(observerA);
        subject.register(observerB);

        // 상태 변경
        subject.setState(State.CHANGED);
    }
}

// 실행결과
// initial state of subject : READY
// state of ObserverA : CHANGED
// state of ObserverB : CHANGED

- 주체의 상태가 변경(READY -> CHANGED)되자, 등록된 옵저버들(ObserverA, ObserverB)에 자동적으로 알림

 

3. Example

* 요구사항

- 유튜브처럼 스트리머(Subject)가 새로운 게시물을 등록하면 구독자(Observer)에게 자동적으로 알림을 발송합니다

 

* step 1) 게시물에 대한 객체(Post)와 사용자 정보(Profile) 객체를 작성합니다

getter, setter 등은 생략

// Profile 객체
// getter, setter, toString 생략
public class Profile implements Serializable {

    private String userId;

    private String name;

    public Profile() {
        this.userId = UUID.randomUUID().toString();
        this.name = "";
    }
}

* step2) 스트리머(subject), 구독자(subscriber) 객체를 작성합니다

Streamer, Subscriber

 

- DefaultStreamer

public class DefaultStreamer {

    private Profile profile;

    private List<Post> posts;

    private DefaultStreamer() {
    }

    // 정적 팩토리 메서드
    public static DefaultStreamer create(String name) {
        DefaultStreamer streamer = new DefaultStreamer();
        streamer.profile = new Profile();
        streamer.posts = new ArrayList<>();
        streamer.getProfile().setName(name);

        return streamer;
    }

    // profile getter
    public Profile getProfile() {
        return this.profile;
    }

    // post getter
    public List<Post> getAllPosts() {
        return this.posts;
    }

    public List<Post> getPost(String title) {
        return this.posts.stream()
            .filter(p -> p.getTitle().equals(title))
            .collect(Collectors.toList());
    }

    // 게시물 등록
    public void publish(String title, String content) {
        Post post = new Post();
        post.setUserId(this.profile.getUserId())
            .setPostId(String.valueOf(this.posts.size() + 1))
            .setTitle(title)
            .setContent(content);
        
        this.posts.add(post);

        String message = this.profile.getName() 
                        + "posted new content"
                        + ": " + post.getTitle();

        sendNotification(message);
    }

    // 알람 전송
    public void sendNotification(String message) {
        // TODO : 구독자들에게 알람 전송 기능 구현
    }
}

- DefaultSubscriber

public class DefaultSubscriber {

    private Profile profile;

    private DefaultSubscriber() {
    }

    // 정적 팩토리 메서드
    public static DefaultSubscriber create(String name) {
        DefaultSubscriber subscriber =  new DefaultSubscriber();
        subscriber.profile = new Profile();
        subscriber.getProfile().setName(name);

        return subscriber;
    }

    // profile getter
    public Profile getProfile() {
        return this.profile;
    }
	
    // 구독자가 게시물을 등록하면, 알람을 전송을 위해서 호출되어야 하는 메서드
    public void alert(String message) {
    }
}

- DefaultStreamer.sendNotification()의 기능을 어떻게 구현해야 할까요?

: DefaultScriber.alert() 메서드에 현재 등록된 포스트에 대한 내용을 넘겨주는 것이 최종 목표입니다

: 현재 존재하는 구독자 객체의 alert() 메서드를 직접 호출한다면 어떻게 될까요?

 

*) 문제점

- 구독자가 추가/삭제 될 때마다 sendNotification()의 내용을 직접 수정해야합니다(OCP 위반)

- 현재 상태에서는 다른 종류의 스트리머/다른 종류의 구독자가 추가된다면 확장할 수 있는 방법이 없습니다

- 또한, 스트리머가 다른 종류의 구독자를 통합적으로 사용할 수 있는 방법이 없습니다

 

- 이러한 상황을 개선하기 위해서 상위 타입의 인터페이스를 정의하고, 옵저버 패턴을 적용해봅시다

 

*) step3 인터페이스 작성

*) Streamer

- 구독자(observer) 목록을 멤버 필드에 저장합니다

- publish() -> sendNotification() -> Subscriber.alert() 호출하는 방식으로 메세지를 전달합니다

// Stream 객체의 상위 타입 인터페이스
public interface Streamer {
    
    public void registerSubscriber(Subscriber subscriber);

    public void removerSubscriber(Subscriber subscriber);

    public void sendNotification(String message);
}

// Streamer 구현체
public class DefaultStreamer implements Streamer {

    private Profile profile;

    private List<Post> posts;

    private List<Subscriber> subscribers;

    private DefaultStreamer() {
    }

    // 정적 메서드
    public static DefaultStreamer create(String name) {
        DefaultStreamer streamer = new DefaultStreamer();
        streamer.profile = new Profile();
        streamer.posts = new ArrayList<Post>();
        streamer.subscribers = new ArrayList<Subscriber>();
        streamer.getProfile().setName(name);

        return streamer;
    }

    // profile getter
    public Profile getProfile() {
        return this.profile;
    }

    // post getter
    public List<Post> getAllPosts() {
        return this.posts;
    }

    public List<Post> getPost(String title) {
        return this.posts.stream()
            .filter(p -> p.getTitle().equals(title))
            .collect(Collectors.toList());
    }

    // subscriber getter
    public List<Subscriber> getScribers() {
        return this.subscribers;
    }

    // 게시물 등록
    public void publish(String title, String content) {
        Post post = new Post();
        post.setUserId(this.profile.getUserId())
            .setPostId(String.valueOf(this.posts.size() + 1))
            .setTitle(title)
            .setContent(content);
        
        this.posts.add(post);

        String message = this.profile.getName() 
                        + " posted new content"
                        + " : " + post.getTitle();

        sendNotification(message);
    }

    @Override
    public void registerSubscriber(Subscriber subscriber) {
        this.subscribers.add(subscriber);
    }

    @Override
    public void removerSubscriber(Subscriber subscriber) {
        this.subscribers.remove(subscriber);
    }

    @Override
    public void sendNotification(String message) {
        this.subscribers.stream()
            .forEach(s -> s.alert(message));
    }
}

*) Subscriber

// Subscriber 상위 타입 인터페이스
public interface Subscriber {

    public void alert(String message);
    
}

// Subscriber 구현체
public class DefaultSubscriber implements Subscriber {

    private Profile profile;

    private DefaultSubscriber() {
    }

    // 정적 메서드
    public static DefaultSubscriber create(String name) {
        DefaultSubscriber subscriber =  new DefaultSubscriber();
        subscriber.profile = new Profile();
        subscriber.getProfile().setName(name);

        return subscriber;
    }

    // profile getter
    public Profile getProfile() {
        return this.profile;
    }

    @Override
    public void alert(String message) {
        System.out.println("alarm to " + getProfile().getName() + " : " + message);
    }
}

 

*) client

public class Main {
    public static void main(String[] args) {
        DefaultStreamer damir = DefaultStreamer.create("Damir");

        DefaultSubscriber alfredo = DefaultSubscriber.create("Alfredo");
        DefaultSubscriber ana = DefaultSubscriber.create("Ana");

        damir.registerSubscriber(alfredo);
        damir.registerSubscriber(ana);

        damir.publish("first post", "first content of Damir");
        damir.publish("second post", "second content of Damir");
        System.out.println(damir.getAllPosts());
    }
}

//alarm to Alfredo : Damir posted new content: first post
//alarm to Ana : Damir posted new content: first post
//alarm to Alfredo : Damir posted new content: second post
//alarm to Ana : Damir posted new content: second post

 

*) 추가적인 개선점?

- 스트리머(subject)는 다른 스트리머의 구독자라면, 어떻게 변경되어야 할까요?

4. Pros vs Cons

*) Pros

- 실시간으로 주체의 변경사항을 다른 객체에 전파할 수 있습니다

- 런타임에서 구독 추가/삭제가 가능합니다

- 주체와 옵저버 간의 결합이 느슨합니다

 

*) Cons

- 옵저버 목록 관리가 힘들어질 수 있습니다

- 속도 이슈

 

5. Appendix

- pull 방식도 존재합니다

- java.util.Observable 클래스의 서브 클래스를 생성해서 구현할 수도 있습니다