m1ndy5's coding blog

옵저버 패턴(Observer Pattern) 본문

백엔드 with java/Design Pattern

옵저버 패턴(Observer Pattern)

정민됴 2023. 11. 22. 20:55

옵저버 패턴의 정의

옵서버 패턴은 객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴이다. 주로 분산 이벤트 핸들링 시스템을 구현하는 데 사용된다.(출처: 위키백과)
즉, 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들에게 연락이가고 내용이 갱신되는 일대다 의존성을 뜻한다.
상태를 저장하고 있는 객체를 Subject, 갱신된 값들을 받는 객체들을 Observer로 보면된다.
보통 Subject Interface와 Observer Interface로 구현한다

public interface Subject{
    // Observer 등록
    public void registerObserver(Observer o);
    // 등록된 Observer 삭제
    public void removeObserver(Observer o);
    // 값이 변경되면 Observers에게 알림
    public void nofifyObservers();
}

public interface Observer{
    public void update();
}

옵저버 패턴 이해

옵저버 패턴은 유튜브 채널을 구독하는 것과 비슷하다고 생각하면 되는데
시청자가 유튜버를 구독하면 (알림설정이 되어있다는 전제)
새로운 동영상을 올리게 되면 시청자에게 새로운 동영상이 올라왔다는 알림이 가고 시청자는 새로운 동영상을 시청한다.
이 때 더이상 알림을 받고싶지 않다면 구독을 해지하고 새로운 동영상들을 보지 않을 수 있다. (해지하면 못본다는 전제)
한 유튜버는 여러 시청자로부터 구독을 당할 수도 해지를 당할 수도 있다.
여기서 유튜버는 Subject, 구독자들은 Observer라고 생각하면 된다.

Subject가 Observer에게 데이터를 보내는 Push 방식

Observer가 Subject로부터 갱신된 데이터를 받는데는 두가지 방법이 있다.
첫 번째로 값이 갱신될 때마다 Subject가 Observer에게 야 나 값바꼈어 가져가 하고 보내주는 방법이 있고
Observer가 야 너 값 바꼈어?? 나 좀 가져간다하고 Subject의 getter를 사용해서 값을 가져오는 방법이 있다.
일단 Push 방식을 알아보자

public interface Subject{
    // Observer 등록
    public void registerObserver(Observer o);
    // 등록된 Observer 삭제
    public void removeObserver(Observer o);
    // 값이 변경되면 Observers에게 알림
    public void nofifyObservers();
}

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

// Subject interface를 implements한 WeatherData
// 온도, 습도, 기압정보를 가지고 있다.
public class WeatherData implements Subject{
    // 이 subject를 구독한 구독자들(?) 여러명이 구독할 수 있기 때문에 list로 표현
    private List<Observer> observers;
    private float temperature;
    private float humidity;
    private float pressure;

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

    // Observer 등록
    public void registerObserver(Observer o){
        observers.add(o);
    }

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

    // Observers에게 알리기
    public void notifyObservers(){
        for (Observer observer : observers){
            observer.update(temperature, humidity, pressure);
        }
    }

    public void measurementsChanged(){
        notifyObservers();
    }

    public void setMeasurements(float temp, float humid, float press){
        this.temperature = temp;
        this.humidity = humid;
        this.pressure = press;
        measurementsChanged(); // setter가 호출되면 값이 변경되었음을 알리는 메서드
    }
}

// 날씨정보를 보여주는 디스플레이, Observer interface를 implements하고 있음
public class CurrentDisplay implements Observer{
    private float temperature;
    private float humidity;
    private WeatherData weatherData;

    // 생성될 때 weatherData에 observer로 등록
    public CurrentDisplay(WeatherData weatherData){
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

    public void update(float temp, float humid, float press){
        this.temperature = temp;
        this.humidity = humid;
    }

    public void display(){
        // 온.습도 정보 디스플레이
    }
}

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

        CurrentDisplay current = new CurrentDisplay(weatherData);
        weatherData.setMeasurements(1, 1, 1); // setter를 호출하면 구독한 옵저버의 update 메서드가 실행되면서 값 갱신
    }
}

이렇게 했을 때 한가지의 안좋은 점은 Observer interface의 update 메서드에 어떤 값이 필요할 때마다 파라미터로 넣어주어야하는데 몇몇 값이 필요없는 observer들도 다 값들을 모두 받게 된다.
그럼 이 값들을 update의 매개변수로 보내주는 게 아니라 getter 메서드로 가져온다면?

Observer가 Subject로부터 값을 가져오는 Pull 방식

public interface Observer{
    public void update();
}

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

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

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

    public void measurementsChanged(){
        notifyObservers();
    }

    public void setMeasurements(float temperature, float humidity, float pressure){
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged(); //setter가 호출되면 Observers에게 변경된 값들을 전달
    }

    // getter를 만들어 주었다.
    public void getTemperature(){
        return this.temperature;
    }
    public void getHumidity(){
        return this.humidity;
    }
    public void getPressure(){
        return this.pressure;
    }
}

public class CurrentDisplay implements Observer{
    private float temperature;
    private float humidity;
    private WeatherData weatherData;

    public CurrentConditionsdisplay(WeatherData weatherData){
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

    //getter를 사용해서 값을 가져옴
    public void update(){
        this.temperature = weatherData.getTemperature();
        this.humidity = weatherData.getHumidity();
    }

    public void display(){
        System.out.println("현재 상태: 온도 " + temperature + "F, 습도 " + humidity + "%");
    }
}

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

        CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
        weatherData.setMeasurements(80, 65, 30.4f);
        // setter를 호출하게 되면 구독한 옵저버의 update메서드가 실행되면서 값이 변경
    }
}

Observer가 원하는 내용만 골라서 가져올 수 있으므로 pull 방법을 사용하면 확장성이 더 좋아진다.