Books/헤드퍼스트 디자인 패턴

2장. 헤드퍼스트 디자인 패턴 - 옵저버 패턴

유로띠 2022. 5. 2. 16:02
반응형

안녕하세요 😀
 유로띠 입니다 😉

 

 

 

 

 

헤드퍼스트 디자인패턴


TIL (Today I Learned)

3줄 요약

 

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

✏️  일대다의 관계를 정의

✏️  쉽게 확장할 수 있도록 push 방식보다는 pull 방식을 사용해 보자.

 

CHAPTER 2

오늘 읽은 범위: 2장. 객체들에게 연락 돌리기 - 옵저버 패턴 

 

😉 책에서 기억하고 싶은 내용을 써보세요.

 

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

기상 스테이션(실제 기상 정보를 수집하는 물리 장비)

WeatherData 객체(기상 스테이션으로부터 오는 정보를 추적하는 객체)

사용자에게 현재 기상 조건을 보여 주는 디스플레이 장비

 

 

Weather 객체로 현재 조건, 기상 통계, 기상 예보, 이렇게 3가지 항목을 디스플레이 장비에서 갱신해 가면서 보여 주는 애플리케이션을 만들어야 합니다. (p.75)

 

WeatherData 클래스 살펴보기

weatherData에서 온도, 습도, 기압 값을 새로 받을 때마다 호출되는 measurementsChanged() 메소드를 살펴봅시다.

 

WeatherData.java

/*
 * 기상 관측값이
 * 갱신될 때마다
 * 이 메소드가 호출됩니다.
 *
 */
public void measurementsChanged() {
	// 코드가 들어갈 자리
}

 

구현 목표

디스플레이를 구현하고 새로운 값이 들어올 때마다, 즉 measurementsChanged() 메소드가 호출될 때마다 WeatherData에서 디스플레이를 업데이트해야 한다는 사실까지는 파악했습니다. 그러려면 어떻게 해야 할까요? 우리가 하려는 일이 무엇인지조금 더 따져 봅시다. (p.77)

 

✅  WeatherData 클래스에는 3가지 측정값(온도, 습도, 기압)의 getter 메소드가 있습니다.

✅  새로운 기상 측정 데이터가 들어올 때마다 measurementsChanged() 소드가 호출됩니다(이 메소드가 어떤 식으로 호출되는지 모르며, 사실 알 요도 없으며, 아무튼 그냥 그 메소드가 호출된다는 사실만 알고 있습니다).

✅  기상 데이터를 사용하는 디스플레이 요소 3가지를 구현해야 합니다. 하나는 현재 조건 디스플레이, 다른 하나는 기상 통계 디스플레이, 마지막은 기상예보 디스플레이입니다. WeatherData에서 새로운 측정값이 들어올 때마다 디스플레이를 갱신해야 합니다.

✅  디스플레이를 업데이트하도록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); 
    }
}

 

어떤 걸 위반하고 어떤 걸 제대로 따랐을까요? 특히 변화가 생겼을 때 이 코드에 어떤 영향을 끼칠지도 생각해 봅시다. 코드를 살펴보면서 자세히 따져 볼까요? (p.79)

 

currentConditionsDisplay.update(temp, humidity, pressure);
statisticsDisplay.update(temp, humidity, pressure);
forecastDisplay.update(temp, humidity, pressure);

🔴  구체적인 구현에 맞춰서 코딩했으므로 프로그램을 고치지 않고는 다른 디스플레이 항목을 추가하거나 제거할 수 없습니다.

🔴  디스플레이 항목과 데이터를 주고받는 데 적어도 공통된 인터페이스를 사용하고 있는 것 같긴 하군요. 모두 온도, 습도, 기압 값(temp, humidity, pressure)을 받아들이는 update() 메소드를 가지고 있으니까요.

🔴  실행 중에 디스플레이를 더하거나 빼려면 어떻게 해야 할까요?

🔴  바뀌는 부분을 캡슐화하지 않았습니다.

 

 

옵저버 패턴 이해하기

신문사(주제: subject) + 구독자(옵저버: observer) = 옵저버 패턴

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

 

주제와 옵저버로 일대다 관계가 정의됩니다.

옵저버는 주제에 딸려 있으며 주제의 상태가 바뀌면 옵저버에게 정보가 전달됩니다. (p.81)

 

 

 

 

느슨한 결합의 위력

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

느슨한 결합(Loose Coupling)은 객체들이 상호작용할 수는 있지만, 서로를 잘 모르는 관계를 의미합니다. 앞으로 배우게 되겠지만 느슨한 결합을 활용하면 유연성이 아주 좋아집니다. 옵저버 패턴은 느슨한 결합을 보여주는 훌륭한 예입니다.

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

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

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

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

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

 

기상 스테이션 설계하기

 

Observer - 주제에서 옵저버에게 갱신된 정보를 전달하는 방법을 제공합니다.

CurrentConditionsDisplay / statisticsDisplay / ForecastDisplay - WeatherData 객체로부터 얻은 측정값을 보여줍니다. (p.93)

 

 

 

기상 스테이션 구현하기

 

 

✅   Subject(주제)

옵저버를 등록(register), 삭제(remove)하는 역할을 합니다.

notifyObservers - 주제의 상태가 변경되었을 때 모든 옵저버에게 변경 내용을 알리기 위한 메소드입니다.

package ObserverPattern;

public interface Subject {

    public void registerObserver(Observer o);
    public void removeObserver(Observer o);
    public void notifyObservers();
}

 

✅   Observer (옵저버)

 

기상정보가 변경되었을 때 옵저버에게 전달되는 상태 값들(temperature, humidity, pressure)입니다.

package ObserverPattern;

public interface Observer {

    void update(float temperature, float humidity, float pressure);
}

 

✅   DisplayElement

package ObserverPattern;

public interface DisplayElement {

    public void display();
}

 

 

✅   WeatherData

 

weatherData가 최신으로 반영되면(setWeatherData) 갱신되었다고 measurementsChanged 메서드에 알립니다.

measurementsChanged 메서드는 갱신된 측정값을 옵저버에게 알립니다.

notifyObservers 메서드는 등록된 모든 옵저버에게 상태 변화를 알려줍니다.

package ObserverPattern;

import java.util.ArrayList;
import java.util.List;

public class WeatherData implements Subject {

    private float temperature;
    private float humidity;
    private float pressure;
    private List<Observer> observers;


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

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

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

    @Override
    public void notifyObservers() {
        for(Observer observer: observers) {
            observer.update(temperature, humidity, pressure);
        }

    }

    public void measurementsChanged() {
        notifyObservers();
    }

    public void setWeatherData(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;

        measurementsChanged();
    }

    public float getTemperature() {
        return this.temperature;
    }

    public float getHumidity() {
        return this.humidity;
    }

    public float getPressure() {
        return this.pressure;
    }
}

 

✅  CurrentConditionsDisplay

package ObserverPattern.display;

import ObserverPattern.DisplayElement;
import ObserverPattern.Observer;

public class CurrentConditionsDisplay implements Observer, DisplayElement {

    private float temperature;
    private float humidity;

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

        display();
    }

    @Override
    public void display() {
        System.out.println("현재 상태:  온도 "+temperature+"F, 습도 "+humidity+"%");
    }
}

 

 

✅  WeatherStation

 

weatherData 객체를 생성합니다.

디스플레이를 생성하고 옵저버를 등록(registerObserver)합니다.

setWeatherData를 통해 새로운 기상 측정값을 업데이트합니다.

package ObserverPattern;

import ObserverPattern.display.CurrentConditionsDisplay;
import ObserverPattern.display.StatisticsDisplay;

public class WeatherStation {
    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();
        Observer currentConditionsDisplay = new CurrentConditionsDisplay();
        Observer statisticsDisplay = new StatisticsDisplay();


        weatherData.registerObserver(currentConditionsDisplay);
        weatherData.setWeatherData(3, 5, 7);

        System.out.println("통계 디스플레이를 추가합니다.");
        weatherData.registerObserver(statisticsDisplay);
        System.out.println("기상 데이터가 업데이트 됩니다.");
        weatherData.setWeatherData(20, 30, 80);

        System.out.println("현재 상태 디스플레이를 제거합니다.");
        weatherData.removeObserver(currentConditionsDisplay);
        weatherData.setWeatherData(25, 30, 80);

    }
}

 

//console.log
현재 상태:  온도 3.0F, 습도 5.0%
통계 디스플레이를 추가합니다.
기상 데이터가 업데이트 됩니다.
현재 상태:  온도 20.0F, 습도 30.0%
평균/최고/최저 온도 = 20.0/20.0/20.0
현재 상태 디스플레이를 제거합니다.
평균/최고/최저 온도 = 22.5/25.0/20.0

 

 

옵저버 데이터 방식의 푸시(push) vs 풀(pull)

지금 만들어 놓은 WeatherData 디자인은 하나의 데이터만 갱신해도 되는 상황에서도 update 메소드에 모든 데이터를 보내도록 되어 있습니다. 하지만 풍속 같은 새로운 데이터가 추가되면 대부분의 update 메소드에서 풍속 데이터를 쓰지 않더라도 모든 디스플레이에 있는 update 메소드를 바꿔야 하지 않을까요?

사실 주제가 옵저버로 데이터를 보내는 푸시(push)를 사용하거나 옵저버가 주제로부터 데이터를 당겨오는 풀(pull)을 사용하는 방법 중 어느 하나를 선택하는 일은 구현 방법의 문제라고 볼 수 있습니다. (p.104)

 

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

옵저버의 update 메소드를 인자 없이 호출하도록 수정합니다.

// WeatherData.java
@Override
    public void notifyObservers() {
        for(Observer observer: observers) {
            observer.update();
        }

    }
    
    
 
 // Observer.java
 public interface Observer {

    void update();
}

 

✅  CurrentConditionsDisplay

 

생성자에서 weatherData를 받은 후 getter 메소드를 사용하여 정보를 가져옵니다.

package ObserverPattern.display;

import ObserverPattern.DisplayElement;
import ObserverPattern.Observer;
import ObserverPattern.WeatherData;

public class CurrentConditionsDisplay implements Observer, DisplayElement {

    private float temperature;
    private float humidity;
    private WeatherData weatherData;

    public CurrentConditionsDisplay(WeatherData weatherData) {
        this.weatherData = weatherData;
    }

    @Override
    public void update() {
        this.temperature = weatherData.getTemperature();
        this.humidity = weatherData.getHumidity();

        display();
    }

    @Override
    public void display() {
        System.out.println("현재 상태:  온도 "+temperature+"F, 습도 "+humidity+"%");
    }
}

 

 

핵심 정리

✅  옵저버 패턴은 객체들 사이에 일대다 관계를 정의합니다.

✅  주제는 동일한 인터페이스를 써서 옵저버에게 연락을 합니다.

✅  Observer 인터페이스를 구현하기만 하면 어떤 구상 클래스의 옵저버라도 패턴에 참여할 수 있습니다.

✅  주제는 옵저버들이 Observer 인터페이스를 구현한다는 것을 제외하면 옵저버에 관해 전혀 모릅니다. 따라서 이들 사이의 결합은 느슨한 결합입니다.

✅  옵저버 패턴을 사용하면 주제가 데이터를 보내거나(푸시 방식) 옵저버가 데이터를 가져올(풀 방식) 수 있습니다(일반적으로 풀 방식이 더 옳은옳은 방식이라고 간주합니다).

 

 

🧐 오늘 익은 소감은? 떠오르는 생각을 가볍게 적어보세요

✅  실용주의 프로그래머의 책(5장. 구부러지거나 부러지거나)에서도 가능한 한 느슨하고 유연한 코드를 작성해야 한다고 한다. 느슨한 결합(Loose Coupling)은 코드의 변경을 유연하게 만든다.

 

5장. 구부러지거나 부러지거나

안녕하세요 😀  유로띠 입니다 😉 실용주의 프로그래머 TIL (Today I Learned) 3줄 요약 ✏️ 구부러지는 유연한 코드를 작성하자. ✏️ 묻지 말고 말하라, TDA ✏️ 내 코드가 도도새가 되지 말자. D

msyu1207.tistory.com

 

 

🙋‍♂️ 궁금한 내용이 있거나, 잘 이해되지 않는 내용이 있다면 적어보세요.

⭐️  출판-구독(Publish-Subscribe) 패턴과 옵저버 패턴의 차이?

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

 

 

 

 

반응형