Development Project

[ 헤드퍼스트 디자인패턴 - 11/08 ] Chap2. 옵저버 패턴 본문

Web Tech/Design Pattern

[ 헤드퍼스트 디자인패턴 - 11/08 ] Chap2. 옵저버 패턴

나를 위한 시간 2022. 11. 8. 20:29

헤드퍼스트 디자인패턴 개정판을 읽은 후 터득한 내용을 정리해 올린 포스팅임을 밝힙니다.


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

 업무 계약 체결서에 따르면,

WeatherData 객체는 현재 기상 조건(온도, 습도, 기압)을 추적하여 가지고있는데,
이 객체를 바탕으로 현재 조건, 기상통계, 간단한 기상예보를 표시하는 애플리케이션을 제작요청함.

위 객체의 값들은 최신 측정치를 수집할때마다 실시간으로 갱신되어야 한다고 덧붙임.

 

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

해당 회사에서 제공한 부분 + 만들어야할 부분 + 후에 확장해야할 부분 들을 파악해야함

해당 시스템은 실제 기상정보를 수집하는 물리장비 / WeatherData객체 / 사용자에게 보여줄 장치로 구성

 

WeatherData객체는?

구조 코드
WeatherData

getTemperature()
getHumidity()
getPressure()
measurementsChanged()
// 기타 메소드
// 게터들은 기기에서 가져오는 부분이므로 생략

public void measurementsChanged(){
    // 코드가 들어갈 자리
}

 

단순하게 구현 해보기

public class WeatherData{

	// 인스턴스 변수 선언
    
    public void measurementsChanged(){
    	// 온도, 습도, 기압을 게터로 받아옴
    	float temp = getTemperature();
        float humidity = getHumidity();
        float pressure = getPressure();
        
        // 각각 현재 조건, 기상통계, 간단한 기상예보를 보여주는 화면에 보내기
        currentComditionsDisplay.update(temp, humidity, pressure);
        statisticsDisplay.update(temp, humidity, pressure);
        forecastDisplay.update(temp, humidity, pressure);

간단하게 전부 구현된 것 처럼 보이지만 사실상 큰 문제들이 있다.

 

① 인터페이스가 아닌 구체적 표현을 바탕으로 코딩하고 있다.

=> 해당 디스플레이 이름처럼 구체적 표현을 사용했기 때문에 프로그램을 고치지 않고서는 다른 디스플레이 항목을 추가하거나 삭제가 불가하다. 즉, 유연하지 않은 강한 결합이기 때문에 변화에 용이하지 않으므로 지양해야한다.

 

② 새로운 디스플레이 항목이 추가될 때마다 코드를 변경해야한다.

=> 해당 디스플레이로 정보를 제각각 보내고 있으므로, 더 추가하거나 삭제하는 등의 처리가 필수적이다. 

 

③ 실행 중에 디스플레이 항목을 추가하거나 제거할 수 없다.

=> 세터가 없기때문에 당연하게도 불가

 

④ 바뀌는 부분을 캡슐화하지 않았다.

=> 온도, 습도, 기압을 float형태로 받고 그대로 여러 디스플레이로 넘겨주고있다.

 

해당 단점들을 극복하기 위한 패턴이 바로 옵저버 패턴이다!

 

 

 

 옵저버 패턴 이해하기

옵저버 패턴을 이해하기 위해서는 크게 "구독 형태"를 생각하면 된다.

책에서는 신문을 예로 들었지만, 좀더 이해하기 쉽도록 Youtube로 설명하고자 한다.

 

Youtube의 구독형태는?

① 유튜브에 가입한 유튜버들이 영상을 찍고 열심히 편집해 업로드한다.

② 유저가 원하는 유튜버 채널에 구독을 누르면, 새로운 영상이 업로드 될 경우 알림이 오고 구독페이지에서 바로 접근이 가능하다.

③ 해당 유튜버의 영상을 더이상 보길 원치않는다면, 구독을 해지한다.

④ 유튜버가 계정을 삭제하지 않는 한, 사용자들은 꾸준히 구독하거나 해지하는 작업을 수행한다.

 

옵저버 패턴의 형태를 Youtube에 비춰보면?

※ Youtube == 주제(Subject) / 구독자 == 옵저버(observer)

1 Youtube에서 영상들을 관리합니다.
Subject(주제)에서 중요한 데이터를 관리합니다
2 영상이 바뀌면 구독자들에게 알림이 갑니다.
Subject 데이터가 바뀌면 옵저버에게 그 소식이 전해집니다.
3 각 구독자들은 Youtube를 구독하고 있으며(== Youtube 사용자로 등록) Youtube의 데이터가 바뀌면 갱신 내용을 알림이나 구독페이지로 볼 수 있습니다.
옵저버 객체들은 주제를 구독하고 있으며(== 주제 객체에 등록) 주제 데이터가 바뀌면 갱신 내용을 전달받습니다.
4 구독자가 아닌 사용자는 따로 연락이 오지 않습니다.
옵저버에 속하지 않은 객체는 Subject 데이터가 바뀌어도 아무런 연락도 받지 못합니다.

 

옵저버 패턴의 작동원리 정리

① 옵저버에 소속되지 않은 객체가 주제에게 옵저버 요청

② 주제가 해당 객체를 구독목록에 갱신하고 옵저버로 등록

③ 주제의 값이 갱신되면 모든 옵저버가 연락을 받음

④ 옵저버에 소속된 객체가 주제에게 탈퇴요청

⑤ 주제가 요청을 받아들여 옵저버 집합에서 제외

⑥ 나간 객체는 더이상 옵저버가 아니므로 연락을 받지 못함

 

옵저버 패턴(Observer Pattern)의 정의

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

옵저버 패턴의 구조

Subject [인터페이스]

registerObserver()
removeObserver()
notifyObservers()
옵저버
--------->
Observer [인터페이스]

update()


 

ConcreteSubject

registerObserver() { ... }  // 옵저버 등록
removeObserver() { ... }  // 옵저버 제거
notifyObservers() { ... }  // 모든 옵저버에게 연락

getState()
setState()
주제
<---------
ConcreteObserver

update()

옵저버 패턴에서는 주제가 상태를 저장하고 제어하기 때문에, 상태가 들어있는 객체는 하나만 있을 수 있음.

반면에, 옵저버는 상태를 사용하지만 소유할 필요가 없음 ( 구독자가 영상을 볼 뿐이지, 가져야하는 것은 아님 )

즉, 옵저버는 주제에서 상태가 바뀌었다는 사실을 알려주길 기다리는 의존적 성질을 가지게 되므로 일대다관계 성립한다!

 

 

 

느슨한 결합을 위한 옵저버 패턴

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

=> 유연성을 얻기위해 필수적임! 

 

어떻게 옵저버 패턴이 느슨한 결합을 만들까

① 주제는 옵저버가 특정 인터페이스를 구현한다는 사실만 안다.

=> 옵저버의 구상클래스가 무엇인지, 옵저버가 무엇을 하는지 알 필요도 없음

Youtube에서 해당 구독자가 어디서 뭘 하는 사람인지 알 이유가 없다는 것과 같은 말

 

② 옵저버는 언제든지 새로 추가 가능하다.

=> 주제는 Observer 인터페이스를 구현하는 객체의 목록에만 의존하므로 언제든지 새로운 옵저버를 추가할 수 있음

 

③ 새로운 형식의 옵저버를 추가할 때도 주제를 변경할 이유가 없다.

=> 옵저버 인터페이스만 구현한다면 어떤 객체에게도 연락이 가능하기 때문!

 

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

=> 서로 단단히 결합되어있지 않기때문에, 다른 용도로 활용할 일이 있다해도 손쉽게 재사용 가능

 

⑤ 주제나 옵저버가 달라져도 서로에게 영향을 미치지는 않는다.

=> 독립적이기 때문에!

 

단단하게 짠 바구니가 유연하게 짠 바구니보다 부서지기 쉽다!

Design Principle. 상호작용하는 객체 사이에는 가능하면 느슨한 결합을 쓰자

< 변경사항이 생겨도 무난히 처리할 수 있는 유연한 객체지향 시스템을 구축할 수 있다! >




 기상 스테이션 설계 및 구현하기

 

Head First Design Patterns | WickedlySmart.com

Welcome to Head First Design Patterns At any given moment, somewhere in the world someone struggles with the same software design problems you have. You know you don’t want to reinvent the wheel (or worse, a flat tire), so you look to Design Patterns–t

wickedlysmart.com

 

인터페이스 구현

// Subject.java
public interface Subject {
	// 옵저버를 등록
	public void registerObserver(Observer o);
	// 옵저버를 제거
	public void removeObserver(Observer o);
	// 모든 옵저버에게 연락하는 메소드
	public void notifyObservers();
}

// Observer.java
public interface Observer {
	// 옵저버에게 측정한 값들 전달
	public void update(float temp, float humidity, foat pressure);
}

// DisplayElement.java
public interface DisplayElement {
	// 화면에 출력하기위한 메소드
	public void display();
}

 

인터페이스에 따른 구상클래스 구현 - Subject

// SimpleSubject.java
public class SimpleSubject implements Subject {
	// 옵저버 객체들을 저장하는 리스트
	private List<Observer> observers;
	private int value;
	
	// 생성자 - 객체생성
	public SimpleSubject() {
		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(value);
		}
	}
	
	// 변경사항을 설정할때 notifyObservers 메소드를 호출!
	public void setValue(int value) {
		this.value = value;
		notifyObservers();
	}
}

 

인터페이스에 따른 구상클래스 구현 - Observer

// SimpleObserver.java
public class SimpleObserver implements Observer {
	private int value;
	private Observable observable;
	
	// 생성자 - 객체생성
	public SimpleObserver(Observable observable) {
		this.observable = observable;
		observable.addObserver((Observer) this);
	}
	
	// 출력용
	public void display() {
		System.out.println("Value: " + value);
	}

	@Override
	public void update(Observable o, Object arg) {
		System.out.println(arg);
		this.value = (int) arg;
		display();
		// 주제에게 받은 내용을 넣음
		if (o instanceof SimpleSubject) {
			SimpleSubject simpleSubject = (SimpleSubject)o;
			this.value = simpleSubject.getValue();
			display();
		}
	}
}

 

디스플레이 구현

public class CurrentConditionsDisplay implements Observer, DisplayElement {
	private int value;
	private SimpleSubject simpleSubject;
    
	public CurrentConditionsDisplay(SimpleSubject simpleSubject) {
		this.simpleSubject = simpleSubject;
		simpleSubject.registerObserver(this);
	}
    
	public void update(int value){
		this.value = value;
		display();
	}
    
	public void display() {
		System.out.println("현재 값 : "+value);
	}
}

실제로는 온습도, 기압을 나타내야하지만, 위에서 구현한 바는 전부 value였으므로 그대로 value로 나타내었다.

 

 

 

 라이브러리 속 옵저버 패턴 알아보기

옵저버 패턴을 사용한 라이브러리 중 스윙(Swing) 라이브러리에 대해 알아보자.

 

Swing 라이브러리

스윙은 인터페이스 용도의 GUI 툴킷인데, 기본 구성 요소 중 하나인 JButton 클래스를 분석해보면,
JButton의 슈퍼클래스인 AbstractButton을 보면 리스너를 추가하고 삭제하는 메소드들이 가득하다.

 

AbstractButton (Java 2 Platform SE 5.0)

action, add, addComponentListener, addFocusListener, addHierarchyBoundsListener, addHierarchyListener, addInputMethodListener, addKeyListener, addMouseListener, addMouseMotionListener, addMouseWheelListener, bounds, checkImage, checkImage, coalesceEvents,

cris.joongbu.ac.kr

 

간단한 Swing라이브러리 테스트

"할까 말까?" 가 작성된 버튼을 누르면 리스너가 "그냥 저질러 버렷!!!" 혹은 "하지 마! 아마 후회할 걸?"이라는 텍스트를 출력하도록 해보자.

import java.awt.*;
import javax.swing.*;
	
public class SwingObserverExample {
	JFrame frame;
	// 프레임을 만들고 그 안에 버튼을 추가
	public static void main(String[] args) {
		SwingObserverExample example = new SwingObserverExample();
		example.go();
	}
	
	public void go() {
		frame = new JFrame();
		JButton button = new JButton("할까 말까?");
		
		// 람다 없이 구현하려면 객체를 꼭 생성해야함.
		//button.addActionListener(new AngelListener());
		//button.addActionListener(new DevilListener());
        
		// 람다를 사용할 경우
		button.addActionListener(event -> 
			System.out.println("하지 마! 아마 후회할 걸?")
		);
		button.addActionListener(event ->
			System.out.println("그냥 저질러 버렷!!!")
		);
        
		// 프레임 속성을 설정
		frame.getContentPane().add(BorderLayout.CENTER, button);
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.getContentPane().add(BorderLayout.CENTER, button);
		frame.setSize(300,300);
		frame.setVisible(true);
	}
}

 

 

pull 방식으로 코드 바꾸기

이때까지는 옵저버가 요청하지 않아도 주제가 모든 옵저버에게 보내는(push) 방식이었는데,  옵저버가 주제로부터 데이터를 당겨오는(pull) 방식을 사용해도 됨

 

사실상 push VS pull을 선택하는 일은 구현방법의 문제이긴 하지만, 대체로는 옵저버가 필요한 데이터를 골라 가져가도록 만드는 것이 더 좋음!

 

주제에서 알림 보내기 - update의 인자 없애기

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

 

옵저버에서 알림 받기 - update 매개변수 삭제

// 옵저버의 인터페이스
public interface Observer {
    // public void update(float temp, float humidity, foat pressure);
    public void update();
}

// 옵저버 구상클래스에서 update 구현부
//public void update(Observable o, Object arg) {
//    this.value = (int) arg;
//    display();
//}
public void update() {
    this.temperature = weatherData.getTemperature();
    this.humidity = weatherData.getHumidity();
    display();
}

 

Comments