BackEnd/Design Patterens

[Design Patterns] Observer Pattern : 옵저버 패턴

샤아이인 2022. 1. 12.

 

Head First Design Patterns 책을 읽으며 정리한 내용 입니다. 문제가 될시 글을 내리도록 하겠습니다!

Observer Pattern 란?

Observer Pattern - 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고
자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의합니다.

옵저버 패턴에서는 일대다 관계로, 1개의 Subject 와 여러개의 Observer로 구성됩니다.

 

또한 Observer들은 Subject에 의존적 입니다. Subject의 상태가 바뀌면 변경사항을 옵저버 한태 통보해주고, 이 통보에 대하여 Observer는 대응할 수 있습니다. 값을 바꿀수도 있고, 삭제할수도 있습니다.

 

또한 Observer들은 언제든 그룹에서 추가/삭제 될 수 있습니다. 추가되면 Subject로부터 정보를 전달받게 될 것 이며, 그룹에서 삭제될 경우 더이상 Subject의 정보를 받을수 없게 됩니다.

 

이를 그림으로 보면 다음과 같습니다.

마지막으로 다이어그램까지는 보셔야겠죠?

 
출처 - 헤드퍼스트 디자인패턴

 

이말이 무슨 의미인지 다음 단락부터 예시를 통하여 설명해 보겠습니다!


패턴 소개

다음과 같은 WeatherData 라는 class가 있다고 가정해 봅시다.

WeatherData{
    getTemperature();
    getHumidity();
    getPressure();
    measurementsChanged();
}
 

getTemperature(), getHumidity(), getPressure() 는 단순한 getter 역할을 수행합니다.

함수 호출시 해당 값을 반환해 줄 것 입니다!

 

우리는 위의 WeatherData 객체를 이용하여 3개의 디스플레이 화면을 구현해야 합니다.

1) 기상 통계

2) 현재 기상 조건

3) 기상 예보

 

따라서 기상 환경이 바뀌어 WeatherData에 새로운 측정값이 기록되면 measurementsChanged()가 호출되고 기상통계, 현재 기상 조건, 기상 예보 이렇게 3가지 디스플레이 또한 새로운 정보를 담아 갱신될 수 있도록 구현해야 합니다.

 

추가적으로 이 시스템은 확장이 가능해야 하며, 다른 개발자가 새로운 디스플레이를 추가해줄 수 있고, 사용자가 자신이 원하는 디스플레이를 추가/제거 할 수 있어야 합니다.

 

이를 해결하기 위해 매우 직관적으로 다음과 같이 코드를 작성해 봅시다!

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

그냥 단순하게 getter로 값을 받아와 이 값으로 갱신해주는 함수 입니다.

 

일단 위 코드의 문제점 몇가지를 생각해 보면...

1) 인터페이스가 아닌 구체적인 구현을 했기 때문에 다형성을 활용할수가 없습니다.

2) 따라서 실행중 디스플레이에 동적으로 항목을 추가/제거 할수가 없습니다.

3) 또한 새로운 디스플레이가 추가 될때 마다 코드를 추가해주어야 합니다.

4) 또한 바뀌는 부분들(위에서 update를 호출하는 것들)은 캡슐화를 해야합니다.

 

여기서 잠깐!!

 

느슨한 결합 (Loose Coupling) 이란?

느슨한 결합이란 상호작용을 하는 둘이 서로에 대하여 잘 모른다는 것을 의미합니다.

 

Subject가 옵저버 에 대해 알고있는 것은 그저 옵저버가 Observer interface를 구현한다는 것 뿐입니다.

또한 언제든 새로운 옵저버를 추가할 수 있습니다. 옵저버를 아무때나 제거해도 상관이 없습니다. subject는 그저 데이터를 보낼 뿐 이죠!

새로운 옵저버를 추가할려 해도 Subject는 변경할 부분이 없으며, Subject와 옵저버는 서로 독립적으로 재상용이 가능합니다.

둘은 변경사항이 있더라도 서로한태 영향을 미치지 않습니다.

서로 상호작용 하는 객체 사이에는 가능하다면
느슨한 결합을 하는 디자인을 사용해야 한다.

다시 돌아와서 옵저버 패턴을 이용하여 직접 구현해 봅시다! 우선 관계도를 살펴본 후 코드를 봅시다

 

 

위에서 볼수 있었듯, 우선 Subject와 Observer 모두 interface로 구현하게 될 것 입니다.

화면을 보여주는 DisplayElement 또한 인터페이스 입니다.

public interface Subject {
    public void registerObserver(Observer o);
    public void removeObserver(Observer o);
    public void notifyObservers(); // Subject 객체의 상태 변경시 이를 모든 옵저버에게 알리는!
}

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

Observer 인터페이스는 모든 옵저버 클래스에서 구현해야 합니다. 이는 모든 옵저버가 update()를 구현해야 함을 의미합니다.

또한 registerObserver()를 통하여 옵저버를 등록시켜 두면 변경사항이 생겼을 때 Subject가 옵저버 에게 날씨에 대한 정보를 인자로 전달하고 있습니다.

 

추가적으로 화면에 출력하는 DisplayElement 인터페이스를 만들어 이를 구현하도록 만듭니다.

public interface DisplayElement {
    public void display();
}
 

Subject 인터페이스를 구현한 WeatherData class 입니다.

실질적인 정보를 저장하고있으며, 변경사항 발생시 옵저버들에게 알리는 역할을 수행하고있죠!

import java.util.ArrayList;

public class WeatherData implements Subject {
    private ArrayList<Observer> observers;
    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherData() { // 생성자 호출시 옵저버들을 저장할 ArrayList 생성
        observers = new ArrayList<>();
    }

    public void registerObserver(Observer o){
        observers.add(o); // 옵저버 저장
    }

    public void removeObserver(Observer o){ // 해당옵저버 삭제
        int i = observers.indexOf(o);
        if(i >= 0){
            observers.remove(i);
        }
    }

    public void notifyObservers(){ // 옵저버들에게 변경사항이 있음을 알려줌!
        for(int i = 0; i < observers.size(); i++){
            Observer observer = (Observer)observers.get(i);
            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();
    }
}
 

현재의 날씨 정보를 보여주는 디스플레이 함수 또한 Observer, DisplayElement 인터페이스를 구현하여 만들어 진다.

public class CurrentConditionsDIsplay implements Observer, DisplayElement{
    private float temperature;
    private float humidity;
    private Subject weatherData;

    public CurrentConditionsDIsplay(Subject weatherData) { // Subject를 인자로 넘겨 받은후,
        this.weatherData = weatherData; // 그 레퍼런스를 저장해 두고,
        weatherData.registerObserver(this); //  레퍼런스를 이용하여 this 옵저버를 등록함
    }

    public void update (float temperature, float humidity, float pressure){ // 정보갱신 
        this.temperature = temperature;
        this.humidity = humidity;
        display();
    }

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

이를 main에서 실행하면 잘 작동하는 것을 알 수 있습니다!!

public class WeatherStation {
    public static void main(String[] args){
        WeatherData weatherData = new WeatherData();

        CurrentConditionsDIsplay curDisplay = new CurrentConditionsDIsplay(weatherData);

        weatherData.setMeasurements(80, 65, 30.4f);
        weatherData.setMeasurements(82, 70, 30.4f);
        weatherData.setMeasurements(77, 77, 20.4f);
    }
}
 

 

두 방식 모두 장/단 점이 있는 방식입니다.

댓글