헤드퍼스트 디자인패턴 Chapter2

헤드퍼스트 디자인패턴

  • 본 책을 읽고 책의 내용을 간략하게 정리한 글입니다.

Chapter 2. 객체들에게 연락 돌리기 - 옵저버 패턴

기상 모니터링 애플리케이션 알아보기

  • 이 시스템은 기상 스테이션(실제 기상 정보를 수집하는 장비), WeatherData 객체(기상 스테이션으로부터 오는 정보를 추적하는 객체), 사용자에게 현재 기상 조건을 보여 주는 디스플레이 장비 3가지 요소로 이루어진다고 가정한다.
  • WeatherData 객체는 물리 기상 스테이션과 통신해서 갱신된 기상 데이터를 가져온다.
  • 디스플레이를 업데이트하려면 우선 WeatherData 객체를 고쳐야 한다.

WeatherData 클래스 살펴보기

|    WeatherData       |
| -------------------- |
| getTemperature()     |
| getHumidity()        |
| getPressure()        |
| measurementChanged() |
/*
 * 온도, 습도, 기압 값 등 기상 관측값을 새로 받을 때마다 메서드가 호출됩니다.
 */
public void measurementsChanged() {
    // 코드가 들어갈 자리
}

구현 목표

  • 디스플레이를 구현하고 새로운 값이 들어올 때마다, 즉 measurementsChanged() 메서드가 호출될 때마다 WeatherData에서 디스플레이를 업데이트해야 한다는 사실을 파악했다.
  • 조금 더 자세히 따져보자.
    • WeatherData 클래스에는 3가지 측정값 (온도, 습도, 기압)의 Getter 메서드가 존재한다.
    • 새로운 기상 측정 데이터가 들어올 때마다 measurementsChanged() 메서드가 호출된다. (어떤 식으로 호출되는지 모르며, 그냥 메서드가 호출된다는 사실만 알고 있다.)
    • 기상 데이터를 사용하는 디스플레이 요소 3가지를 구현해야 한다. 하나는 현재 조건 디스플레이, 다른 하나는 기상 통계 디스플레이, 마지막은 기상 예보 디스플레이이다. WeatherData에서 새로운 측정값이 들어올 때마다 디스플레이를 갱신해야 한다.
    • 디스플레이를 업데이트하도록 measurementsChanged() 메서드에 코드를 추가해야 한다.

추가 목표

  • 소프트웨어 개발에서 바뀌지 않는 단 하나: 변화를 생각해야 한다.
  • 나중에 기상 스테이션이 성공하면 디스플레이가 더 늘어날 수도 있고, 디스플레이를 추가할 수 있는 마켓플레이스가 만들어질지도 모른다.
    • 확장 기능을 추가해본다면?
확장성
  • 지금은 3가지 디스플레이 뿐이지만 언젠가는 마켓플레이스에 새로운 디스플레이가 들어오게 될지도 모른다.

기상 스테이션용 코드 추가하기

public class WeatherData {
    // 인스턴스 변수 선언
    
    public void measurementsChanged() {
        
        float temp = getTemperature();
        float humidity = getHumidity();
        float pressure = getPressure();
        
        currentConditionDisplay.update(temp, humidity, pressure);
        statisticDisplay.update(temp, humidity, pressure);
        forecastDisplay.update(temp, humidity, pressure);
    }
    
    // 기타 메서드
}

원칙적으로 추가 코드 살펴보기

currentConditionDisplay.update(temp, humidity, pressure);
statisticDisplay.update(temp, humidity, pressure);
forecastDisplay.update(temp, humidity, pressure);
  • 위 부분은 바뀔 수 있는 부분이므로 캡슐화해야 한다.
    • 디스플레이 항목과 데이터를 주고받는 데 공통된 인터페이스를 사용하고 있는 것으로 보인다.
      • 모두 온도, 습도, 기압 값을 받아들이는 update() 메서드를 가지고 있으므로
    • 실행 중에 디스플레이를 더하거나 빼려면 어떻게 해야 할까?

옵저버 패턴 이해하기

  • 신문 구독 메커니즘을 제대로 이해한다면 옵저버 패턴을 쉽게 이해할 수 있다.
    • 신문사 + 구독자 = 옵저버 패턴
    • 신문사를 주제(subject), 구독자를 옵저버(observer)
  • 주제 객체
    • 주제에서 중요한 데이터를 관리한다.
    • 주제 데이터가 바뀌면 옵저버 객체에게 그 소식이 전해진다.
    • 데이터가 바뀌면 새로운 데이터 값이 어떤 방법으로든 옵저버 객체에게 전달된다.
  • 옵저버 객체
    • 옵저버 객체들은 주제를 구독하고 있으며 (주제 객체에 등록되어 있으며) 주제 데이터가 바뀌면 갱신 내용을 전달받는다.

옵저버 패턴의 정의

  • 옵저버 패턴은 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 (One-to-Many) 의존성을 정의한다.
  • 주제 객체와 옵저버 객체로 일대다 관계가 정의된다.
  • 옵저버 패턴은 여러 가지 방법으로 구현할 수 있지만, 보통 주제 인터페이스와 옵저버 인터페이스가 들어있는 클래스 디자인으로 구현한다.

옵저버 패턴의 구조

  • Subject (인터페이스)
    • 주제를 나타내는 Subject 인터페이스
    • 객체에서 옵저버로 등록하거나 옵저버 목록에서 탈퇴하고 싶을 때는 이 인터페이스 안에 있는 메서드를 사용한다.
      |      Subject       |
      | ------------------ |
      | registerObserver() |
      | removeObserver()   |
      | notifyObserver()   |
      
  • Observer (인터페이스)
    • 옵저버가 될 가능성이 있는 객체는 반드시 Observer 인터페이스를 구현해야 한다.
    • 이 인터페이스에는 주제의 상태가 바뀌었을 때 호출되는 update() 메서드밖에 없다.
    • 각 주제마다 여러 개의 옵저버가 있을 수 있다.
      | Observer |
      | -------- |
      | update() |
      
  • ConcreteObserver
    • Observer 인터페이스만 구현한다면 무엇이든 옵저버 클래스가 될 수 있다.
    • 각 옵저버는 특정 주제에 등록해서 연락할 수 있다.
      |  ConcreteObserver     |
      | --------------------- |
      | update()              |
      | // 기타 옵저버용 메서드 |
      
  • ConcreteSubject
    • 주제 역할을 하는 구상 클래스에는 항상 Subject 인터페이스를 구현해야 한다.
    • 주제 클래스에는 등록 및 해지용 메서드와 상태가 바뀔 때마다 모든 옵저버에게 연락하는 notifyObserver() 메서드로 구현해야 한다.
    • 주제 클래스에는 상태를 설정하고 알아내는 Setter/Getter 메서드가 들어있을 수 있다.
      |     ConcreteSubject      |
      | ------------------------ |
      | registerObserver() {...} |
      | removeObserver() {...}   |
      | notifyObserver() {...}   |
      | getState()               |
      | setState()               |
      

느슨한 결합의 위력

  • 느슨한 결합 (Loose Coupling)은 객체들이 상호작용할 수는 있지만, 서로를 잘 모르는 관계를 의미한다.
  • 옵저버 패턴은 느슨한 결합을 보여주는 대표적인 예시이다.
  • 옵저버 패턴에서 어떻게 느슨한 결합을 만들까?

주제는 옵저버가 특정 인터페이스(Observer 인터페이스)를 구현한다는 사실만 압니다

  • 옵저버의 구상 클래스가 무엇인지, 옵저버가 무엇을 하는지는 알 필요 없다.

옵저버는 언제든지 새로 추가할 수 있다.

  • 주제는 Observer 인터페이스를 구현하는 객체의 목록에만 의존하므로 언제든지 새로운 옵저버를 추가할 수 있다.
  • 실행 중에 하나의 옵저버를 다른 옵저버로 바꿔도 주제는 계속해서 다른 옵저버에 데이터를 보낼 수 있다.
  • 마찬가지로 아무 때나 옵저버를 제거해도 된다.

새로운 형식의 옵저버를 추가할 때도 주제를 변경할 필요가 전혀 없다.

  • 옵저버가 되어야 하는 새로운 구상 클래스가 생겼을 때도, 새로운 클래스 형식을 받아들일 수 있도록 주제를 바꿔야 할 필요 없다.
  • 새로운 클래스에서 Observer 인터페이스를 구현하고 옵저버로 등록하기만 해도 된다.

주제와 옵저버는 서로 독립적으로 재사용할 수 있다.

  • 주제나 옵저버를 다른 용도로 활용할 일이 있다고 해도 손쉽게 재사용할 수 있다.

주제나 옵저버가 달라져도 서로에게 영향을 미치지 않는다.

  • 주제나 옵저버 인터페이스를 구현한다는 조건만 만족한다면 어떻게 고쳐도 문제가 생기지 않는다.

디자인 원칙 4

  • 상호작용하는 객체 사이에는 가능하면 느슨한 결합을 사용해야 한다.
    • 느슨하게 결합하는 디자인을 사용하면 변경 사항이 생겨도 무난히 처리할 수 있는 유연한 객체지향 시스템을 구축할 수 있다.
    • 객체 사이의 상호의존성을 최소화할 수 있기 때문

기상 스테이션 설계하기

기상 스테이션 프로젝트에 옵저버 패턴 적용하기

  • 옵저버 패턴은 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 일대다 의존성을 정의한다.
    • WeatherData 클래스가 일(one)에 해당하고, 디스플레이 요소는 다(many)에 해당한다.
    • WeatherData 객체가 주제가 되고 디스플레이 요소가 옵저버가 되면 디스플레이에서 원하는 정보를 얻기 위해 WeatherData 객체에 등록한다.
    • 모든 디스플레이 요소가 다를 수 있으므로 공통적인 인터페이스를 사용해야 한다.
      • 모든 디스플레이에 WeatherData에서 호출할 수 있는 update() 메서드가 있어야 한다.
      • update()는 공통 인터페이스에서 정의해야 한다.

기상 스테이션 설계 만들어보기

  • WeatherData는 Subject 인터페이스를 구현한다.
    |      WeatherData      |
    | --------------------- |
    | registerObserver()    |
    | removeObserver()      |
    | notifyObserver()      |
    |                       |
    | getTemperature()      |
    | getHumidity()         |
    | getPressure()         |
    | measurementsChanged() |
    
  • 모든 가상 구성 요소에 Observer 인터페이스를 구현한다.
  • 이 인터페이스는 주제에서 옵저버에게 갱신된 정보를 전달하는 방법을 제공한다.
    | <<Interface>>  |
    |    Observer    |
    | -------------- |
    | update()       |
    
  • 모든 디스플레이 요소에 구현 인터페이스를 하나 더 만든다.
  • 디스플레이 항목에는 display() 메서드만 구현한다.
    | <<Interface>>  |
    | DisplayElement |
    | -------------- |
    | display()      |
    
  • 디스플레이 항목들을 각각 구현한다.
    |  CurrentConditionDisplay  |
    | ------------------------- |
    | update()                  |
    | display()                 |
    | { // 측정값을 화면에 표시 } |
    
    |      StatisticsDisplay         |
    | ------------------------------ |
    | update()                       |
    | display()                      |
    | { // 평균, 최저, 최고값 표시 }   |
    
    |  ForecastDisplay  |
    | ----------------- |
    | update()          |
    | display()         |
    | { // 기상 예보 }   |
    

기상 스테이션 구현하기

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, float pressure);
    }
    
    public interface DisplayElement {
        public void display();
    }
}

Subject 인터페이스 구현체

public class WeatherData implements Subject {
    private List<Observer> observers;
    private float temperature;
    private float humidity;
    private float pressure;
    
    public WeatherData() {
        observers = new ArrayList<Observer>();
    }
    
    /*
    * 옵저버가 등록을 요청하면 추가해주는 메서드
    */
    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }
    
    /*
     * 옵저버가 탈퇴를 요청하면 빼주는 메서드
     */
    @Override
    public void removeObserver(Observer o) {
        observers.remove(o);
    }
    
    /*
    * 가장 중요한 부분
    * 모든 옵저버에게 상태 변화를 알려주는 부분
    * 모두 Observer 인터페이스를 구현하여 update() 메서드가 있는 객체들이므로 손쉽게 상태 변화를 알려줄 수 있다.
    */
    @Override
    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();
    }
    
    // 기타 WeatherData 메서드
}

디스플레이 요소 구현체

public class CurrentConditionDisplay implements Observer, DisplayElement {
    private float temperature;
    private float humidity;
    private WeatherData weatherData;
    
    /*
     * 생성자에 WeatherData 주제가 전달되며 그 객체를 써서 디스플레이를 옵저버로 등록한다.
     * 주제 레퍼런스를 미리 저장하면 나중에 옵저버 목록에서 탈퇴할 때 유용하게 사용할 수 있다.
     */
    public CurrentConditionDisplay(WeatherData weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

    /*
     * update()가 호출되면 온도와 습도를 저장하고 display()를 호출한다.
     */
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        display();
    }
    
    /*
     * 가장 최근에 받은 온도와 습도를 출력한다.
     */
    public void display() {
        System.out.println("현재 상태: 온도 %f F, 습도 %f %".formatted(temperature, humidity));
    }
}
update() 메서드에서 display() 메서드를 호출하는 방법이 최선일까?
  • 위 예제에서는 값이 바뀔 때마다 display()를 호출해주는 방법이 괜찮아보이지만 최선의 방법은 아니다.
  • 데이터를 화면에 표시하는 더 좋은 방법은 모델-뷰-컨트롤러 패턴에서 더 자세히 알아볼 것

라이브러리 속 옵저버 패턴 알아보기

스윙 라이브러리

  • 스윙 툴킷 중 하나인 JButton 클래스
    • JButton의 슈퍼클래스인 AbstractButton을 찾아보면 리스너를 추가하고 제거하는 메서드가 잔뜩 들어있다.
    • 이 메서드들은 스윙 구성 요소에서 일어나는 다양한 유형의 이벤트를 감시하는 옵저버를 추가하거나 제거하는 역할을 한다.
      • (스윙 라이브러리에서는 리스너라고 함)

자바빈

  • 자바빈에 있는 옵저버 패턴이 궁금하다면 PropertyChangeListener 인터페이스를 확인해보자.
자바의 Observable 클래스?
  • 자바에는 옵저버 패턴용 Observable 클래스 (주제 클래스)와 Observer 인터페이스가 존재했다.
    • Observable 클래스는 직접 코드를 작성하지 않아도 옵저버를 추가하고 삭제하고 옵저버에게 알림을 보내는 메서드를 제공했다.
    • Observer 인터페이스는 update() 메서드를 포함하여 앞서 만들었던 것과 유사한 인터페이스를 제공했다.
  • 이 클래스들은 자바 9 이후로 쓰이지 않는다.
    • 각자 코드에서 더 강력한 기능을 스스로 구현하는 게 낫겠다고 생각하는 사람들이 늘면서 사라지게 되었다.

풀 (pull) 방식으로 코드 바꾸기

푸시를 풀로 바꾸는 건 좋은 생각이다

  • 사실 주제가 옵저버로 데이터를 보내는 푸시(push)를 사용하거나 옵저버가 주제로부터 데이터를 당겨오는 풀(pull)을 사용하는 방법 중 하나를 선택하는 일은 구현 방법의 문제로 볼 수 있다.
  • 대체로 옵저버가 필요한 데이터를 골라서 가져가도록 만드는 방법이 더 좋다.
  • 값이 변했다는 알림을 옵저버가 받았을 때 주제에 있는 Getter 메서드를 호출해 필요한 값을 당겨오도록 하자.
1. 주제에서 알림 보내기
  • 옵저버의 update 메서드를 인자 없이 호출하도록 WeatherData의 notifyObservers() 메서드를 수정한다.
    public void notifyObservers() {
      for (Observer observer : observers) {
          observer.update();
      }
    }
    
2. 옵저버에서 알림 받기
  • Observer 인터페이스에서 update() 메서드에 매개변수가 없도록 서명을 수정해준다.
    public interface Observer {
      public void update();
    }
    
  • update() 메서드의 서명을 바꾸고 WeatherData의 Getter 메서드로 주제의 기상 데이터를 가져오도록 각 Observer 구현 클래스를 수정한다.
  • CurrentConditionsDisplay 클래스 코드는 다음과 같이 수정한다.
    public void update() {
      this.temperature = weatherData.getTemperature();
      this.humidity = weatherData.getHumidity();
      display();
    }