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

반응형

1. 옵저버 패턴 정의

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

옵저버 패턴을 구현하는 방법에는 여러가지가 있지만 대부분 주제 (subject) 인터페이스와 옵저버 (observer) 인터페이스가 있는 클래스 디자인을 바탕으로 한다.

 

일대다(1:n) 관계는 주제와 옵저버에 의해 정의된다.

옵저버는 주제에 의존하며, 주제의 상태가 바뀌면 의존하는 옵저버에게 연락이 간다.

 

subject : 상태를 저장하고 있는 주제 인터페이스를 구현한 하나의 주제객체

observer : 주제객체에 의존하고 있는 옵저버 인터페이스를 구현한 여러개의 옵저버객체

 

데이터의 변경이 발생했을 때, 상대 클래스나 객체에 의존하지 않으면서 데이터 변경을 통보할 때 유용하다.

 

 

2. 옵저버 패턴 특징

행위 (Behavioral) 패턴

 

※ 데이터 전달 방식

Push 방식 : 주제객체에서 옵저버로 데이터를 보내는 방식

Pull 방식 : 옵저버에서 주제객체의 데이터를 가져가는 방식

 

옵저버 패턴은 주제와 옵저버가 느슨하게 결합되어 있는 객체 디자인을 제공한다.

 

■ 느슨한 결합 (Loose Coupling)

📌 디자인 원칙
느슨한 결합 (Loose Coupling)
서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.

주제는 옵저버가 특정 인터페이스 (옵저버 인터페이스) 를 구현한다는 것 외에는 모른다.

주제는 옵저버 인터페이스를 구현하는 객체 목록에만 의존하기 때문에 옵저버는 언제든지 추가, 변경, 삭제할 수 있다.

새 옵저버를 추가하려 할 때 주제를 변경할 필요가 없다.

주제나 옵저버를 변경해도 서로에게 영향을 주지 않는다. 그래서 주제와 옵저버는 서로 독립적으로 재사용 할 수 있다.

 

느슨하게 결합하는 디자인을 사용하면 변경사항이 생겨도 무난히 처리할 수 있는 유연한 객체지향 시스템을 구축할 수 있다.

 

3. 기본 예제

1. 주제 인터페이스

public interface Subject {
    void registerObserver(Observer observer);    // 옵저버 등록
    void removeObserver(Observer observer);    // 옵저버 삭제
    void notifyObserver();    // 상태변경이 있을때 알림
}

2. 옵저버 인터페이스

public interface Observer {
    void update(int value);
}

3. 주제 인터페이스 구현

public class ConcreteSubject implements Subject {

    private List<Observer> observerList;    // 옵저버들
    private int value = 0;

    public ConcreteSubject() {
        this.observerList = new ArrayList<>();
    }

    @Override
    public void registerObserver(Observer observer) {
        observerList.add(observer);    // 등록
    }

    @Override
    public void removeObserver(Observer observer) {
        observerList.remove(observer);    // 삭제
    }

    @Override
    public void notifyObserver() {
        for (Observer o : observerList) {
            o.update(value);    // 등록되어있는 모든 옵저버들에게 값 전달 
        }
    }

    public void setValue(int value) {    // 값이 변경이 있을때
        this.value = value;
        notifyObserver();    // 값 전달하는 메소드 호출
    }
}

4. 옵저버 인터페이스 구현

public class ConcreteObserver implements Observer {
    private int value;
    private Subject subject;

    public ConcreteObserver(Subject subject) {
        this.subject = subject;
        subject.registerObserver(this);    // 옵저버 생성할때 주제에 등록
    }

    @Override
    public void update(int value) {    // 값 갱신되고
        this.value = value;
        display();    // 갱신 표시
    }

    public void display() {
        System.out.println("Value = " + value);
    }

    // 테스트용 getter
    public int getValue() {
        return value;
    }
}

5. 확인

@Test
void basic() {
    ConcreteSubject concreteSubject = new ConcreteSubject();    // 주제생성
    ConcreteObserver concreteObserver = new ConcreteObserver(concreteSubject);    // 옵저버 생성 및 주제등록

    concreteSubject.setValue(50);
    concreteObserver.display();
    Assertions.assertThat(concreteObserver.getValue()).isEqualTo(50);    // OK

    concreteSubject.setValue(100);
    concreteObserver.display();
    Assertions.assertThat(concreteObserver.getValue()).isEqualTo(100);    // OK

    // 제거하면 값이 변경되지 않는다.
    concreteSubject.removeObserver(concreteObserver);

    concreteSubject.setValue(33333);
    concreteObserver.display();
    Assertions.assertThat(concreteObserver.getValue() != 33333).isTrue();    // OK
    Assertions.assertThat(concreteObserver.getValue()).isEqualTo(100);    // OK
}

6. display() 출력확인

Value = 50
Value = 100
Value = 100    // 제거하면 값이 변경되지 않는다.

4. 예제 : 날씨데이터 (Push 방식)

날씨 데이터를 가지고 있는 한 회사와 데이터를 연동하여 여러 종류의 디스플레이에 각각의 정보를 출력해줘야하는 업무가 생겼다. 온도, 습도, 기압 정보를 출력한다.

간단히 구현해보자.

public class WeatherData{
    // 인스턴스 변수들

    // measurementsChanged() : 기상 관측값이 갱신될 때마다 알려주기위한 메소드
    public void measurementsChanged(){ 
        float temp = getTemperature();    // 온도
        float humidity = getHumidity();    // 습도
        float pressure = getPressure();    // 기압

        // 디스플레이 갱신
        currentCondirionsDisplay.update(temp, humidity, pressure); 
        statisticsDisplay.update(temp, humidity, pressure);
        forecastDisplay.update(temp, humidity, pressure);
    }
    
    // 기타 메소드들
}

이렇게 구현하면 문제가 있다.

update() 메소드를 보면 구체적인 구현에 맞춰서 코딩이 되어 있다. 이러한 경우 프로그램을 고치지 않고서는 다른 디스플레이 항목을 추가 및 제거 할 수 없다. 향후에 바뀔 수 있는 부분은 캡슐화해서 분리해야 하므로, 효과적으로 모든 디스플레이들에게 Weather 의 상태정보를 알려줄 수 있는 방법이 필요하다.

 

해결해보자

1. 기상 스테이션 - 인터페이스

// 1. 주제 인터페이스
public interface Subject {
    void registerobserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers();
}

// 2. 옵저버 인터페이스
public interface Observer {
    void update(float temp, float humidity, float pressure);
}

// 3. 디스플레이 인터페이스
public interface DisplayElement {
    void display();
}

2. subject 인터페이스를 구현

public class WeatherData implements Subject {
    private List<Observer> observers;    // 옵저버들
    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherData() {
        observers = new ArrayList<>();
    }
   
    public void registerObserver(Observer o) {
        observers.add(o);    // 옵저버 등록
    }

    public void removeObserver(Observer o) {
        observers.remove(o);    // 옵저버 삭제
    }

    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();
    }
    // getter
    public float getTemperature() { return temperature; }
    public float getHumidity() { return humidity; }
    public float getPressure() { return pressure; }
}

3. Observer, DisplayElement 인터페이스 구현

// 현재 날씨
public class CurrentConditionsDisplay implements Observer, DisplayElement {
    private float temperature;
    private float humidity;
    private WeatherData weatherData;

    public CurrentConditionsDisplay(WeatherData weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this); // 옵저버 등록
    }

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

    @Override
    public void display() {
        System.out.println("현재 날씨 = 기온 : " + temperature + "도, 습도 : " + humidity + "%");
    }
}
// 예보
public class ForecastDisplay implements Observer, DisplayElement {
    private float currentTemperature = 26.56f;
    private float lastTemperature;
    private WeatherData weatherData;

    public ForecastDisplay(WeatherData weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this); // 옵저버 등록
    }
    public void update(float temp, float humidity, float pressure) {
        lastTemperature = currentTemperature;
        currentTemperature = temp;
        display();
    }

    public void display() {
        System.out.print("기상예보 = ");
        if (currentTemperature > lastTemperature) {
            System.out.println("어제보다 기온이 높아 따뜻합니다.");
        } else if (currentTemperature == lastTemperature) {
            System.out.println("어제와 비슷한 기온의 날씨입니다.");
        } else if (currentTemperature < lastTemperature) {
            System.out.println("기온이 내려가 어제보다 쌀쌀합니다.");
        }
    }
}
// 측정
public class StatisticsDisplay implements Observer, DisplayElement {
    private float maxTemp = 0.0f;
    private float minTemp = 10;
    private float tempSum= 0.0f;
    private int numReadings;
    private WeatherData weatherData;

    public StatisticsDisplay(WeatherData weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this); // 옵저버 등록
    }

    public void update(float temp, float humidity, float pressure) {
        tempSum += temp;
        numReadings++;
        if (temp > maxTemp) { maxTemp = temp; }
        if (temp < minTemp) { minTemp = temp; }
        display();
    }

    public void display() {
        System.out.println("평균/최고/최저 온도 = " + (tempSum / numReadings)
                + "/" + maxTemp + "/" + minTemp);
    }
}

4. 실행

@Test
void execute() {
    WeatherData weatherData = new WeatherData();
    // 옵저버 등록
    CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
    StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
    ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);

    weatherData.setMeasurements(20, 65, 30.4f);
    weatherData.setMeasurements(15, 70, 29.2f);
    weatherData.setMeasurements(32, 80, 27.2f);
    weatherData.setMeasurements(32, 90, 29.2f);
	// 예보 디스플레이만 삭제
    weatherData.removeObserver(forecastDisplay);
    weatherData.setMeasurements(27, 78, 31.1f);
}

5. 출력확인

현재 날씨 = 기온 : 20.0도, 습도 : 65.0%
평균/최고/최저 온도 = 20.0/20.0/10.0
기상예보 = 기온이 내려가 어제보다 쌀쌀합니다.

현재 날씨 = 기온 : 15.0도, 습도 : 70.0%
평균/최고/최저 온도 = 17.5/20.0/10.0
기상예보 = 기온이 내려가 어제보다 쌀쌀합니다.

현재 날씨 = 기온 : 32.0도, 습도 : 80.0%
평균/최고/최저 온도 = 22.333334/32.0/10.0
기상예보 = 어제보다 기온이 높아 따뜻합니다.

현재 날씨 = 기온 : 32.0도, 습도 : 90.0%
평균/최고/최저 온도 = 24.75/32.0/10.0
기상예보 = 어제와 비슷한 기온의 날씨입니다.

현재 날씨 = 기온 : 27.0도, 습도 : 78.0%
평균/최고/최저 온도 = 25.2/32.0/10.0

마지막 실행 결과에 예보 옵저버를 삭제시켰기 때문에 기상예보가 표시되지 않는 것을 확인할 수 있다.

 

■ UML Class Diagram

 

반응형