[헤드퍼스트 디자인패턴] Chapter 02 - 옵저버 패턴

뚱이·2023년 4월 23일
0
post-thumbnail

옵저버 패턴 (Observer Pattern)

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

1. 옵저버 패턴

(1) 옵저버 패턴이란?

옵저버 패턴 = 주제 (subject) + 옵저버 (observer)

마치 '신문사와 구독자', '헤드헌터와 개발자' 같다.
구독을 하면 정보가 업데이트 될 때마다 연락이 가고, 구독을 해지하면 더 이상 연락이 오지 않는다.

약간 아이돌 버블과도 비슷한 거 같다는 생각을 했다 🤔
구독하면 아이돌의 톡을 실시간으로 받을 수 있고,
구독 해지하면 더 이상 톡을 받지 못 하고

다음은 책에서 비유한 옵저버 패턴이다.


(2) 옵저버 패턴의 구조

옵저버 패턴의 클래스 다이어그램은 다음과 같다.

  • Subject
    나(주제)를 구독하는 친구들(옵저버)을 관리해야 한다.
    그래서 필요한 메소드는 다음과 같다.
    • registerObserver() : 나를 구독하는 친구 추가
    • removeObserver() : 친구 한 명이 나를 구독 해지
    • notifyObservers() : 나를 구독하는 친구들한테 업데이트 알림
  • Observer
    구독을 하는 이유는 업데이트 정보를 바로 받기 위해서 !
    그러므로 Observer는 update 메소드가 꼭 필요하다.
    얘의 존재 이유는 update를 위해서
    • update()
  • ConcreteSubject
    Subject 인터페이스를 구현한 구상 클래스가 되겠다.
    인터페이스에 정의된 3가지 메소드를 구현해야 하고,
    그 외 본인에게 필요한 메소드를 작성하면 된다.
  • ConcreteObserver
    Observer 인터페이스를 구현한 구상 클래스가 되겠다.
    얘는 update() 메소드를 구현하면 되고,
    얘 또한 자기한테 필요한 메소드를 추가로 작성하면 된다.

(3) 느슨한 결합

느슨한 결합 이란?
: 객체들이 상호작용할 수는 있지만, 서로를 잘 모르는 관계를 의미

느슨한 결합의 좋은 예시 중 하나가 옵저버 패턴 !

다시 버블을 생각해보자 🤔
버블을 안 해봐서 잘 모르지만 내가 알고있는 걸 바탕으로 하자면,
아이돌 입장에선 나를 구독하는 팬들이 있는 건 알지만
(팬 입장에선 슬프게도) 팬 한 명 한 명이 누군진 모른다 ..
팬들끼리도 나 외엔 누가 구독을 하고 있는지 알 수 없다
-> 이게 바로 느슨한 결합 이다.

만약에 버블이 아주아주 끈끈한 결합 관계였다면 어떨까 🤔
실명제로 운영이 돼서,
아이돌도 나에게 어떠한 말을 한 사람이 누군지 알고 ,,
아이돌에게 열렬한 사랑의 메세지를 보내는 팬이 내 친구고 ,,
내가 보낸 메세지도 친구가 알고 ,,
부모님이 알고 ,, ,,
교수님이 알고 ,, ,, ,,
직장 상사가 알고 ,, ,, ,, ,,
완전 최악이다 ! !
아주 인간관계가 민망해질 거 같다

이게 바로 단단한 결합의 단점이자 느슨한 결합의 장점이다.
느슨한 결합을 통해 유연한 디자인을 만들 수 있고,
서로 잘 모르기 때문에 변화에 더 잘 대응할 수 있다.
어려운 말로 표현하자면, 객체 사이의 상호의존성을 최소화할 수 있따.
즉, 변경 사항이 생겨도 무난히 처리할 수 있는 유연한 객체지향 시스템을 구축할 수 있다.

옵저버 패턴에서 느슨한 결합을 만드는 방법은 다음과 같다.

  • 주제는 옵저버가 특정 인터페이스(Observer 인터페이스)를 구현한다는 사실만 안다
    Observer 인터페이스를 구현한 구상 클래스가 뭔지, 걔네가 무엇을 하는지 상세정보는 알 필요가 없다!
  • 옵저버는 언제든지 새로 추가/삭제 할 수 있다
    당연한 말이다.
  • 새로운 형식의 옵저버를 추가할 때도 주제를 변경할 필요가 전혀 없다
    이것도 당연한 말이다.
  • 주제와 옵저버는 서로 독립적으로 재사용할 수 있다
    얘네들은 앞에서 말했다시피 느슨한 결합이기 때문에 서로 뭘 하는지 알 수가 없다. 다른 용도로 활용할 일이 있으면 손쉽게 재사용하면 된다.
  • 주제나 옵저버가 달라져도 서로에게 영향을 미치지 않는다
    Subject, Observer 인터페이스를 구현한다는 조건만 만족한다면, 각자에게 필요한 메소드를 추가하든 수정하든 뭘하든 괜찮다!

여기서 나오는 디자인 원칙

디자인 원칙

상호작용하는 객체 사이에는 가능하면 느슨한 결합을 사용해야 한다.



2. 적용해보자: Weather-O-Rama

(0) '기상 모니터링 어플리케이션'을 만들자

[조건]

  1. WeatherData 객체를 바탕으로 함.
  2. WeatherData 객체: 현재 기상 조건(온도, 습도, 기압)을 추적
  3. '기상 모니터링 어플리케이션': 3개의 항목을 화면에 표시
  4. 화면 종류는 3가지: 현재 조건, 기상 통계, 간단한 기상 예보
  5. 3개의 항목들은 WeatherData 객체에서 최신 측정치 수집할 때마다 실시간 갱신
  6. 다른 개발자가 직접 화면 만들어서 넣을 수 있도록 확장성 있어야 함.

[WeatherData 클래스]

[구현 목표]

  1. WeaterData 클래스: 측정값(온도, 습도, 기압)에 대한 getter 메소드 존재
  2. 측정값 업데이트 될 때마다 measurementsChanged() 메소드 호출
  3. 디스플레이 3가지 구현
    (1) 현재 조건 디스플레이
    (2) 기상 통계 디스플레이
    (3) 기상 예보 디스플레이
  4. 측정값 업데이트 될 때마다 디스플레이도 갱신 -> measurementsChanged() 메소드에 반영
  5. 확장성 고려: 다른 개발자가 새로운 디스플레이 추가 시

(1) measurementsChanged() 메소드에 코드 추가

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);
   }
   
   // 기타 메소드
   
}

위 코드는 잘 짠 코드일까 ???
아니다.

위 코드의 문제점은 다음과 같다.

  • 인터페이스가 아닌 구체적인 구현을 바탕으로 코딩했다.
  • 새로운 디스플레이 항목이 추가될 때마다 코드를 변경해야 한다.
  • 실행 중에 디스플레이 항목을 추가하거나 제거할 수 없다.
  • 바뀌는 부분을 캡슐화하지 않았다.

1장에서도 나온 거지만, 항상 바뀌는 부분과 바뀌지 않는 부분을 생각해야 한다.

여기서도 생각해보자 🤔
구현해야 할 디스플레이들은 저 3개가 끝일까?? 더이상 안 바뀔까??
update 항목들은 저 3개가 끝일까?? 새로운 측정값이 추가될 수도 있지 않을까??

이제 위 코드에 옵저버 패턴을 적용해보자.


(2) 옵저버 패턴 적용하기

옵저버 패턴을 적용해 다이어그램을 그리면 위와 같다.

WeatherData 클래스가 '일(one)',
디스플레이 요소가 '다(many)'
에 해당하므로 Subject와 Observer를 이렇게 구현한 것이다.

또한 디스플레이 클래스들에서 공통적으로 사용되는 메소드는
update()display() 인데, 이 또한 각각 캡슐화시켰다.
update()Observer 인터페이스에서 담당하였기 때문에 패스하고,
display()DisplayElement 인터페이스를 만들어 담당하였다.

구체적인 코드는 다음과 같다.
[인터페이스]

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 WeaterData() {
    	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(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 메소드
 
}

[구상 클래스 - Observer]

public class CurrentConditionDisplay implements Observer, DisplayElement {
	private float temperature;
    private float humidity;
    private WeatherData weaterData;
    
    public CurrentConditionDisplay(WeatherData weaterData) {
    	this.weaterData = weatherData;
        weatherData.registerObserver(this);
    }
    
    public void update(float temperature, float humidity, float pressure) {
    	this.temperature = temperature;
        this.humidity = humidity;
        display();
    }
    
    public void display() {
    	// 디스플레이 코드
        // ex) System.out.println(~~);
    }
}

[테스트 클래스]

public class WeatherStation {
	
    public static void main(String[] args) {
    	WeatherData weaterData = new WeatherData();
        
        CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
        StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
        ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
        
        // 측정값 업데이트
        weatherData.setMeasurements(80, 65, 30.4f);
        weatherData.setMeasurements(82, 70, 29.2f);
        weatherData.setMeasurements(78, 90, 29.2f);
    }
}

이렇게 코드를 작성하면 옵저버 패턴을 잘 적용한 것이다 !

그런데,
이 코드에는 이제 문제가 없을까 ?

슬프게도 문제가 있다.

CurrentConditionDisplay 클래스의 update() 메소드를 봐보자.
파라미터로 받은 건 3개의 측정값이다.
그러나, CurrentConditionDisplay 클래스에는 온도습도 정보만 저장하므로 업데이트 할 때도 두 개의 데이터만 있으면 된다.

-> 이건 풀(pull) 방식을 사용하면 된다


(3) 풀(pull) 방식 적용하기

  • 푸시(push) : 주제가 옵저버에게 상태를 알리는 방식
  • 풀(pull) : 옵저버가 주제로부터 상태를 끌어오는 방식

(2)에서 구현한 코드는 푸시(push) 방식의 코드이다.
그렇기 때문에 사용하지 않는, 필요하지 않은 정보도 받게 된다.

그러나 풀(pull) 방식을 사용하면 옵저버가 필요한 데이터만 가져올 수 있다.

둘 중 어느 방식을 택하느냐, 이건 정답이 정해져 있지는 않다.
그러나 대체로 옵저버가 필요한 데이터를 골라서 가져가는 풀(pull) 방식을 사용하는 게 더 좋긴 하다.
나중에 더 쉽게 확장할 수 있기 때문이다.

풀(pull) 방식으로 코드를 수정하면 다음과 같다.

[Subject]

// 구상 클래스
public void notifyObservers() {
	for (Observer observer : observers) {
    	observer.update();
    }
}

[Observer]

// 인터페이스
public interface Observer() {
	public void update();
}

// 구상 클래스
public void update() {
	this.temperature = weatherData.getTemperature();
    this.humidity = weatherData.getHumidity();
    display();
}

파라미터를 지정하지 않고,
옵저버에서 필요한 데이터를 getter 함수를 통해 가져오면 된다.



3. 기타

  • 스윙 은 다른 여러 GUI 프레임워크와 마찬가지로 옵저버 패턴을 많이 사용한다.
  • RxJava, 자바빈, RMI코코아, 스위프트, 자바스크립트 등 다른 언어의 프레임워크에서도 옵저버 패턴을 많이 사용한다.
  • 옵저버 패턴은 여러 개의 주제와 메시지 유형이 있는 복잡한 상황에서 사용하는 출판-구독 패턴 과 친척이다.
  • 옵저퍼 패턴은 자주 쓰이는 패턴으로, 모델-뷰-컨트롤러(MVC) 를 배울 때 다시 등장한다.


4. 정리

객체지향 기초

  • 추상화
  • 캡슐화
  • 다형성
  • 상속

객체지향 원칙

  • 바뀌는 부분은 캡슐화 한다.
  • 상속보다는 구성을 활용한다.
  • 구현보다는 인터페이스에 맞춰서 프로그램이한다.
  • 상호작용하는 객체 사이에서는 가능하면 느슨한 결합을 사용해야 한다.

객체지향 패턴

전략 패턴

전략 패턴은 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 한다.
전략패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있다.

옵저버 패턴

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

0개의 댓글