코딩하는 털보

디자인 패턴 2. 옵저버 패턴 본문

Book/헤드 퍼스트 디자인 패턴

디자인 패턴 2. 옵저버 패턴

이정인 2021. 8. 25. 17:40

디자인 패턴 2. 옵저버 패턴

옵저버 패턴을 정기 신문 구독으로 예를 들어 설명해줬다.
신문출판사 : 주제(subject) 객체, 구독자 : 옵저버
주제 객체에서 일부 데이터를 관리하며 데이터가 달라지면 옵저버에 새로운 데이터 값이 전달된다.
옵저버 객체들은 주제를 구독하고(주제 객체에 등록되어)있으며 주제의 데이터가 바뀌면 갱신 내용을 전달받는다.

옵저버 패턴

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

  • 일대다 관계
    옵저버 패턴에서는 일련의 객체들(주제와 옵저버) 사이에서 일대다 관계를 정의한다.

옵저버 패턴은 대부분 주제 인터페이스와 옵저버 인터페이스가 들어있는 클래스 디자인을 바탕으로 구현된다.
주제에서는 옵저버를 등록 및 해지하고 각 옵저버에 상태 변경을 알리기 위한 메서드를 포함하고 있다.

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

옵저버에는 주제의 상태가 바뀔 때 호출되는 메서드가 있다.

public interface Observer {
    public void update();
}

느슨한 결합
옵저버 패턴에서는 주제와 옵저버가 느슨하게 결합되어 있는 객체 디자인을 제공한다.
느슨하게 결합되어 있다는 것은 서로 상호작용을 하긴 하지만 서로에 대해 잘 모른다는 것을 의미한다.

디자인원칙, 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.
느슨한 결합의 장점은 변경 사항이 있더라도 서로에게 영향을 미치지 않는(객체 사이의 상호의존성 최소화) 유연한 객체지향 시스템을 구축할 수 있다는 것이다.


아직 옵저버 패턴을 적용하지 않은 코드
WeatherData는 각 디스플레이 항목에 데이터를 전달해야 한다.
구체적인 구현에 맞춰서 코딩했고 바뀔 수 있는 부분이 캡슐화 되지 않았다.

import lombok.Getter;

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

    //...
}

WeatherData를 주제로, 각 디스플레이 항목들을 옵저버로 생각하고 각각의 인터페이스를 구현하면서 옵저버 패턴을 적용한 코드

public class WeatherData implements Subject{
    //옵저버와의 관계가 상속이 아닌 구성에 의해 이루어진다.
    private ArrayList<Observer> observers;
    private float temperature;
    private float humidity;
    private float pressure;

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

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

    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    //바뀔 수 있는 부분이 옵저버로 캡슐화되었다.
    //구체적인 구현대신 추상화를 사용한다.
    @Override
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(temperature, humidity, pressure);
        }
    }

    public void measurementsChanged() {
        notifyObservers();
    }

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

    // 기타 WeatherData 메서드
}
public class CurrentConditionsDisplay implements Observer, DisplayElement {
    private Subject weatherData;
    private float temperature;
    private float humidity;

    public CurrentConditionsDisplay(Subject weatherData) {
        this.weatherData = weatherData;
        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("conditions: temp "+temperature+",humidity "+humidity+"%");
    }
}

느슨한 결합
각 디스플레이의 변경에 WeatherData는 영향받지 않는다.
추가로 새로운 디스플레이를 개발하더라도 기존 코드는 변경 될 필요가 없다. 그냥 옵저버 인터페이스만 구현하면 그만이다.


자바 내장 옵저버 패턴 사용하기

자바가 지원하는 옵저버 패턴 API 중 가장 일반적인 java.util.Observer 인터페이스와 java.util.Observable 클래스를 사용할 수 있다.
위에서 직접 만든 주제,옵저버 인터페이스와 비슷하지만 가장 큰 차이점은 주제 객체의 동작을 구현하는게 아닌 Observable 클래스로부터 상속받는다는 것이다.

java.util.Observable의 메서드
addObserver() : 옵저버 등록
deleteObserver() : 옵저버 탈퇴
setChanged() : 주제 객체의 상태가 바뀌었다는 것을 알림, 이 메서드가 호출되지 않은 상태에서는 옵저버에게 연락할 수 없다.
notifyObservers() 또는 notifyObserver(Object arg) : arg 파라미터에는 전달할 임의의 데이터 객체를 넣을 수 있다(푸시 방식).

java.util.Observer의 추상메서드
update(Observable o, Object arg) : 옵저버 객체에서 구현해야 한다. 이 메서드를 통해 notifyObserver 메서드의 아규먼츠 데이터를 전달받는다.
전달받은 Observable 객체로부터 원하는 데이터를 가져갈 수도있다.(풀 방식)

//옵저버를 등록 및 제거하고 변경을 전달하는 메서드를 구현하지 않아도 된다.
//풀 방식을 사용하기 위해서 getter를 사용한다.
@Getter
public class WeatherData extends Observable {
    private float temperature;
    private float humidity;
    private float pressure;

    public void measurementsChanged() {
        setChanged();
        //notifyObservers 메서드에 옵저버로 전달하는 데이터 객체가 없으므로 풀 방식이다.
        notifyObservers();
    }

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

    // 기타 WeatherData 메서드
}
public class CurrentConditionsDisplay implements Observer, DisplayElement {
    private Observable weatherData;
    private float temperature;
    private float humidity;

    public CurrentConditionsDisplay(Observable weatherData) {
        this.weatherData = weatherData;
        weatherData.addObserver(this);
    }

    @Override
    public void update(Observable o, Object arg) {
        if ( weatherData instanceof WeatherData ) {
            WeatherData weatherData = (WeatherData) o;
            //주제 객체의 getter를 사용하여 풀 방식으로 데이터를 가져온다.
            temperature = weatherData.getTemperature();
            humidity = weatherData.getHumidity();
            display();
        }
    }

    @Override
    public void display() {
        System.out.println("conditions: temp "+temperature+",humidity "+humidity+"%");
    }
}

java.util.Observable의 단점

  • Observable이 클래스이다.
    다른 클래스를 상속받는 클래스는 Observable를 상속받을 수 없다. (재사용성이 제한적이다.)
    또한 Observable 관련 인터페이스가 없기 때문에 직접 구현할 수 도 없다.
  • Observable의 핵심 메서드를 외부에서 호출할 수 없다.
    setChanged() 및 여러 핵심 메서드가 protected로 선언되어 있기 때문에 Observable를 상속받는 서브클래스에서만 호출할 수 있다.
    디자인 원칙을 토대로 상속 대신에 구성을 사용한다면 이 메서드들을 사용할 수 없게 된다.

java.util.Observable 및 java.util.Observer는 자바 9부터 deprecated 되었다.
그에 대한 문서 내용은 아래와 같다.

Observer와 Observable이 지원하는 이벤트 모델은 제한적입니다.
Observable이 전달하는 알림의 순서는 정할 수 없습니다.
상태 변경과 알림이 일대일로 대응하지 않습니다.

보다 풍부한 이벤트 모델은 java.beans 패키지 확인.
thread-safety 및 순서있는 알림은 java.util.concurrent 패키지의 동시성 데이터 구조 확인.
reactive streams style 프로그래밍의 경우 Flow API.
Comments