옵저버 패턴은 중요한 일이 일어났을 때 객체에게 새 소식을 알려 줄 수 있는 패턴이다.
일대 다 관계나 느슨한 결합같은 개념을 통해 구성되며 자주 사용되는 패턴 중 하나이다.
옵저버 패턴
옵저버 패턴은 주제(Subject), 옵저버(observer)로 구성된다.
flowchart LR a((주제 객체)) subgraph 옵저버 객체 b((객체 1)) c((객체 2)) d((객체 3)) end a-.->b a-.->c a-.->d
이 책에서는 옵저버 패턴을 신문 구독을 예시로 들고 있다.
- 신문사가 신문을 찍어낸다.
- 독자가 특정 신문사에 구독 신청을 하면 구독 해지 전까지 새로운 신문이 나올 때마다 배달을 받을 수 있다.
- 신문을 보고싶지 않으면 구독 해지 신청을 한다.
- 신문사가 망하지 않는 이상 여러 구독자들은 신문을 구독하거나 해지하는 것을 반복한다.
옵저버 패턴의 정의
옵저버 패턴은 일련의 객체 사이에서 일대다 관계를 정의하고, 한 객체의 상태가 변경되면 그 객체에 의존하는 모든 객체에 연락이 간다.
옵저버 패턴은 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다(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 인터페이스만 구현한다면 어떤 객체에도 연락할 수 있다.
- 주제와 옵저버는 서로 독립적으로 재사용 할 수 있다.
- 둘이 서로 단단하게 결합되어 있지 않기 때문에 손쉽게 재사용 할 수 있다.
- 주제나 옵저버가 달라져도 서로에게 영향을 미치지는 않는다.
- 느슨하게 결합되어 있으므로 주제나 옵저버 인터페이스를 구현한다는 조건만 만족한다면 어떻게 고쳐도 문제가 생기지 않는다.
+디자인 원칙: 상호작용하는 객체 사이에는 가능하면 느슨한 결합을 사용해야 한다.
느슨하게 결합하는 디자인을 사용하면 상호의존성을 최소화 할 수 있기 때문에 변경 사항이 생겨도 무난히 처리할 수 있는 유연한 객체지향 시스템을 구축할 수 있다.
풀 방식
기본적인 옵저버 패턴은 주제가 변경되었을 때 옵저버에게 알리는 방식(푸시)으로 처리되었다. 하지만 이러한 방식은 변경 반영이 필요없는 옵저버에게도 알리게 된다.
이러한 문제가 존재한다면 옵저버가 필요할 때마다 데이터를 끌어오는 풀 방식을 통해 개선될 수 있다.
값이 변했다는 알림을 옵저버가 받았을 때 주제에 있는 게터 메소드를 호출해서 필요한 값을 당겨오도록 변경한다.
푸시와 풀은 구현 방법의 문제이지만 시간이 지남에 따라 애플리케이션이 계속 바뀌고 복잡해지므로, 대체로 옵저버가 필요한 데이터를 골라서 가져가도록 만드는 방법이 더 좋다.
주제에서 알림 보내기
|
|
옵저버에서 알림 받기
Observer
인터페이스에서update()
메소드에 매개변수가 없도록 서명을 바꾼다.
|
|
update()
메소드의 서명을 바꾸고Subject
구상 주제 메소드의 게터로 날씨를 받아오도록Observer
구상 클래스를 수정한다.
|
|
디자인 원칙
- 달라지는 부분을 찾아내고 달라지지 않는 부분과 분리한다.
- 옵저버 패턴에서 변하는 것은 주제의 상태와 옵저버의 개수, 형식이다.
- 옵저버 패턴에서는 주제를 바꾸지 않고도 주제의 상태에 의존하는 객체들을 바꿀 수 있다.
- 나중에 바뀔 것을 대비해 두면 편하게 작업할 수 있다.
- 구현보다는 인터페이스에 맞춰 프로그래밍한다.
- 주제와 옵저버에서 모두 인터페이스를 사용했다.
- 상속보다는 구성을 활용한다.
- 옵저버 패턴에서는 구성을 활용해서 옵저버들을 관리한다.
- 주제와 옵저버 사이의 관계가 상속이 아닌 구성으로 이루어진다.
기상 모니터링 애플리케이션
요구 사항 분석
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차적으로 다음과 같이 구현될 수 있다.
|
|
원칙으로 추가 코드 살펴보기
- 구체적인 구현(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 같은 다른 언어의 프레임워크에서도 옵저버 패턴을 많이 사용한다.