옵저버 패턴

돔푸·2023년 11월 2일

<들어가며>

2일차, 옵저버 패턴을 공부했다!

옵저버 패턴은 느슨한 결합을 사용하는 패턴이다. 어떠한 객체의 변경이 이루어졌을때, 그 객체에 의존하는 다른 객체들에게 어떠한 신호가 가고, 자동으로 내용이 업데이트 되는 일대다 의존성에 사용된다.

<옵저버 패턴>

만약 Weather 클래스가 있고, 그 Weather 클래스에는 기상 상황을 나타내는 여러 변수가 있다. 외부로부터(우리는 Main메서드에서) 값의 변경이 이루어졌을때, ForecastDisply, TemperatureDisplay등의 클래스에서 자동으로 변경 사항을 출력하고자 한다.(이 디스플레이들은 언제든지 추가/변경/삭제 될 수 있다.) 어떻게 구현하는게 좋을까?

1. 추상화해보기.

일단 Weather가 업데이트 되었을 때, 각 디스플레이를 호출할 공통된 메서드-update()가 필요할 것 같다. 그렇게 만들면, update() 메서드를 가진 Observer 인터페이스로 디스플레이들을 추상화한 후, iterator로 이용해서 update를 뿌릴 수 있기 때문이다. 구현해보면 이 정도 코드가 될 것 같다.

Weather 클래스

public class Weather implements Subject {

    private List<Observer> observers = new ArrayList<>();
    private float temperature;
    private float humidity;
    private float pressure;

    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    public void deleteObserver(Observer observer) {
        observers.remove(observer);
    }

    public void notifyObserver() {
        for (Observer observer : observers) {
            observer.update(float temperature, float humidity, float pressure);
        }
    }

	//외부로부터 호출되는 메서드
    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        notifyObserver();
    }
}

이렇게 만들면 좋을 것 같다. 외부로부터 setMeasurements() 메서드가 호출되면 값을 업데이트하고 Observer들에게 신호를 보낸다.(update) 이러면 디스플레이에서 신호를 받아 자신의 값을 또한 업데이트하고 출력하면 된다.

ForecastDisplay 클래스

@Slf4j
public class ForecastDisplay implements Observer, DisplayElement {

	//Observer는 update메서드를 추상화, DisplayElement는 display메서드를 추상화하였다.
    private float temperature;
    private float humidity;
    private float pressure;

    public ForecastDisplay(Weather weather) {
        weather.addObserver(this);
    }

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

    @Override
    public void display() {
        log.info("기상 디스플레이 - 온도 : {} / 습도 : {} / 기압 : {}", temperature, humidity, pressure);
    }
}

이렇게 Display클래스도 짜면 된다. 좋다! 하지만 문제점이 하나가 있다. 현재는 ForecastDisplay 클래스 하나만 존재하지만 미래에 다른 Observer들이 추가될 수 있다. 그렇다면 그 클래스는 어쩌면 3가지 정보 중 일부만 필요할 수도 있다. 그런 클래스은 update 방식을 또 다르게 구성해야 한다. 그런 수고를 줄일 수는 없을까?

2. 업데이트 방식을 푸시에서 풀로 바꿔보자!

풀 방식으로 업데이트를 하면 위 문제를 해결할 수 있다. Weather클래스에서 Observer로 정보를 밀어주는 것이 아니라, Weather클래스는 Observer에게 신호만 보내고, 정보를 업데이트하는 것은 Observer에게 맡기는 것이다. 이렇게 하면 각 Observer마다 필요한 정보만 취할 수 있으므로 후에 추가/수정에 용이하다. 바뀐 코드는 이렇게 된다.

풀 방식의 Weather 클래스

@Getter
public class Weather implements Subject {

    private List<Observer> observers = new ArrayList<>();
    private float temperature;
    private float humidity;
    private float pressure;

    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    public void deleteObserver(Observer observer) {
        observers.remove(observer);
    }

    public void notifyObserver() {
        for (Observer observer : observers) {
        	//신호만 준다!
            observer.update();
        }
    }

	//외부로부터 호출되는 메서드
    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        notifyObserver();
    }
}

풀 방식의 ForecastDisplay 클래스

@Slf4j
public class ForecastDisplay implements Observer, DisplayElement {

	//Observer는 update메서드를 추상화, DisplayElement는 display메서드를 추상화하였다.
    private float temperature;
    private float humidity;
    private float pressure;

	//이제는 Weather를 멤버로 갖는다!!!
    private final Weather weather;

    public ForecastDisplay(Weather weather) {
        this.weather = weather;
        weather.addObserver(this);
    }

    @Override
    public void update() {
    	//Weather의 Getter메서드로 풀 한다!!!
        this.temperature = weather.getTemperature();
        this.humidity = weather.getHumidity();
        this.pressure = weather.getPressure();
        display();
    }

    @Override
    public void display() {
        log.info("기상 디스플레이 - 온도 : {} / 습도 : {} / 기압 : {}", temperature, humidity, pressure);
    }
}

출력결과

기상 디스플레이 추가
온도 변경1
기상 디스플레이 - 온도 : 10.0 / 습도 : 10.0 / 기압 : 10.0
온도 변경2
기상 디스플레이 - 온도 : 20.0 / 습도 : 30.0 / 기압 : 40.0
온도 디스플레이 추가
온도 변경3
기상 디스플레이 - 온도 : 40.0 / 습도 : 20.0 / 기압 : 30.0
온도 디스플레이 - 온도 : 40.0

이렇게 Weather를 구성하게 되면 후에 다른 Observer가 생기더라도 Weather클래스를 수정할 일이 없다. 이제 정리해보자.

<마무리>

옵저버 패턴은 일대다 의존성에 유용하다. 한 객체의 상태가 변했을 때, 다른 객체들에게 자동으로 신호가 가고 처리를 하는 형태의 코드는 사실 우리가 많이 접해왔다. 자바빈 등에서도 볼 수 있는 패턴이다.
옵저버 패턴은 한때 java.util에서 제공하기도 하였지만 기능이 너무 빈약하여 그냥 개발자가 직접 구현하는 것이 용이하다고 판단하였는지 deprecated되었다. 이제는 옵저버 패턴을 몸으로 익혀서 써야할 것 같다.
또한 옵저버 패턴을 통해 느슨한 결합에 대해 공부할 기회가 있었다. 느슨한 결합은 계속 반복되는 개념인데, 서로에게 강하게 의존하지 않아서 추가/변경/삭제가 쉽고 독립적으로 재사용될 수 있으며 영향을 미치지 않는 형태이다.

위 글은 다음 책을 바탕으로 작성하였습니다.
헤드퍼스트 디자인패턴 : https://www.yes24.com/Product/Goods/108192370
소스 코드 : https://github.com/Dompoo/DesignPatternStudy/tree/master/src/main/java/dompoo/study/observerPattern

profile
나중에 또 모를 것들 모음

0개의 댓글