옵저버 패턴

이주오·2022년 3월 6일
1

디자인 패턴

목록 보기
5/12

옵저버 패턴

헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.

옵저버 패턴에서는 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.


개요

실제 기상 정보를 수집하는 장비인 기상 스테이션과 기상 스테이션으로부터 오는 데이터를 추적하는 객체인 WeatherData, 그리고 사용자에게 현재 기상 조건을 보여주는 디스플레이, 세 요소로 이루어진다.

WeatherData 객체에서는 기상 스테이션으로부터 데이터를 가져올 수 있다. 데이터를 가져온 후에는 디스플레이 장비에 세 가지 항목을 표시할 수 있다.

  • 현조 조건(온도, 습도, 압력)
  • 기상 통계
  • 기상 예보

WeatherData 객체를 사용하여 디스플레이 장비에서 위의 3가지 요소를 갱신해 가면서 보여주는 애플리케이션을 만들어보자


주어진 WeatherData 클래스와 상황

  • 세가지의 게터 메소드는 각각 가장 최근에 측정된 온도, 습도, 기압 값을 리턴하는 메소드
  • measurementsChanged 메소드를 현재 조건, 기상 통꼐, 기상 예측 3가지 디스플레이를 갱신할 수 있도록 구현해야 한다.
  • 시스템은 확장 가능해야 한다. 추후 디스플레이 항목들은 추가/제거될 수 있다.

초기 구현

public class WeatherData {

    // 인스턴스 변수 선언

    public void measurementsChanged() {
        float temp = getTemperature();
        float humidity = getHumidity();
        float pressure = getPressure();

        currentConditionsDisplay.update(temp, humidity, pressure);
        statisticsDisplay.update(temp, humidity, pressure);
        forecastDisplay.update(temp, humidity, pressure);

    }

}

위의 코드의 문제는 무엇일까??

  • 인터페이스가 아닌 구체적인 구현을 바탕으로 코딩하고 있다.
  • 새로운 디스플레이 항목이 추가될 때마다 코드를 변경해야 한다.
  • 실행중에 디스플레이 항목을 추가/제거할 수 없다.
  • 바뀌는 부분을 캡슐화하지 않았다.

구체적인 구현에 맞춰서 코딩되어 있기 때문에 코드를 고치지 않고는 다른 디스플레이 항목을 추가하거나 제거할 수 없고 디스플레이에 항목들을 갱신하여 업데이트하는 부분들은 바뀔 수 있는 부분이므로 캡슐화해야 한다.

이제 옵저버 패턴에 대해서 알아보자


옵저버 패턴

쉽게 생각해서 신문 구독 메커니즘과 비슷한다. 즉 출판사 + 구독자 = 옵저버 패턴 인 것

출판사를 주제 or 주체(subject), 구독자를 옵저버(observer)라고 부른다.

  • subject 객체에서 일부 데이터를 관리
  • subejct의 데이터가 달라지면 옵저버한테 소식과 데이터가 전달된다.
  • observer 객체들은 subject 객체를 구독(등록)하고 있으며 subject의 데이터가 바뀌면 갱신 내용을 전달받는다.

옵저버 패턴에서는 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.

  • 일대다 관계는 subject와 observer에 의해 정의되고 observer는 subject에 의존한다.
  • 옵저버 패턴을 구현하는 방법에는 여러가지가 있지만 대부분 subject 인터페이스와 observer 인터페이스를 사용한 클래스 디자인을 바탕으로 한다.

Subject 인터페이스

  • 객체에서 옵저버로 등록하거나 옵저버 목록에서 탈퇴하고 싶을 때 이 인터페이스에 있는 메소드를 사용한다.

ConcreteSubject

  • Subject 역할을 하는 Concrete 클래스에서는 항상 subject 인터페이스를 구현해야 한다.
  • subject 클래스에서는 등록 및 해지를 위한 메소드 외에 상태가 바뀔때마다 모든 옵저버들에게 연락을 하기 위한 notifyObservers() 메소드도 구현해야 한다.
  • subject 클래스에는 상태를 설정하고 알아내기 위한 세터/게터 메소드가 있을 수도 있다.

Observer 인터페이스

  • observer가 될 가능성이 있는 객체에서는 반드시 observer 인터페이스를 구현해야 한다.
  • observer 인터페이스에는 subject의 상태가 바뀌었을 때 호출되는 update() 메소드 밖에 없다.

ConcreteObserver

  • Observer 인터페이스만 구현한다면 어떤 클래스든 옵저버 클래스가 될 수 있다.
  • 각 옵저버는 특정 주제 객체에 등록을 해서 연락을 받을 수 있다.

옵저버 패턴에서 상태를 저장하고 지배하는 것은 subject 객체이다. 따라서 상태가 들어있는 객체는 하나만 존재하고, 옵저버는 반드시 상태를 가지고 있어야 하는 것은 아니기 때문에 옵저버는 여러 개가 있을 수 있으며 subject 객체에서 상태가 바뀌었다는 알려주기를 기다리는, subject에 의존적인 성질을 가진다.

따라서 하나의 subject와 여러개의 observer가 연관된 일대다 관계가 성립하고 해당 의존을 통해 여러 객체에서 동일한 데이터를 제어하도록 할 수 있다.


느슨한 결합

추가로 옵저버 패턴의 장점은 subject와 observer가 느슨하게 결합되어 있는 디자인을 제공하는 것이다. 두 객체가 느슨하게 결합되어 있다는 것은, 해당 객체들이 상호작용을 하지만 서로에 대해 잘 모른다는 것을 의미한다.

subject가 observer에 대해 아는 것은 해당 옵저버가 observer 인터페이스를 구현한다는 것 뿐이다

  • 옵저버의 구상 클래스, 어떤 행동을 하는지 등, 나머지는 알 필요가 없다.

observer는 언제든지 추가 가능하다

  • subject는 observer 인터페이스를 구현하는 객체 목록에먼 의존하기 때문에 실행중에 한 옵저버를 다른 옵저버로 바꿔도 되고, 언제든지 새로운 옵저버를 추가하거나 삭제할 수 있다.

새로운 형식의 observer를 추가하려고 할 때도 subject를 변경할 필요가 없다.

  • observer 인터페이스에 의조하기 때문에 새로운 옵저버 구상 클래스가 생겨도 문제 없다.

subject와 observer는 서로 독립적으로 재사용할 수 있다.

  • 다른 용도로 활용할 일이 있어도 문제 없다. 느슨하게 결합되어 있기 때문

subject와 observer에 변경이 생겨도 서로에게 영향이 미치지 않는다.

  • 마찬가지로 느슨한 결합 덕분

옵저버 패턴 적용

  • Subject, Observer 인터페이스 생성
  • WeatherData는 Subject 인터페이스를 구현하도록 수정
  • 모든 기상 구성요소에서 Observer 인터페이스를 구현하도록 수정
    • subject 객체에서 갱신된 데이터를 전달할 수 있도록 메소드 제공
  • 모든 디스플레이 항목에서 구현하는 DisplayElement 인터페이스를 하나 더 추가
    • 측정값을 바탕으로 각각 다른 내용을 표시하는 메소드인 display() 메소드 추가
public interface Subject {
    void registerObserver(Observer o);
    void removeObserver(Observer o);
    void notifyObserver();
}

public interface Observer {
    void update(float temperature, float humidity, float pressure);
}

public interface DisplayElement {
    void display();
}
import java.util.ArrayList;
import java.util.List;

public class WeatherData implements Subject {

    private List<Observer> observers;
    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherData() {
        this.observers = new ArrayList<>();
    }

    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }

    @Override
    public void removeObserver(Observer o) {
        int i = observers.indexOf(o);
        if(i >= 0) {
            observers.remove(i);
        }
    }

    @Override
    public void notifyObserver() {
        observers.forEach(observer -> observer.update(temperature, humidity, pressure));
    }

    public void measurementsChanged() {
        notifyObserver();
    }

    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }
}
public class CurrentConditionsDisplay implements Observer, DisplayElement {

    private float temperature;
    private float humidity;
    private Subject weatherData;

    public CurrentConditionsDisplay(Subject weatherData) {
        this.weatherData = weatherData;
        this.weatherData.registerObserver(this);
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        display();
    }

    @Override
    public void display() {
        System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity");
    }
}
public class WeatherStation {

    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();
        CurrentConditionsDisplay conditionsDisplay = new CurrentConditionsDisplay(weatherData);
        weatherData.setMeasurements(70, 60, 30.4f);
        weatherData.setMeasurements(55, 50, 15.4f);
        weatherData.setMeasurements(90, 70, 40.4f);
    }

}


데이터 전달 시 두가지 방식

현재는 subject의 상태가 변경될 때마다 observer에게 알려주고(push) 있다. 옵저버 입장에서는 필요한 상황에서만 주체의 상태를 가져오는 방식(pull)이 더 편할 수도 있지 않을까??

이처럼 옵저버 패턴은 PUSH 방식과 PULL 방식으로 구분할 수 있다.

PUSH 방식 : 주제의 내용이 변경될 때마다 구독자에게 알려주는 방식

PULL 방식 : 구독자가 필요할 때마다 주제에게 정보를 요청하는 방식

또한 자바에서 몇 가지 API를 통해 자체적으로 옵저버 패턴을 지원한다. 일반적으로 java.util 패키지에 들어있는 Observer 인터페이스와 Observable 클래스이다. 해당 내장 클래스들은 푸시 방식과 풀 방식 모두 가능하다.

그렇다면 자바 내장 클래스를 이용하여 풀 방식으로 수정해보자

Pull 방식으로 수정(with 자바 내장 옵저버 패턴)

  • Observable은 인터페이가 아니라 클래스이므로 WeatherData 클래스에서 해당 클래스를 상속하면서 메소드들을 상속받는다.
  • setChange() 제공
    • 해당 메소드는 상태가 바뀌었다는 것을 밝히기 위한 용도로 사용된다. setChange() 메소드가 호출되지 않은 상태에서 notifiyObservers()가 호출되면 옵저버들에게 연락이 가지 않는다. 해당 메소드를 조건에 따라서 적절히 호출하여 옵저버들에게 연락이 가는 것을 제어할 수 있다.
  • Observer 인터페이스는 앞에서 만든 클래스와 거의 똑같다.

바뀐 WeatherData

import java.util.ArrayList;
import java.util.List;
import java.util.Observable;
import observer.after.Observer;
import observer.after.Subject;

public class WeatherDataObservable extends Observable {

    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherDataObservable() {
    }

    public void measurementsChanged() {
        setChanged();
        notifyObservers(); // pull 방식, push 방식인 경우 notifyObservers(Object arg);
    }

    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }

    // pull 방식이므로 옵저버가 주체 객체의 상태를 알아야하므로 필요하다.
    public float getTemperature() {
        return temperature;
    }

    public float getHumidity() {
        return humidity;
    }

    public float getPressure() {
        return pressure;
    }
}
import java.util.Observable;
import java.util.Observer;
import observer.after.DisplayElement;
import observer.after2.WeatherDataObservable;

public class CurrentConditionsDisplayObserver implements Observer, DisplayElement {

    private float temperature;
    private float humidity;
    Observable observable;

    public CurrentConditionsDisplayObserver(Observable observable) {
        this.observable = observable;
        observable.addObserver(this);
    }

    @Override
    public void update(Observable o, Object arg) {
        if(o instanceof WeatherDataObservable) {
            WeatherDataObservable weatherData = (WeatherDataObservable) o;
            this.temperature = weatherData.getTemperature();
            this.humidity = weatherData.getHumidity();
            display();
        }
    }

    @Override
    public void display() {
        System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity");
    }
}

java.util.Observable의 단점

Observer와 Observable은 Java SE 9 버전부터 Deprecated 되었다. 그 이유는 무엇일까?

  • Observer와 Observable이 제공하는 이벤트 모델이 제한적이다.
  • Observable의 notify는 순서를 보장할 수 없으며, 상태 변경은 1:1로 일치하지 않는다.
  • 더 풍부한 이벤트 모델은 java.beans 패키지가 제공하고 있다.
  • 멀티 스레드에서의 신뢰할 수 있고 순서가 보장된 메시징은 java.util.concurrent 패키지의 concurrent 자료 구조들 중 하나를 골라 쓰는 편이 낫다.
  • reactive stream 스타일 프로그래밍은 Flow API를 쓰기를 권한다.

Observable의 문제는 헤드 퍼스트 디자인 패턴에서도 지적하고 있다

  • Observable이 interface가 아니라 class이다.
    • 다른 클래스를 상속하는 클래스가 Observable을 상속할 수 없다. 따라서 재사용성에 제약이 생긴다.
    • 내장된 Observer API하고 잘 맞는 클래스를 직접 구현하는 것이 불가능하다.
  • Obserable 클래스의 핵심 메소드를 외부에서 호출할 수 없다.
    • setChanged() 메소드가 protected으로 선언되어 있기 때문이다.
    • 상속보다 구성을 사용한다는 디자인 원칙을 위배한다.

마무리 핵심 정리

  • 옵저버 패턴에서는 객체들 사이에 일대다 관계를 정의한다.
  • 주체 객체는 동일한 인터페이스를 써서 옵저버에 연락한다.
  • 주체와 옵저버는 서로 느슨한 결합
  • 주체에서 데이터를 보내는 푸시방식과 옵저버가 데이터를 가져오는 풀방식이 있다.
    • 풀 방식 추천
  • 옵저버들한테 연락을 돌리는 순서에 절대로 의존하면 안 된다.
    • 만약 의존하도록 했다면 잘못된 것, 느슨한 결합이라고 볼 수 없다.
profile
동료들이 같이 일하고 싶어하는 백엔드 개발자가 되고자 합니다!

0개의 댓글