옵저버 패턴 (Observer Pattern)
한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다(one-to-many) 의존성을 정의
옵저버 패턴 = 주제 (subject) + 옵저버 (observer)
마치 '신문사와 구독자', '헤드헌터와 개발자' 같다.
구독을 하면 정보가 업데이트 될 때마다 연락이 가고, 구독을 해지하면 더 이상 연락이 오지 않는다.
약간 아이돌 버블과도 비슷한 거 같다는 생각을 했다 🤔
구독하면 아이돌의 톡을 실시간으로 받을 수 있고,
구독 해지하면 더 이상 톡을 받지 못 하고
다음은 책에서 비유한 옵저버 패턴이다.
옵저버 패턴의 클래스 다이어그램은 다음과 같다.
Subject
인터페이스를 구현한 구상 클래스가 되겠다.Observer
인터페이스를 구현한 구상 클래스가 되겠다.느슨한 결합 이란?
: 객체들이 상호작용할 수는 있지만, 서로를 잘 모르는 관계를 의미
느슨한 결합의 좋은 예시 중 하나가 옵저버 패턴 !
다시 버블을 생각해보자 🤔
버블을 안 해봐서 잘 모르지만 내가 알고있는 걸 바탕으로 하자면,
아이돌 입장에선 나를 구독하는 팬들이 있는 건 알지만
(팬 입장에선 슬프게도) 팬 한 명 한 명이 누군진 모른다 ..
팬들끼리도 나 외엔 누가 구독을 하고 있는지 알 수 없다
-> 이게 바로 느슨한 결합 이다.
만약에 버블이 아주아주 끈끈한 결합 관계였다면 어떨까 🤔
실명제로 운영이 돼서,
아이돌도 나에게 어떠한 말을 한 사람이 누군지 알고 ,,
아이돌에게 열렬한 사랑의 메세지를 보내는 팬이 내 친구고 ,,
내가 보낸 메세지도 친구가 알고 ,,
부모님이 알고 ,, ,,
교수님이 알고 ,, ,, ,,
직장 상사가 알고 ,, ,, ,, ,,
완전 최악이다 ! !
아주 인간관계가 민망해질 거 같다
이게 바로 단단한 결합의 단점이자 느슨한 결합의 장점이다.
느슨한 결합을 통해 유연한 디자인을 만들 수 있고,
서로 잘 모르기 때문에 변화에 더 잘 대응할 수 있다.
어려운 말로 표현하자면, 객체 사이의 상호의존성을 최소화할 수 있따.
즉, 변경 사항이 생겨도 무난히 처리할 수 있는 유연한 객체지향 시스템을 구축할 수 있다.
옵저버 패턴에서 느슨한 결합을 만드는 방법은 다음과 같다.
Observer
인터페이스를 구현한 구상 클래스가 뭔지, 걔네가 무엇을 하는지 상세정보는 알 필요가 없다!Subject
, Observer
인터페이스를 구현한다는 조건만 만족한다면, 각자에게 필요한 메소드를 추가하든 수정하든 뭘하든 괜찮다!여기서 나오는 디자인 원칙
디자인 원칙
상호작용하는 객체 사이에는 가능하면 느슨한 결합을 사용해야 한다.
[조건]
[WeatherData 클래스]
[구현 목표]
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개가 끝일까?? 새로운 측정값이 추가될 수도 있지 않을까??
이제 위 코드에 옵저버 패턴을 적용해보자.
옵저버 패턴을 적용해 다이어그램을 그리면 위와 같다.
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) 방식을 사용하면 된다
(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 함수를 통해 가져오면 된다.
객체지향 기초
- 추상화
- 캡슐화
- 다형성
- 상속
객체지향 원칙
- 바뀌는 부분은 캡슐화 한다.
- 상속보다는 구성을 활용한다.
- 구현보다는 인터페이스에 맞춰서 프로그램이한다.
- 상호작용하는 객체 사이에서는 가능하면 느슨한 결합을 사용해야 한다.
객체지향 패턴
전략 패턴
전략 패턴은 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 한다.
전략패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있다.옵저버 패턴
한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로, 일대다 (one-to-many) 의존성을 정의한다.