Featured image of post 2. 옵저버 패턴

2. 옵저버 패턴

헤드 퍼스트 디자인 패턴

옵저버 패턴은 중요한 일이 일어났을 때 객체에게 새 소식을 알려 줄 수 있는 패턴이다.

일대 다 관계나 느슨한 결합같은 개념을 통해 구성되며 자주 사용되는 패턴 중 하나이다.

옵저버 패턴

옵저버 패턴은 주제(Subject), 옵저버(observer)로 구성된다.

flowchart LR
  a((주제 객체))

  subgraph 옵저버 객체
    b((객체 1))
    c((객체 2))
    d((객체 3))
  end

  a-.->b
  a-.->c
  a-.->d

이 책에서는 옵저버 패턴을 신문 구독을 예시로 들고 있다.

  1. 신문사가 신문을 찍어낸다.
  2. 독자가 특정 신문사에 구독 신청을 하면 구독 해지 전까지 새로운 신문이 나올 때마다 배달을 받을 수 있다.
  3. 신문을 보고싶지 않으면 구독 해지 신청을 한다.
  4. 신문사가 망하지 않는 이상 여러 구독자들은 신문을 구독하거나 해지하는 것을 반복한다.

옵저버 패턴의 정의

옵저버 패턴은 일련의 객체 사이에서 일대다 관계를 정의하고, 한 객체의 상태가 변경되면 그 객체에 의존하는 모든 객체에 연락이 간다.

옵저버 패턴은 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다(one-two-many) 의존성을 정의한다.

  • 옵저버는 주제에 딸려 있으며 주제의 상태가 바뀌면 옵저버에게 정보가 전달된다.
  • 보통 주제 인터페이스와 옵저버 인터페이스가 들어있는 클래스 디자인으로 구현한다.

옵저버 패턴의 구조

classDiagram
    direction LR
    class Subject {
      << interface >>  
      registerObserver()
      removeObserver()
      notifyObservers()
    }
    class Observer {
      << interface >>  
      update()
    }
    class ConcreteSubject {
      registerObserver()
      removeObserver()
      notifyObservers()
  
      getState()
      setState()
    }
    class ConcreteObserver {
      update()
    }

    Subject --> Observer : 옵저버
    
    ConcreteObserver ..> Observer
    
    ConcreteSubject <-- ConcreteObserver : 주제
    Subject <.. ConcreteSubject
  • Subject
    • 주제를 나타내는 인터페이스로 객체에서 옵저버로 등록하거나 옵저버 목록에서 탈퇴하고 싶을 때 해당 인터페이스의 메소드를 사용한다.
  • Observer
    • 옵저버가 될 가능성이 있는 객체는 만드시 Observer 인터페이스를 구현해야 한다.
    • 주제의 상태가 바뀌었을 때 호출되는 update() 메소드로만 구성된다.
  • ConcreteSubject
    • 주제 역할을 하는 구상 클래스는 항상 Subject 인터페이스를 구현해야 한다.
    • 주제 클래스에는 등록 및 해지용 메소드와 상태가 바뀔 때마다 모든 옵저버에게 연락하는 notifyObservers() 메소드도 구현해야 한다.
  • Concreteobserver
    • Observer 인터페이스만 구현한다면 무엇이든 옵저버 클래스가 될 수 있다.
    • 각 옵저버는 특정 주제에 등록해서 연략 받을 수 있다.

출판-구독(publish-Subscribe) 패턴과의 차이점

출판-구독 패턴은 구독자가 서로 다른 유형의 메시지에 관심을 가질 수 있고, 출판사와 구독자를 더 세세하게 분리할 수 있는 복잡한 패턴이다.

느슨한 결합의 위력

느슨한 결합(Loose Coupling)은 객체들이 상호작용할 수는 있지만, 서로 잘 모르는 관계를 의미한다.

  • 느슨한 결합을 활용하면 유연성이 좋아진다.
  • 옵저버 패턴은 느슨한 결합의 좋은 예시이다.

옵저버 패턴의 느슨한 결합

  • 주제는 옵저버가 특정 인터페이스(Observer)를 구현한다는 사실만 알고있다.
  • 옵저버는 언제든지 추가할 수 있다.
    • 주제는 Observer 인터페이스를 구현하는 객체의 목록에만 의존하므로 엔제든지 새로운 옵저버를 추가할 수 있다.
    • 실행 중에 하나의 옵저버를 다른 옵저버로 바꿔도 주제는 계속해서 다른 옵저버에게 데이터를 보낼 수 있다.
  • 새로운 형식의 옵저버를 추가할 때도 주제를 변경할 필요가 없다.
    • 새로운 옵저버 클래스를 추가할 때 변경 없이 Observer 인터페이스만 구현한다면 어떤 객체에도 연락할 수 있다.
  • 주제와 옵저버는 서로 독립적으로 재사용 할 수 있다.
    • 둘이 서로 단단하게 결합되어 있지 않기 때문에 손쉽게 재사용 할 수 있다.
  • 주제나 옵저버가 달라져도 서로에게 영향을 미치지는 않는다.
    • 느슨하게 결합되어 있으므로 주제나 옵저버 인터페이스를 구현한다는 조건만 만족한다면 어떻게 고쳐도 문제가 생기지 않는다.

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

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

풀 방식

기본적인 옵저버 패턴은 주제가 변경되었을 때 옵저버에게 알리는 방식(푸시)으로 처리되었다. 하지만 이러한 방식은 변경 반영이 필요없는 옵저버에게도 알리게 된다.

이러한 문제가 존재한다면 옵저버가 필요할 때마다 데이터를 끌어오는 풀 방식을 통해 개선될 수 있다.

값이 변했다는 알림을 옵저버가 받았을 때 주제에 있는 게터 메소드를 호출해서 필요한 값을 당겨오도록 변경한다.

푸시와 풀은 구현 방법의 문제이지만 시간이 지남에 따라 애플리케이션이 계속 바뀌고 복잡해지므로, 대체로 옵저버가 필요한 데이터를 골라서 가져가도록 만드는 방법이 더 좋다.

주제에서 알림 보내기

1
2
3
4
5
6
7
class Subject {
  public void notifyObservers() {
      for (Observer observer: observers) {
          observer.update();
      }
  }
}

옵저버에서 알림 받기

  1. Observer 인터페이스에서 update() 메소드에 매개변수가 없도록 서명을 바꾼다.
1
2
3
public interface Observer {
    public void update();
}
  1. update() 메소드의 서명을 바꾸고 Subject 구상 주제 메소드의 게터로 날씨를 받아오도록 Observer 구상 클래스를 수정한다.
1
2
3
4
5
6
class ExtendObserver extends Observer {
  public void update() {
      this.item = ExtendSubject.getItem();
      이후_동작();
  }
}

디자인 원칙

  • 달라지는 부분을 찾아내고 달라지지 않는 부분과 분리한다.
    • 옵저버 패턴에서 변하는 것은 주제의 상태와 옵저버의 개수, 형식이다.
    • 옵저버 패턴에서는 주제를 바꾸지 않고도 주제의 상태에 의존하는 객체들을 바꿀 수 있다.
    • 나중에 바뀔 것을 대비해 두면 편하게 작업할 수 있다.
  • 구현보다는 인터페이스에 맞춰 프로그래밍한다.
    • 주제와 옵저버에서 모두 인터페이스를 사용했다.
  • 상속보다는 구성을 활용한다.
    • 옵저버 패턴에서는 구성을 활용해서 옵저버들을 관리한다.
    • 주제와 옵저버 사이의 관계가 상속이 아닌 구성으로 이루어진다.

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

요구 사항 분석

flowchart LR
    a((습도 센서))
    b((온도 센서))
    c((기압 센서))
    
    d([기상 스테이션])
    
    e((WeatherData 객체))
    
    f[[디스플레이 장비]]

    a-.->d
    b-.->d
    c-.->d
    
    d<-.데이터 취득.-e
    e-.화면에 표시.->f
  • WatherData 객체를 바탕으로 만들어짐
    • 현재 기상 조건(온도, 습도 기압) 추적
  • WatherData 객체를 바탕으로 3개 항목을 화면에 표시함
    • 현재 조건, 기상 통계, 간단한 기상 예보
    • 해당 항목들이 최신 측정치를 수집할 때마다 실시간으로 갱신
  • 다른 개발자가 직접 날씨 디스플레이를 만덜어 바로 넣을 수 있도록 확장 가능해야 함
  • 정보가 화면에 표시되는 횟수로 고객에가 요금 부과

구현 목표

디스플레이를 구현하고 새로운 값이 들어올 때마다(measurementsChanged() 호출 시) WeatherData에서 디스플레이를 업데이트 해야한다.

classDiagram
    direction RL
    class WeatherData {
        getTemperature()
        getHumidity()
        getPressure()
        measurementsChanged()
    }
    
    note for WeatherData "기상 관측값 갱신시 measurementsChanged() 호출"
    
  • WeatherData 클래스에는 3가지 측정값(온도, 습도 기압)의 게터 메소드가 있다.
  • 새로운 기상 측정 데이터가 들어올 때마다 measurementsChanged() 메소드가 호출됨
    • 이 메소드가 어떤 식으로 호출되는지 모른다(알 필요도 없다)
  • 가상 데이터를 사용하는 디스플레이 요소 3가지를 구현해야 한다.
    • 현재 조건, 기상 통계, 기상 예보
  • 디스플레이를 업데이트하도록 measurementsChanged()에 코드를 추가해야 한다.

추가 목표

기상 스테이션이 성공하면 디스플레이가 더 늘어날 수도 있고, 디스플레이를 추가할 수 있는 마켓플레이스가 만들어질지도 모른다. 따라서 확장성을 고려한다면 좋을 수 있다.

가상 스테이션용 코드 추가

1차적으로 다음과 같이 구현될 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class WeatherData {
    // 인스턴스 변수 선언
    public void 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);
    }
    
    // 기타 메소드
}

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

  • 구체적인 구현(Bad)
    • 각 디스플레이를 업데이트 하는 로직이 구체적인 구현에 맞춰져 있으므로 프로그램을 고치지 않고는 다른 디스플레이를 추가, 제거할 수 없다.
  • 캡슐화 부재(Bad)
    • 디스플에이를 업데이트 하는 로직은 바뀔 수 있는 부분으로 캡슐화가 필요함
  • 공통된 인터페이스(Good)
    • {객체}.update 메소드를 호출하는 것으로 업데이트를 하는 공통적인 인테페이스를 구성했음

기상 스테이션 설계하기

classDiagram
    direction LR
    class Subject {
      << interface >>  
      registerObserver()
      removeObserver()
      notifyObservers()
    }
    class Observer {
      << interface >>  
      update()
    }
    class DisplayElement {
        << interface >>
        display()
    }
    
    class WeatherData {
        registerObserver()
        removeObserver()
        notifyObservers()
        
        getTemperature()
        getHumidity()
        getPressure()
        measurementsChanged()
    }
    
    class CurrentConditionsDisplay {
        update()
        display()
    }
    
    class StatisticsDisplay {
        update()
        display()
    }
    
    class ThirdPartyDisplay {
        update()
        display()
    }
    
    class ForecastDisplay {
        update()
        display()
    }

    Subject --> Observer : 옵저버
    WeatherData ..> Subject
    
    CurrentConditionsDisplay --> WeatherData : 주제
    CurrentConditionsDisplay ..> Observer
    
    StatisticsDisplay ..> DisplayElement
    StatisticsDisplay ..> Observer
    
    ThirdPartyDisplay ..> DisplayElement
    ThirdPartyDisplay ..> Observer
    
    ForecastDisplay ..> DisplayElement
    ForecastDisplay ..> Observer
  • Subject: 주제 인터페이스
  • Observer: 옵저버 인터페이스, 주제에서 옵저버에게 갱신된 정보를 전달하는 방법 제공
  • DisplayElement: 모든 디스플레이 요소의 구현 인터페이스
  • WeatherData: Subject 인터페이스를 구현할 기상 정보
  • CurrentConditionsDisplay: WatherData 객체로부터 얻은 현재 측정값을 보여줄 옵저버 이면서 디스플레이 요소
  • StatisticsDisplay: 측정치의 통계치를 표시할 옵저버 이면서 디스플레이 요소
  • ForecastDisplay: 측정치를 바탕으로 기상 예보를 화면에 보여줄 디스플레이 요소
  • ThirdPartyDisplay: 새롭게 구현될 디스플레이 요소…

디자인 도구상자 안에 들어가야 할 도구들

  • 객체지향 기초
    • 추상화
    • 캡슐화
    • 다형성
    • 상속
  • 객체지향 원칙
    • 바뀌는 부분은 캡슐화힌다.
    • 상속보다는 구성을 활용한다.
    • 구현보다는 인터페이스에 맞춰 프로그래밍한다.
    • + 상호작용을 하는 객체 사이에는 가능하면 느슨한 결합을 사용한다.
  • 객체지향 패턴
    • 전략패턴
    • + 옵저버 패턴
      • 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.

핵심 정리

  • 옵저버 패턴은 객체들 사이에 일대다 관계를 정의한다.
  • 주제는 동일한 인터페이스를 써서 옵저버에게 연락한다.
  • Observer 인터페이스를 구현하기만 하면 어떤 구상 클래스의 옵저버라도 패턴에 참여할 수 있다.
  • 주제는 옵저버들이 Observer 인터페이스를 구현한다는 것을 제외하면 옵저버에 관해 전혀 모른다.(느슨한 결합)
  • 옵저버 패턴을 사용하면 주제가 데이터를 보내거나(푸시) 옵저버가 데이터를 가져올(풀) 수 있다.
    • 일반적으로 풀 방식을 옳은 방식으로 간주함
  • 옵저버 패턴은 여러 개의 주제와 메시지 유형이 있는 복잡한 상황에서 사용하는 출판-구독 패턴과 친척이다.
  • 옵저버 패턴은 자주 쓰이는 패턴으로 모델-뷰-컨트롤러(MVC)를 배울 때 다시 볼 수 있을것이다.
  • GUI 프레임 워크들이 옵저버 패턴을 많이 사용한다.
  • RxJava, 자바빈, RMI 외 코코아, 스위프트, JS 같은 다른 언어의 프레임워크에서도 옵저버 패턴을 많이 사용한다.