Lee's Grow up

[디자인패턴/Design Pattern] Observer 패턴 / 관찰자 패턴 / Publish-Subscribe 패턴 본문

PROGRAMMING/디자인패턴

[디자인패턴/Design Pattern] Observer 패턴 / 관찰자 패턴 / Publish-Subscribe 패턴

효기로그 2019. 12. 11. 16:39
반응형

관련 내용은 [자바 언어로 배우는 디자인 패턴 입문],[Head First Design Pattern] 의 내용을 정리해서 작성한 내용입니다. 잘못된 부분이 있으면 댓글로 알려주시면 감사하겠습니다.

1. Observer 패턴이란?


한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들에게 변화를 알려주고 자동으로 내용이 갱신되는 방식으로 일대다의 의존성을 정의합니다.
즉 상태 변화에 따른 처리를 기술할 때 사용합니다. 또한 느슨한 결합을 통해 의존성을 제거해줍니다.

2. Observer 패턴의 등장 인물


  • Subject(관찰 대상자)의 역할
    1. '관찰되는 대상'을 관리하는 요소
  • ConcreteSubject(구체적인 관찰 대상자)의 역할
    1. 구체적으로 Subject를 표현하는 역할이며, 통보하는 클래스
  • Observer(관찰자)의 역할
    1. '상태가 변화됨'을 전달 받는 역할을 합니다.
  • ContreteObserver(구체적인 관찰자)의 역할
    1. Observer를 구체적으로 표현하며, '변화를 통보 받는' 클래스

3. 예제

그렇다면 ConcreteSubject와 ContreteObserver를 바로 연관관계를 맺고 사용하면 되지 왜 Subject와 Observer 인터페이스를 구현하면서 사용하는 것일까 ?

3-1. WeatherDataSubject 클래스
public class WeatherDataSubject {

    // 인스턴스 변수 
    ...

    public void weatherUpdate() {

        float temp = getTemperature(); // 온도 가져오기

        // 변화를 알려주기
        currentWeatherDisplay.update(temp, humidiry,);
        forecastDisplay.update(temp, humidiry,);
    }
}

위와 같이 존재하고 각각 CurrentWeatherDisplay 클래스와, ForecastDisplay 클래스가 있을 때 여러가지 문제점이 발생합니다.

  • 첫째로, 통계를 보고 싶어서 StatisticsDisplay 라는 클래스를 추가하고 싶은 경우, weatherUpdate() 메서드 부분에 수정이 필요합니다.
  • 둘째로, 각 클래스마다 행동이 다를 수 있기 때문에 캡슐화를 해야 합니다.
  • 셋째로, 공통적으로 사용하는 메소드 update()를 인터페이스로 정의합니다.

이렇게 객체 간 의존성이 존재하게 됩니다.

3. 해결법

위와 같은 문제를 해결하기 위해 Observer 패턴을 사용합니다.

  • 각 객체는 언제든지 옵저버로 추가되고 삭제될 수 있어야 합니다.
  • 변화될 수 있는 행동은 구현이 아닌 인터페이스를 사용해야 합니다.
3-1. 예제 클래스 다이어그램

3-2. interface ( Subject, Observer, DisplayElement )

여기서 DisplayElement는 행동을 캡슐화할 목적인 인터페이스로 디자인의 원칙에 따라 디자인하기 위해서 추가된 인터페이스입니다. 가장 핵심은 Subject, Observer 인터페이스입니다.

public interface Subject {
    public void registerObserver(Observer o); // 옵저버 등록
    public void removeObserver(Observer o);      // 옵저버 삭제
    public void notifyObservers();               // 변화를 알려주기 위한 메소드
}

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

public interface DisplayElement {
    public void display();
}
  • Subject : '관찰되는 대상'을 관리하는 요소 인터페이스. Observer를 저장, 삭제, 알려주는 메소드를 선언합니다.
  • Observer : '상태가 변화됨'을 관리하는 요소 인터페이스. 통보당했을 때 처리할 메소드를 선언합니다.
  • DisplayElement 인터페이스는 행동을 추상화하기 위한 인터페이스로 화면에 그려줄 display() 메소드를 선언합니다.
3-3. WeatherData 클래스

이제 처음에 3번에서 나온 예제의 문제점을 보였던 WeatherData 클래스입니다. 아래는 옵저버 패턴을 사용한 클래스의 변경 내용입니다.

public class WeatherDataSubject implements Subject {
    private ArrayList<Observer> observers; // 옵저버들을 저장
    private float temperature;                
    private float humidity;

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

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

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

    // 변화된 상태를 알려주는 메소드
    @Override
    public void notifyObservers() {
        for (Observer o : observers) {
            o.update(temperature, humidity);
        }
    }

    // 상태가 변화됨을 감지하는 메소드
    public void measurementsChanged() {
        notifyObservers();
    }

    // 상태를 변화 시키는 메소드
    public void setMeasurments(float temperature, float humidity) {
        this.temperature = temperature;
        this.humidity = humidity;
        measurementsChanged();
    }
}

우선 문제였던 옵저버의 추가 삭제를 쉽게 하기 위해서 observers 라는 List 컬렉션을 필드로 가지고, 생성자를 통해 초기화를 해줍니다.
그리고 Subject 인터페이스의 메소드들을 정의해줍니다. 마지막으로 변경된 정보를 받아들일 메소드들을 차례로 정의해줍니다.

3-4. CurrentWeatherDisplay 클래스
public class CurrentWeatherDisplay implements Observer, DisplayElement {
    private float temperature;
    private float humidity;
    private Subject weatherData;

    public CurrentWeatherDisplay( WeatherDataSubject weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

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

    @Override
    public void display( ) {
        System.out.println("최근 기온은 : " + temperature + " / 습도는 : " + humidity +"% 입니다.");
    }
}
  1. WeatherData 객체로부터 변경 사항을 받기 위해 Observer 인터페이스를 구현합니다. 같은 Subject를 쓰기위해 생성자를 통해 weatherData를 전달받고, 필드의 객체에 참조시킵니다. 그리고 registerObserver(Observer) 메소드를 호출해 해당 클래스를 List에 등록시킵니다. 이 부분은 필요에 따라 메소드로 분리하셔도 됩니다.
  2. update() 메소드를 재정의합니다. 그리고 display()메소드를 호출시켜 줍니다. 마찬가지로 재정의해줍니다.
3-5 Main 클래스
public class Main {
    public static void main(String[] args) {
        WeatherDataSubject weatherData = new WeatherDataSubject();

        CurrentWeatherDisplay cwd = new CurrentWeatherDisplay(weatherData);
        ForecastDisplay fd = new ForecastDisplay(weatherData);
        //StatisticsDisplay sd = new StatisticsDisplay(weatherData);

        weatherData.setMeasurments(28, 30);
        weatherData.setMeasurments(34, 89);

    }
}
실행 결과
최근 기온은 : 28.0 / 습도는 : 30.0% 입니다.
내일의 예상 기온은 : 30.0 / 에상 습도는 : 35% 입니다.
최근 기온은 : 34.0 / 습도는 : 89.0% 입니다.
내일의 예상 기온은 : 37.0 / 에상 습도는 : 88% 입니다.
람다로 리팩토링

위 예제에서 사용된 Observer 인터페이스는 함수형 인터페이스라고 볼 수 있다. 그렇기에 아래 처럼 사용이 가능

weatherData.registerObserver((temperature, humidity) 
                -> System.out.println("최근 기온은 : " + temperature + " / 습도는 : " + humidity +"% 입니다."));

        weatherData.registerObserver((temperature, humidity) 
                -> System.out.println("내일의 예상 기온은 : " + (temperature + (new Random().nextInt(6))) +" / 에상 습도는 : " + (new Random().nextInt(100)) + "% 입니다." ));

예제에서는 Display라는 행동을 분리해서 인터페이스화 했었지만, 람다에서는 필요한 구현을 넘기기 때문에 해당 Display 인터페이스와 옵저버 구현 클래스 2개가 불필요해진다.

다만 람다가 만능은 아닙니다. 현재는 구현 자체가 간단하지만, 구현의 로직이 복잡하고 어려운 경우는 기존의 방식처럼 클래스와 인터페이스를 통한 구현을 고려해봐야합니다. 상황에 맞게 사용하시면 되겠습니다.

사용 이유

실행결과와 같이 Clilet는 이제 옵저버를 등록만하고, 변경만 해주면 알아서 변화된 상태를 감지하고 동작을 합니다. ( 예제에서는 display()와 같이 )

  • 문제점이였던 직접적인 참조가 해결되어서 언제든 Obverser을 추가하고 삭제가 가능하다. ( 느슨한 결합 )
  • 전혀 새로운 클래스 예를들어 StatisticsDisplay 클래스가 추가 되어도 Observer 인터페이스를 구현하고, 필요한 로직을 작성해두면, 기존의 코드에는 수정이 없이 옵저버로 사용이 가능하게 됩니다.

4. 관련 패턴


  • Mediator 패턴 : 패턴의 역할간의 통신에 Observer 패턴을 사용하는 경우가 있습니다. 두 패턴 모두 '상태 변화를 알린다'는 점에서 비슷하지만, 목적이나 시점이 다릅니다. 해당 패턴은 상태변화의 알림 목적이 다른 역할의 조정이라는 목적으로 동작하고, Observer은 상태변화를 알려서 동기화를 이루는 일에 주안점을 두고 있습니다.
반응형
Comments