옵저버 패턴1. 기상 모니터링 애플리케이션 개요2. 옵저버 패턴을 본격적으로 배워보자2.1 옵저버 패턴 클래스 다이어그램2.2 느슨한 결합에 대하여3. 다시 기상 스테이션 시스템을 구현해보자3.1 다이어그램3.2 인터페이스 소스 코드3.2.1 Subject3.2.2 Observer3.2.3 DisplayEelement3.3 구현 코드3.3.1 WeatherData 3.3.2 Display 항목3.3.3 Main 소스4. Java Observer4.1 자바 내장 옵저버 패턴 작동 방식4.2 Observer 객체 만드는 방법4.3 Observable에서 Observer 객체에게 데이터 전달 하는 방법4.4 Observer가 데이터를 전달 받는 방법4.5 setChanged() 메소드?4.6 자바 내장 옵저버를 이용한 가상 기상스테이션 구현 코드4.6.1 WeatherData 클래스4.6.2 Display 클래스4.7 자바 내장 옵저버 단점
옵저버 패턴
- 옵저버 패턴은 뭔가 중요한 일이 일어났을 때, 객체들한테 새 소식을 알려 줄 수 있는 패턴이다.
- 객체는 해당 정보를 받을지 여부를 실행 중에 결정할 수 있다.
- 이 패턴은 JDK에서 가장 많이 쓰이는 패턴 중 하나이다.
1. 기상 모니터링 애플리케이션 개요
기상 정보 스테이션 구축 프로젝트에 참여하게 됐다.
기상 스테이션으로부터 데이터를 받아서 디스플레이 장비에서 갱신해 가면서 보여 주는 애플리케이션을 구현해야 한다.
이 시스템은 크게 2 개의 역할 군으로 구성된다.
기상 스테이션
- 습도, 온도, 압력 센서로부터 데이터를 수집
WeatherData 객체
기상 스테이션을 통해 데이터를 취득
세 개의 디스플레이 항목
- 현재 온도, 습도 압력
- 기상 통계
- 간단한 기상 예보
추가 디스플레이를 개발할 수 있도록 확장 가능 해야함
간단하게 코드로 구현해보자
- WeatherData.java
public class WeatherData{
//인스턴스 변수 선언
//데이터를 받는 부분은 이미 구현이 되어있다고 가정
private float getTemperature(){
//...
return value;
}
private float getHumidity(){
//...
return value;
}
private float getPressure(){
//...
return value;
}
//기상 관측값이 갱신될 때마다 알려주기 위한 메소드
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);
}
//기타메소드
}
혹시 위의 코드가 어떤 점이 문제가 있는지 알겠는가?
인터페이스가 아닌 구체적인 구현을 바탕으로 코딩하고 있다.
- currentConditionsDisplay, statisticsDisplay, forecastDisplay
- 그래도 update라는 공통된 인터페이스를 사용하고 있다.
새로운 디스플레이 항목이 추가될 때마다 코드를 변경해야 한다.
추가
Display.update(temp, humidity, pressure) 같은 코드가 계속 추가된다.
실행 중에 디스플레이 항목을 추가/제거할 수 없다.
- 디스플레이를 추가/제거하려면 코드를 변경해야 하기 때문이다.
바뀌는 부분을 캡슐화하지 않았다.
- 디스플레이의 성격에 따라 화면에 표시되는 데이터가 다를 수 있다.
- 현재 update 메소드의 파라미터가 모두 동일하다. (temp, humidity, pressure)
2. 옵저버 패턴을 본격적으로 배워보자
우선 옵저버 패턴을 공부한 다음, 나중에 기상 모니터링 애플리케이션에 어떤식으로 적용 할 수 있을지 생각해보자
발행/구독 매커니즘만 알고있다면 옵저버 패턴은 비교적 이해하기 쉬운 패턴이다. 옵저버 패턴의 정의는 아래와 같다.
옵저버 패턴은 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다(one-to-many) 의존성을 정의한 패턴이다.
Subject(주제) 객체의 역할
- 변경될 데이터를 관리
- 주제의 데이터가 변경되면 변경된 값을 옵저버들에게 전달함
Observer(옵저버) 객체의 역할
옵저버 객체들은 주제 객체를 구독하고 있으며(주제 객체에 등록되어 있으며)
주제의 데이터가 바뀌면 갱신 내용을 전달 받음
위 그림의 Dog, Cat, Mouse 객체는 옵저버 인터페이스를 구현한 객체임
2.1 옵저버 패턴 클래스 다이어그램
Subject(주제)
- 주제를 나타내는 인터페이스
- 각 주제마다 여러 개의 옵저버가 있을 수 있음 (일대다)
ConcreteSubject
- Subject 인터페이스의 실제 구현 클래스
- 옵저버의 등록 및 해지를 위한 register, remove 메소드 구현
- 모든 옵저버들에게 연락하기 위한 notify 메소드 구현
Observer(옵저버)
- 데이터의 변경을 알아야 하는 객체는 Observer 인터페이스를 구현하면 됨
- update 메소드 하나만 존재함
ConcreteObserver
- Observer 인터페이스의 실제 구현 클래스
- update 메소드에 데이터를 처리하는 로직이 구현되면 됨
- update 메소드외에 추가적으로 필요한 메소드를 더 구현할 수 있음
2.2 느슨한 결합에 대하여
Subject와 Observer의 의존성 관계
- 데이터의 주인은 Subject이다.
- 옵저버는 데이터가 변경되었을 때 갱신해주기를 기다리는 입장이기 때문에, 의존성을 가진다고 볼 수 있다.
느슨한 결합
옵저버 패턴은 느슨한 결합의 패턴이다.
- Subject와 Observer는 서로 상호작용을 하긴 하지만 서로에 대해 잘 모른다는 것을 의미함
Subject가 Observer대해 아는 것은 그저 인터페이스를 구현했다는 것 뿐
- Observer의 구현부(ConcreteObserver) 클래스가 무엇인지, 어떤 일을 하는지 등에 대해 알 필요가 없다.
Observer는 언제든 추가/제거가 가능하다.
- 느슨한 결합이기 때문에, 코드 변경 없이 아무 때나 추가/제거가 가능하다.
새로운 Observer를 추가하려고 할 때, Subject의 코드를 변경할 필요가 없다.
느슨하게 결합하는 디자인을 사용하면 변경 사항이 생겨도 무난히 처리할 수 있는 유연한 객체지향 시스템을 구축할 수 있다. 객체 사이의 상호 의존성을 최소화할 수 있기 때문이다.
3. 다시 기상 스테이션 시스템을 구현해보자
3.1 다이어그램
DisplayElement
- 화면을 출력하는 공통 기능이 있기 때문에, 공통 인터페이스 생성
CurrentConditionsDisplay: 현재 기상 값 표시 화면
ForcastDisplay: 일기예보 화면
StatsticsDisplay: 기상 통계 화면
ThridPartDisplay: 새로 추가 될 화면
3.2 인터페이스 소스 코드
3.2.1 Subject
xxxxxxxxxx
public interface Subject {
public void registerObserver(Observer o);
public void removeObserver(Observer o);
public void notifyObserver();
}
3.2.2 Observer
xxxxxxxxxx
public interface Observer {
public void update(float temp, float humidity, float pressure);
}
3.2.3 DisplayEelement
xxxxxxxxxx
public interface DisplayElement {
public void display();
}
3.3 구현 코드
3.3.1 WeatherData
xpublic class WeatherData implements Subject {
private List<Observer> observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData() {
observers = new ArrayList<Observer>();
}
public void registerObserver(Observer o) {
observers.add(o);
}
public void removeObserver(Observer o) {
int i = observers.indexOf(o);
if (i >= 0) {
observers.remove(i);
}
}
public void notifyObserver() {
for (Observer o : observers) {
o.update(temperature, humidity, pressure);
}
}
public void measurementsChanged() {
notifyObserver();
}
//옵저버 패턴 테스트를 위한 테스트 코드
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
}
- 기상청 API를 통해 실제 측정값을 가져 오도록 만들면 더 재미있을 것!
- 위 코드는 setMeasurements 메소드를 통해 수동으로 값을 입력하는 테스트 코드로 구현
3.3.2 Display 항목
xxxxxxxxxx
public class CurrentConditionsDisplay implements Observer, DisplayElement {
private Subject weatherData;
private float temperature;
private float humidity;
public CurrentConditionsDisplay(Subject weatherData) {
//Subject 멤버변수 저장
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
public void display() {
System.out.println("현재 온도: " + temperature);
System.out.println("현재 습도: " + humidity);
}
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
display();
}
}
xxxxxxxxxx
public class StatisticsDisplay implements Observer, DisplayElement {
private float maxTemp = 0.0f;
private float minTemp = 200;
private float tempSum = 0.0f;
private int numReadings;
public StatisticsDisplay(Subject weatherData) {
//Subject를 멤버변수로 따로 저장 안함
weatherData.registerObserver(this);
}
public void display() {
System.out.println("평균/최대/최소 온도 = " + (tempSum / numReadings) + "/" + maxTemp + "/" + minTemp);
}
public void update(float temp, float humidity, float pressure) {
tempSum += temp;
numReadings++;
if (temp > maxTemp) {
maxTemp = temp;
}
if (temp < minTemp) {
minTemp = temp;
}
display();
}
}
xxxxxxxxxx
public class ForecastDisplay implements DisplayElement, Observer {
private float currentPressure = 29.92f;
private float lastPressure;
public ForecastDisplay(Subject weatherData) {
weatherData.registerObserver(this);
}
public void display() {
System.out.print("일기예보: ");
if (currentPressure > lastPressure) {
System.out.println("날씨가 점차 좋아질 예정!");
} else if (currentPressure == lastPressure) {
System.out.println("비슷할 예정");
} else if (currentPressure < lastPressure) {
System.out.println("조금씩 추워지면서 비가 올 예정");
}
}
public void update(float temp, float humidity, float pressure) {
lastPressure = currentPressure;
currentPressure = pressure;
display();
}
}
3.3.3 Main 소스
xxxxxxxxxx
public class WeatherStation {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
weatherData.setMeasurements(30, 60, 30.4f);
weatherData.setMeasurements(33, 50, 29.2f);
weatherData.setMeasurements(28, 60, 29.2f);
weatherData.setMeasurements(25, 80, 28.2f);
}
}
4. Java Observer
자바 java.util 패키지에 Observer 인터페이스와 Observable(Subject) 클래스를 지원합니다.
자바에서 제공하는 옵저버 패턴은 어떤식으로 사용되고, 어떤 차이가 있는지 알아보겠습니다.
4.1 자바 내장 옵저버 패턴 작동 방식
가장 큰 차이점은 주제 객체, 즉 Observable가 클래스로 구현된 구현체
- 위 예제에서는 Subject 라는 인터페이스를 사용하여, 인터페이스에 맞게 직접 구현 하였음
- registerObserver, removeObserver, notifyObserver 메소드를 직접 구현
- 반면, Observable 클래스는 위 메소드가 이미 구현되어있음 (addObserver, deleteObserver, notifyObservers)
자바 Observable 클래스는 위의 메소드 이외에 다른 메소드를 더 지원함
- Observable 메소드 종류
4.2 Observer 객체 만드는 방법
위 날씨 예제에서 구현한 Observer 방법과 동일(메소드 이름만 상이)
- java.util.Observer 인터페이스를 상속 받아 구현 후 ,Observable 객체의 addObserver 메소드 호출
- 옵저버 목록에서 탈퇴하고 싶다면 deleteObserver 메소드 호출
4.3 Observable에서 Observer 객체에게 데이터 전달 하는 방법
java.util.Observable 클래스를 상속받아 주제 객체를 생성
setChanged() 메소드를 호출하여 객체의 상태가 바뀌었다는 것을 알림
- 즉, 데이터가 변경되는 부분에 setChanged() 메소드를 호출
그 다음 notifyObservers 메소드 호출 (2가지 종류 메소드가 존재)
notifyObservers()
notifyObservers(Object arg)
- update 메소드의 arg 인자로 전달
4.4 Observer가 데이터를 전달 받는 방법
- java.util.Observer에서 구현하는 update 메소드의 형태는 아래과 같음
- 기본적으로 observable 객체를 받고, 인자가 추가적으로 필요하다면 arg 파라미터를 통해 전달 받음
- 인자가 없다면 arg값에는 null이 전달됨
xxxxxxxxxx
void update(Observable o, Object arg)
4.5 setChanged() 메소드?
- 이 메소드는 상태가 바뀌었다는 것을 밝히기 위한 용도로 쓰임
- notifyObservers() 메소드가 호출되었을 때 이 메소드에서 옵저버들을 갱신 시킴
- setChanged() 메소드가 호출 되지 않은 상태에서 notifyObservers() 메소드가 호출되면 옵저버에게 데이터를 갱신시키지 않음
- 아래는 코드는 Observable 클래스의 일부
x
public synchronized boolean hasChanged()
{
return changed; // 현재 changed 값을 반환
}
public void notifyObservers(Object obj)
{
//changed 값이 false면 함수 종료
//옵저버에게 연락하기 앞서 setChanged() 메소드를 호출하여 changed 값을 true로 바꿔야 함
if (!hasChanged())
return;
//코드가 길어져서 간단하게 요약하였음
//실제 순회하는 코드는 Set을 Iterator 하는 방식으로 순회하여 옵저버 객체의 update() 메소드를 호출합니다.
//순회하는 코드 외에는 실제 자바 API 코드들 입니다.
//옵저버 데이터 전달
목록에 있는 모든 옵저버{
update(this,obj)
}
//순회가 끝나면 changed 값을 false로 변경
clearChanged();
}
protected synchronized void setChanged()
{
changed = true;
}
protected synchronized void clearChanged()
{
changed = false;
}
4.6 자바 내장 옵저버를 이용한 가상 기상스테이션 구현 코드
4.6.1 WeatherData 클래스
- 옵저버를 관리하는 코드와 메소드는 Observable 클래스에 모두 있기 때문에, 따로 구현하지 않아도 됨
- WeatherData 클래스에서는 언제, 어떤 데이터를 줄 것 인가에 대한 코드만 구현
xxxxxxxxxx
public class WeatherData extends Observable {
private float temperature;
private float humidity;
private float pressure;
public WeatherData() {
}
public void measurementsChanged() {
setChanged(); //상태 변경
notifyObservers();
}
//옵저버 패턴 테스트를 위한 테스트 코드
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
//처음 예제는 전달받은 데이터(온도,습도,기압)을 직접 구현한 notify 함수에 update 메소드 인자로 전달했지만,
//현재 예제에서는 데이터가 들어있는 객체자체를 update 인자로 전달하고
//Observer 객체에서 전달받은 객체의 데이터에 접근하기 위해 Getter 메소드 생성
public float getTemperature() {
return temperature;
}
public float getHumidity() {
return humidity;
}
public float getPressure() {
return pressure;
}
}
4.6.2 Display 클래스
- java.uill.Observer 인터페이스 상속
- java.util.Observable 클래스 래퍼런스 인자 생성
xxxxxxxxxx
public class CurrentConditionsDisplay implements Observer, DisplayElement {
private Observable weatherData; // java.util.Observable
private float temperature;
private float humidity;
public CurrentConditionsDisplay(Observable weatherData) {
this.weatherData = weatherData;
weatherData.addObserver(this);
}
public void display() {
System.out.println("현재 온도: " + temperature);
System.out.println("현재 습도: " + humidity);
}
public void update(Observable obs, Object arg1) {
if (obs instanceof WeatherData) {
//객체를 전달 받고, getter 메소드를 통해 해당 객체 데이터로 접근
WeatherData weatherData = (WeatherData) obs;
this.temperature = weatherData.getTemperature();
this.humidity = weatherData.getHumidity();
display();
}
}
}
4.7 자바 내장 옵저버 단점
Observable은 인터페이스가 아니라 클래스라는 점
- 다른 수퍼 클래스를 상속받아 확장하고 있는경우 Observable의 기능을 추가 할 수 없음, 따라서 재사용성이 제약되어 디자인패턴의 사용 목적을 상실
Observable의 핵심 메소드를 외부에서 호출 할 수 없음
- setChagned() 메소드가 protected로 선언되어있어, 서브클래스가 아닌 인트턴스 변수로 사용 불가능
결론
- java.util.Observable을 상속하여 사용할 수 있는 상황이라면 Observable API를 쓰는 것도 괜찮지만, 직접 옵저버 패턴을 구현해야 할 수도 있다.
- 핵심은 옵저버 패턴만 제대로 알고 있다면 그 패턴을 활용하는 API는 어떤 것이든 잘 활용할 수 있을 것
'Design pattern' 카테고리의 다른 글
데코레이터 패턴(decorator pattern) 정리 (0) | 2020.07.06 |
---|---|
스트래티지 패턴(strategy pattern) 정리 (1) | 2020.05.20 |
댓글