[디자인] Observer 패턴

Kang In Sung·2021년 3월 14일
0

Design

목록 보기
1/2
post-thumbnail

서론

Observer 패턴은 마치 각 국가 별 특파원과 같은 역할을 한다.

예를 들어 미국에 있는 소식이 궁금하면, 한국에 있는 기자가 직접 미국에 가지 않더라도 특파원에게 미국 소식을 전파 받을 수 있다.

또한 영국에 있는 소식이 궁금하면, 마찬가지로 영국에 있는 특파원한테 영국 소식을 전파 받으면 된다.

그리고 영국이나 미국에 있는 특파원들도 한국에 특별한 일이 없으면 가지 않더라도 그 곳에서 한국 소식을 받을 수 있다.

그러나 이란이나 아프가니스탄 등 여행 위험 지역에 있는 경우엔 특파원을 따로 두게 되면 위험하기 때문에 특수 안전 교육을 받은 취재진이 파견되는 수 밖에 없다.

위와 마찬가지로, 하나의 주제 (Subject) 가 바뀌면 여러 객체들 (Observer) 이 참고하는 값에 대해 언제든지 갱신되어야 한다.

이러한 역할을 하는 패턴이 바로 Observer 패턴이다.

정의

Observer

  • Subject 는 실시간으로 바뀌는 주제의 역할을 한다.
  • Observer 는 관찰자의 역할을 한다.
  • Subject 상태가 바뀌면 이에 의존하는 개체들한테 연락을 취해 내용이 갱신된다.
  • Subject 객체는 Observer 객체들을 등록 및 삭제를 할 수 있다.

특징

  • 의존성은 대개 1:N (일대다) 방식으로 정의된다.
    • 즉, Observer 객체가 여러 개 존재하는 의미.
  • 느슨한 결합 (Loose Coupling)
    • Subject, Observer 객체의 상호작용은 보이지만, 서로에 대한 사실은 자세히 알지 못하는 관계이다.
    • 예를 들어 미국 특파원은 한국 소식에 대해 특별하지 않은 건은 굳이 궁금해 하지 않을 것이다.
    • 또한 한국 기자들도 미국 대선 등 특별한 일이 아닌 경우에는 굳이 궁금해 하지 않을 것이다.
    • 이처럼 변경 사항이 생기는 경우에도 다른 객체들과의 의존성을 줄여서 용이하게 처리할 수 있다.
  • 되도록이면 원시 데이터를 사용하는 것이 좋겠다.
    • Map 과 같이 큰 데이터들에 대해서 상태값을 수정하는데 메모리 비용이 더 들 수도 있다.

예제

public interface Subject {
    void registerObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers();
}

Subject.java

public class NewsTopic implements Subject {
    private List<Observer> observers;
    private Map<String, String> news;

    public NewsTopic() {
        this.observers = new LinkedList<>();
        this.news = new HashMap<>();
    }

    @Override
    public void registerObserver(Observer observer) {
        if (!this.observers.contains(observer)) {
            this.observers.add(observer);
            this.news.put(observer.getKey(), "-- 공백 --");
        }
    }

    @Override
    public void removeObserver(Observer observer) {
        if (this.observers.contains(observer)) {
            this.observers.remove(observer);
            this.news.remove(observer.getKey());
        }
    }

    @Override
    public void notifyObservers() {
        this.observers.forEach(o -> o.update(this.getNews()));
    }

    public void setNews(Observer observer, String news) {
        this.news.put(observer.getKey(), news);
        this.notifyObservers();
    }

    public String getNews() {
        return this.news
                .keySet()
                .stream()
                .map(key -> String.format("[%s] %s\n", key, this.news.get(key)))
                .collect(Collectors.joining());
    }
}

NewsTopic.java

public interface Observer {
    void update(String news);
    String getKey();
}

Observer.java

public class UKReporter implements Observer {
    private Subject subject;
    private String key;
    private String news;

    public UKReporter(Subject subject, String key) {
        this.subject = subject;
        this.key = key;
        this.news = "";
        subject.registerObserver(this);
    }

    public void fired() {
        this.subject.removeObserver(this);
    }

    public void subscribe() {
        this.subject.registerObserver(this);
    }

    @Override
    public void update(String news) {
        this.news = news;
        System.out.printf("-- <<%s Topic Briefing>> --\n%s\n", this.key, this.news);
    }

    @Override
    public String getKey() {
        return this.key;
    }

UKReporter.java

public class USAReporter implements Observer {
    private Subject subject;
    private String key;
    private String news;

    public USAReporter(Subject subject, String key) {
        this.subject = subject;
        this.key = key;
        this.news = "";
        subject.registerObserver(this);
    }

    public void fired() {
        this.subject.removeObserver(this);
    }

    public void subscribe() {
        this.subject.registerObserver(this);
    }

    @Override
    public void update(String news) {
        this.news = news;
        System.out.printf("-- <%s TODAY NEWS> --\n%s\n", this.key, this.news);
    }

    @Override
    public String getKey() {
        return this.key;
    }
}

USAReporter.java

public class Main {
    public static void main(String[] args) {
        NewsTopic news = new NewsTopic();

        System.out.println("-- LA 타임즈 뉴스 보도 시작 --");
        USAReporter usaReporter = new USAReporter(news, "The L.A. Times");
        news.setNews(usaReporter, "LA 다저스 팀 인원 일부 교체될 것");

        System.out.println("-- USA 타임즈 뉴스 보도 시작 --");
        USAReporter usaReporter2 = new USAReporter(news, "The USA Times");
        news.setNews(usaReporter2, "올해 미국은 조 바이든이 이끄는 걸로");

        System.out.println("-- 영국 타임즈 뉴스 보도 시작 --");
        UKReporter ukReporter = new UKReporter(news, "The United Kingdom Times");
        news.setNews(ukReporter, "영국 여왕 올해 내한 예정을 밝혀");

        System.out.println("-- 멘체스터 타임즈 뉴스 보도 시작 --");
        UKReporter ukReporter2 = new UKReporter(news, "The Manchester Times");
        news.setNews(ukReporter2, "올해 멘체스터 경기장 신축될 것");

        System.out.println("-- 영국 타임즈 보도 내용 변경 --");
        news.setNews(ukReporter, "영국 여왕 올해는 메로나 때문에 내한 힘들 것");

        System.out.println("-- USA 타임즈 보도 종료 --");
        usaReporter2.fired();
        news.notifyObservers();
    }
}

Main.java

-- LA 타임즈 뉴스 보도 시작 --
-- <The L.A. Times TODAY NEWS> --
[The L.A. Times] LA 다저스 팀 인원 일부 교체될 것

-- USA 타임즈 뉴스 보도 시작 --
-- <The L.A. Times TODAY NEWS> --
[The USA Times] 올해 미국은 조 바이든이 이끄는 걸로
[The L.A. Times] LA 다저스 팀 인원 일부 교체될 것

-- <The USA Times TODAY NEWS> --
[The USA Times] 올해 미국은 조 바이든이 이끄는 걸로
[The L.A. Times] LA 다저스 팀 인원 일부 교체될 것

-- 영국 타임즈 뉴스 보도 시작 --
-- <The L.A. Times TODAY NEWS> --
[The USA Times] 올해 미국은 조 바이든이 이끄는 걸로
[The L.A. Times] LA 다저스 팀 인원 일부 교체될 것
[The United Kingdom Times] 영국 여왕 올해 내한 예정을 밝혀

-- <The USA Times TODAY NEWS> --
[The USA Times] 올해 미국은 조 바이든이 이끄는 걸로
[The L.A. Times] LA 다저스 팀 인원 일부 교체될 것
[The United Kingdom Times] 영국 여왕 올해 내한 예정을 밝혀

... (중략) ...

-- 영국 타임즈 보도 내용 변경 --
-- <The L.A. Times TODAY NEWS> --
[The USA Times] 올해 미국은 조 바이든이 이끄는 걸로
[The L.A. Times] LA 다저스 팀 인원 일부 교체될 것
[The United Kingdom Times] 영국 여왕 올해는 메로나 때문에 내한 힘들 것
[The Manchester Times] 올해 멘체스터 경기장 신축될 것

-- <The USA Times TODAY NEWS> --
[The USA Times] 올해 미국은 조 바이든이 이끄는 걸로
[The L.A. Times] LA 다저스 팀 인원 일부 교체될 것
[The United Kingdom Times] 영국 여왕 올해는 메로나 때문에 내한 힘들 것
[The Manchester Times] 올해 멘체스터 경기장 신축될 것

-- <<The United Kingdom Times Topic Briefing>> --
[The USA Times] 올해 미국은 조 바이든이 이끄는 걸로
[The L.A. Times] LA 다저스 팀 인원 일부 교체될 것
[The United Kingdom Times] 영국 여왕 올해는 메로나 때문에 내한 힘들 것
[The Manchester Times] 올해 멘체스터 경기장 신축될 것

-- <<The Manchester Times Topic Briefing>> --
[The USA Times] 올해 미국은 조 바이든이 이끄는 걸로
[The L.A. Times] LA 다저스 팀 인원 일부 교체될 것
[The United Kingdom Times] 영국 여왕 올해는 메로나 때문에 내한 힘들 것
[The Manchester Times] 올해 멘체스터 경기장 신축될 것

-- USA 타임즈 보도 종료 --
-- <The L.A. Times TODAY NEWS> --
[The L.A. Times] LA 다저스 팀 인원 일부 교체될 것
[The United Kingdom Times] 영국 여왕 올해는 메로나 때문에 내한 힘들 것
[The Manchester Times] 올해 멘체스터 경기장 신축될 것

-- <<The United Kingdom Times Topic Briefing>> --
[The L.A. Times] LA 다저스 팀 인원 일부 교체될 것
[The United Kingdom Times] 영국 여왕 올해는 메로나 때문에 내한 힘들 것
[The Manchester Times] 올해 멘체스터 경기장 신축될 것

-- <<The Manchester Times Topic Briefing>> --
[The L.A. Times] LA 다저스 팀 인원 일부 교체될 것
[The United Kingdom Times] 영국 여왕 올해는 메로나 때문에 내한 힘들 것
[The Manchester Times] 올해 멘체스터 경기장 신축될 것

실행 결과

부가 설명 및 문제점

  • 각 미국, 영국 보도자 객체 별로 subscribe(), fired() 메소드를 추가했다.
    • 이를 조금 수정하여 Observer 객체도 Subject 객체를 버리게끔 할 수 있다.
    • 원칙을 벗어나는 방법으로 Observer 객체가 Subject 객체의 구독을 취소할 수 있다.
      (즉, 절이 싫으면 중이 떠날 수도 있다는 의미.)
  • public 접근자 setNews(), getNews() 메소드를 사용하면 Observer 관점에서 데이터 은닉을 어길 수 밖에 없다.
  • 상태 값이 계속 바뀌다 보면 주기적으로 notifyObserver() 를 실행해야 하기 때문에 수행 비용의 낭비가 초래된다.
  • Observer 는 구독 순서에 영향을 받는다. 이는 느슨한 결합을 깨는 원인이기도 하다.
  • Observer 의 궁극적인 목표는 Subject 객체 측에서 보내는 Push 방식보다 Observer 측에서 주기적으로 받는 Pull 방식을 더 선호한다.
  • Thread Safe 하지 않은 단점이 있다. 이를 해결하려면 Concurrent 기반 자료구조를 사용해야 한다.

Java 의 Observer 인터페이스

Java 9 이전 버전을 사용하면 Observable 클래스, Observer 인터페이스를 제공한다.

하지만 상속 등 번거로운 작업들이 많아지기 때문에 Deprecated 가 된 듯 하다.

더욱 자세한 Deprecated 원인은 다음과 같다.

  • 이론적으로 Observable 은 인터페이스로 되어 있어야 한다.
    • 다른 클래스가 상속될 때, 이를 상속하지 못 해 사용되지 못 하는 현상이 일어난다.
  • 상속 위주의 작업들만 가득하다.
    • Observable 을 제대로 쓰려면 오버라이딩 작업을 많이 해야 할 것이다.
    • Observable 내부에 있는 멤버 변수 및 메소드들을 짜집기 하는 것도 어렵다.
    • 상속을 위한 구조가 아닌 구성을 따르는 구조로 되어 있다.

아래는 Java SE 9 에서 언급한 Deprecated 원인 내용이다.

  • Observer, Observable 에 제공되는 이벤트 모델이 제약적이다.
  • notify 의 순서를 보장할 수 없다. 게다가 상태 변화도 1:1 로 이뤄지지 않는다.
  • 멀티 쓰레드를 사용할거면 java.util.concurrent 패키지의 자료구조를 사용하는 것이 좋다.

활용

  • 차후에 배울 MVC 모델에서 주로 사용되는 패턴이기도 하다.
  • Java Swing 에서 버튼, 체크박스 등 클릭하는 요소들에 대해 이벤트를 감지할 때 사용된다.
  • 클라이언트와 서버 모델이서 서로의 상태를 확인하는 방식이 옵저버 패턴과 유사하다.
  • RxJava 등에서 Observer 자료구조가 많이 사용된다.
profile
Back-End Developer

0개의 댓글