헤드 퍼스트 디자인 패턴 - 2장 Observer Pattern 옵저버 패턴 in Swift

koi·2022년 9월 30일
0
post-thumbnail

에릭 프리먼의 <헤드 퍼스트 디자인 패턴>을 읽고 정리한 후,
Swift 로 적용해보는 스터디입니다.

📖 2장 - 옵저버 패턴

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

기상 모니터링 애플리케이션은 기상 스테이션, WeatherData 객체, 디스플레이로 이루어져있습니다.

  1. 기상 스테이션 (실제 기상 정보를 수집)
  2. WeatherData 객체 (기상 스테이션으로 부터 오는 데이터를 추적)
  3. 사용자에게 현재 기상 조건을 보여주는 디스플레이

💡 이제 WeatherData 객체를 사용하여
현재 조건(온도, 습도, 압력), 기상 통계, 기상 예측
이렇게 세 항목을 보여주는 기상 모니터링 애플리케이션을 만들어야 합니다.


Weather Class

// 기상 관측값이 갱신될 때 마다 알려주기 위한 메소드
public void measurementsChanged() { }
  • measurementsChanged()현재 조건, 기상 통계, 기상 예측
    이렇게 세가지 디스플레이를 갱신할 수 있도록 구현해야합니다.

  • 그리고 시스템이 확장 가능해서 다른 개발자들이 별도의 디스플레이 항목을 만들 수 있도록 해야합니다.


WeatherData 객체 구현

public class WeatherData {
	public void measurementChanged() {
	float temp = getTemperture();
	float humidity = getHumidity();
	float pressure = getPressure();

	// 각 디스플레이 항목을 불러서 디스플레이를 갱신하도록 하는 메소드
	currentConditionDisplay.update(temp, humidity, pressure);
	statisticsDisplay.update(temp, humidity, pressure);
	forecastDisplay.update(temp, humidity, pressure);
	}
}

⛔️ 문제점

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

→ 바뀔 수 있는 부분을 캡슐화 해야합니다.


옵저버 패턴이란?

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

  • 신문 구독 매커니즘과 동일합니다.

  • 출판사를 주제 Subject , 구독자를 옵저버 Observer 라고 부릅니다.

  • 옵저버 객체들은 주제 객체를 구독하고(등록되어 있으며)
    주제의 데이터가 바뀌면 갱신 내용을 전달받습니다.

  • 옵저버를 등록하고 해제 할 수 있으며 옵저버가 주제가 될 수도 있습니다.

옵저버 패턴을 구현하는 방법에는 여러 가지가 있지만,
대부분 주제 인터페이스와 옵저버 인터페이스가 들어있는 클래스 디자인을 바탕으로 합니다.


느슨한 결합

두 객체가 느슨하게 결합되어 있다는 것은, 그 둘이 상호작용을 하긴 하지만 서로에 대해 잘 모른다는 것을 의미합니다. 옵저버 패턴에서는 주제와 옵저버가 느슨하게 결합되어 있는 객체 디자인을 제공한다.

  • 주제가 옵저버에 대해서 아는 건 옵저버가 특정 인터페이스(Observer 인터페이스)를 구현한다는 것 뿐입니다.
  • 옵저버는 언제든지 새로 추가하고 제거할 수 있습니다.
  • 새로운 형식의 옵저버를 추가하려고 할 때도 주제를 전혀 변경할 필요가 없습니다.
  • 주제나 옵저버가 바뀌더라도 서로한테 영향을 미치지 않습니다.

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

느슨하게 결합하는 디자인을 사용하면 변경사항이 생겨도 무난히 처리할 수 있는 유연한 객체지향 시스템을 구축할 수 있습니다.

객체 사이의 상호의존성을 최소화할 수 있기 때문입니다.


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

옵저버 패턴 직접 구현하기

public interface Subject {
	public void registerObserver(Observer o);
	public void removeObserver(Observer o);
}
public interface Observer {
	public void update(float temp, float humidity, float pressure);
}
public interface DisplayElement {
	public void display();
}

Subject 인터페이스 구현하기

public class WeatherData impelements Subject {
	private ArrayList observers; // Observer 객체들 저장
	private float temperature;
	private float humidity;
	private float pressure;
	
	public WeatherData() {
		observers = new ArrayList();
	}

	public void registerObserver(observer o) {
		observer.add(o);
	}
	
	public void removeObserver(observer o) {
		int i = observers.indexof(o);
		if (i >= 0) {
			observers.remove(i);
		}
	}

	public void notifyObservers() {
		for (int i = 0; i < observer.size(); i ++) {
			Observer observer = (Observer)observer.get(i);
			observer.update(temperature, humidity, pressure);
		}
	}

	public void measurementsChanged(float temperature,
    								float humidity,
                                    float pressure) {
		notifyObservers();
	}

	public void setMeasurements(float temperature,
    							float humidity,
                                float pressure) { 
		this.temperature = temperature;
		this.humidity = humidity;
		this.pressure = pressure;
		measurementsChanged();
	}

}

디스플레이스 항목 구현하기

public class CurrentConditionDisplay implements Observer, DisplayElement {
	private float temperture;
	private float humidity;
	private float pressure;

	public CurrentConditionsDisplay(Subject weatherData) {
		this.weatherData = wheaterData;
		weahterData.registerObserver(this);
	}

	public void update(float temperture, float humidity, float pressure) {
		this.temperture = temperture;
		this.humidity = humidity;
		this.pressure = pressure;
		display();
	}

	public void display() {
		System.out.printIn("현재 조건은" + temperture + humidity + humidity);
	}
}
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);

    }
}

옵저버 Pull vs Push 방식

지금 만들어 놓은 WeatherData 디자인은 하나의 데이터만 갱신해도 되는 상황에서도 update 메소드에 모든 데이터를 보내도록 되어 있습니다.

하지만 풍속 같은 새로운 데이터가 추가되면, 대부분의 update 메소드에서 풍속 데이터를 쓰지 않더라도 모든 디스플레이에 있는 update 메소드를 바꿔야 할 수도 있습니다.

옵저버 패턴에는 두가지 방식이 있습니다.

  • PUSH 방식 : 주제의 내용이 변경될 때마다 구독자에게 알려주는 방식
  • PULL 방식 : 구독자가 필요할 때마다 주제에게 정보를 요청하는 방식

사실 주제가 옵저버로 데이터를 보내는 PUSH 방식을 사용하거나, 옵저버가 주제로부터 데이터를 당겨오는 PULL 방식을 사용하는 방법 중 어느 하나를 선택하는 일은 구현 방법의 문제라고 볼 수 있습니다.


Pull 방식 옵저버로 바꾸기

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

    }
    
 
 // Observer.java
 public interface Observer {
    void update();
}
  • 옵저버의 update 메소드를 인자 없이 호출하도록 수정합니다.

// CurrentConditionDisplay.java
public class CurrentConditionsDisplay implements Observer, DisplayElement {
   private float temperature;
   private float humidity;
   private WeatherData weatherData;

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

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

       display();
   }

   public void display() {
       System.out.println("현재 상태:  온도 "+temperature+"F, 습도 "+humidity+"%");
   }
}
  • 생성자에서 weatherData를 받은 후 getter 메소드를 사용하여 정보를 가져옵니다.

옵저버 패턴 in Swift

옵저버 패턴 직접 구현하기


Subject 인터페이스 구현하기


디스플레이 구현하기

결과

기상정보가 바뀌면 옵저버들에게 notify되면서 display 되고,
정상적으로 해제되는 것이 보이네요!


옵저버 패턴은 언제 쓸까?

참고

📢 다른 객체의 상태가 변경될 때마다 어떤 행동을 하고 싶다면 옵저버 패턴을 사용하면 된다.

이러한 패턴은 iOS에서는 ViewController에 Observer(Subscriber)가 있고, Model에 Subject(Publisher)가 있는 MVC 패턴에서 사용할 수 있습니다.

이를 통해 Model은 ViewController의 타입에 대해 알 필요 없이 상태가 변경될 때마다 이를 ViewController에 전달할 수 있습니다.

따라서 여러 개의 ViewController가 하나의 Model의 변경사항을 사용할 수 있게 됩니다.


옵저버 패턴의 결과

장점

  • 개방 폐쇄 원칙을 지킬 수 있습니다.
  • Subject(Publisher)의 코드를 수정하지 않고 새로운 Observer(Subscriber) 클래스를 추가할 수 있습니다. (반대도 가능)
  • 런타임에서 객체간 관계를 설정할 수 있습니다.

단점

  • Observer(Subscriber)에게 알림이 가는 순서는 보장하지 않습니다.
  • Observer, Subject의 관계가 잘 정의되지 않으면 원하지 않는 동작이 발생할 수도 있습니다.

옵저버 패턴과 발행 구독 패턴과 차이점

참고

발행 구독 패턴

발행 구독 패턴은 비동기 메세징 패러다임입니다.

  1. 발행자 메시지의 수신자가 정해져 있지 않음
  2. 메시지는 정해진 범주에 따라서 구독을 신청한 수신자에게 전달됨
  3. 수신자는 발행자에 대한 정보 없이, 원하는 메시지를 수신할 수 있음
  4. 메시지 큐 패러다임과 마치 형제같은 관계로, 대형 메시지 지향 미들웨어 솔루션의 일부

공통점

  • Subject(Publisher)Observer(Subscriber) 에게 변화를 알려주는 관계

차이점

  • 발행 구독 패턴에서 PublisherSubscriber 사이에는 브로커(Broker) 혹은 이벤트 버스(Event Bus)로 불리는 계층이 존재
  • 가장 큰 차이는 송신자와 수신자가 직접적으로 메시지를 송수신하지 않는다는 것
    서로의 존재를 몰라도 Event Channel에 의해 전달된다.
profile
Don't think, just do 🎸

0개의 댓글