옵저버 패턴
옵저버 패턴
옵저버 패턴은 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의합니다.
🔍 subject
주제를 나타내는 인터페이스입니다.
객체에서 옵저버로 등록하거나 옵저버 목록에서 탈퇴하고 싶을 때는 이 인터페이스에 있는 메소드를 사용합니다.
🔍 ConcreteSubject
주제 역할을 하는 구상 클래스는 항상 Subject 인터페이스를 구현해야 합니다.
주제클래스는 등록 및 해지 메소드와 상태가 바뀔 때 마다 모든 옵저버에게 연락하는 메소드를 구현해야합니다.
🔍 Obsever
옵저버 될 가능성이 있는 객체는 반드시 Observer 인터페이스를 구현해야 합니다.
이 인터페이스는 주제의 상태가 바뀌었을 때 호출되는 update() 메소드만 구현하면 됩니다.
🔍 ConcreteObsever
Observer 인터페이스만 구현한다면 무엇이든 옵저버 클래스가 될 수 있습니다.
각 옵저버는 특정 주제에 등록해서 연락을 받을 수 있습니다.
옵저버 패턴 구현하기
📌 Subject 인터페이스
public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}
📌 WeahterInformation
public class WeatherInformation {
private final float temperature;
private final float humidity;
private final float pressure;
public WeatherInformation(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
}
public float temperature() {
return temperature;
}
public float humidity() {
return humidity;
}
public float pressure() {
return pressure;
}
@Override
public String toString() {
return "WeatherInformation{" +
"temperature=" + temperature +
", humidity=" + humidity +
", pressure=" + pressure +
'}';
}
}
날씨 정보들을 하나의 객체로 만들어서 관리합니다.
📌 Observers
public class Observers {
private final List<Observer> observers = new ArrayList<>();
public void register(Observer observer) {
observers.add(observer);
}
public void remove(Observer observer) {
observers.remove(observer);
}
public void forEach(Consumer<? super Observer> action) {
observers.forEach(action);
}
@Override
public String toString() {
return "Observers{" +
"observers=" + observers +
'}';
}
}
옵저버를 관리하는 자료구조를 일급 컬렉션으로 만들었습니다. 일급 컬렉션으로 만듦으로써, 비즈니스에 종속적인 객체이고, 이름을 가지는 컬렉션이 되도록 하였습니다.
📌 Weather
public class Weather implements Subject {
private final Observers observers = new Observers();
private WeatherInformation weatherInformation;
public Weather(WeatherInformation weatherInformation) {
this.weatherInformation = weatherInformation;
}
@Override
public void registerObserver(Observer observer) {
observers.register(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
observers.forEach(Observer::update);
}
public void measureTheWeather(WeatherInformation weatherInformation) {
this.weatherInformation = weatherInformation;
notifyObservers();
}
public float temperature() {
return weatherInformation.temperature();
}
public float humidity() {
return weatherInformation.humidity();
}
public float pressure() {
return weatherInformation.pressure();
}
@Override
public String toString() {
return "Weather{" +
"observers=" + observers +
", weatherInformation=" + weatherInformation +
'}';
}
}
Subject의 인터페이스를 구현하고 있는 ConcreteSubject에 해당하는 클래스입니다. WeahterInformation 클래스와, Observers 클래스를 통해서 서로 연관성이 있는 것들로 묶었습니다. 그러니 Weather 클래스의 멤버변수 개수가 줄어든 것을 볼 수 있습니다.
단지, 마음에 들지 않는 점은 멤버변수 개수가 줄었지만, 클래스 크기는 아직도 크다고 생각합니다. 왜냐하면 Weahter 클래스를 통해 날씨 정보들을 조회하려면 WeahterInformatio 클래스의 멤버 변수들을 조회해서 가져와야 하기 때문에 메소드 개수가 많은걸 볼 수 있습니다. (디미터 규칙 준수)
한 클래스에 멤버 변수도 적고, public 메소드 수를 줄이고 싶지만 아직 제 실력이 부족하여 클래스의 크기를 줄이는법을 모르겠습니다.
📌 Observer
public interface Observer {
void update();
}
📌 ConcreteConditionDisplay
public class CreateConditionsDisplay implements Observer {
private float temperature;
private float humidity;
private Weather weather;
public CreateConditionsDisplay(Weather weather) {
this.weather = weather;
this.temperature = weather.temperature();
this.humidity = weather.humidity();
}
@Override
public void update() {
temperature = weather.temperature();
humidity = weather.humidity();
}
@Override
public String toString() {
return "CreateConditionsDisplay{" +
"temperature=" + temperature +
", humidity=" + humidity +
", weather=" + weather +
'}';
}
}
📌 Clinet
public class Client {
public static void main(String[] args) {
WeatherInformation weatherInformation = new WeatherInformation(3.14f, 1.0f, 2.0f);
Weather weather = new Weather(weatherInformation);
Observer observer = new CreateConditionsDisplay(weather);
weather.registerObserver(observer);
weather.notifyObservers();
WeatherInformation newWeatherInformation = new WeatherInformation(4.0f, 10.f, 2.5f);
weather.measureTheWeather(newWeatherInformation);
}
}
옵저버 패턴은 이렇게 구현할 수 있습니다. 하지만 여기서 문제가 있습니다. 바로, Observer 구현 클래스와 Subject 구현 클래스가 서로 양방향 참조라는 것입니다 .
📌 Clinet (문제 발생)
public class Client {
public static void main(String[] args) {
WeatherInformation weatherInformation = new WeatherInformation(3.14f, 1.0f, 2.0f);
Weather weather = new Weather(weatherInformation);
Observer observer = new CreateConditionsDisplay(weather);
weather.registerObserver(observer);
weather.notifyObservers();
WeatherInformation newWeatherInformation = new WeatherInformation(4.0f, 10.f, 2.5f);
weather.measureTheWeather(newWeatherInformation);
/**
* 문제 발생 시점
*/
System.out.println("weather = " + weather);
}
}
weather를 조회하거나 observer를 조회할 경우 서로 양방향 참조이기 때문에 StackOverflowError가 발생합니다.
그리고 기본적으로 양방향 보다는 단방향이 복잡성이 더 낮고 이해하기 쉽습니다.
해결 방안
그래서 이 양방향 참조를 어떻게 해결할지에 대해서 제가 구상한 해결방안을 소개하고자 합니다.
1. pull이 아닌 push로 받기
옵저버가 주제로부터 상태를 끌어오는 방식이 아닌 주제가 옵저버에게 상태를 알리는 방식으로 설계하는 것입니다. 주제가 pull 이 더 좋은 방법입니다. 옵저버가 주제로부터 필요한 데이터를 골라서 가져도록 하는 방법이 더 좋기 때문입니다.
➰ pull 방식
📌 Weather
public class Weather implements Subject {
private final Observers observers = new Observers();
private WeatherInformation weatherInformation;
public Weather(WeatherInformation weatherInformation) {
this.weatherInformation = weatherInformation;
}
@Override
public void notifyObservers() {
observers.forEach(observer -> observer.update();
}
...
}
➰ push 방식
📌 Weather
public class Weather implements Subject {
private final Observers observers = new Observers();
private WeatherInformation weatherInformation;
public Weather(WeatherInformation weatherInformation) {
this.weatherInformation = weatherInformation;
}
@Override
public void notifyObservers() {
observers.forEach(observer -> observer.update(this));
}
...
}
push 방식은 pull 방식과 달리 날씨 정보를 옵저버에게 전달한다. 그리고 옵저버는 멤버변수로 주제 클래스를 참조하고 있지 않다.
📌 CreateConditionDisply
public class CreateConditionsDisplay implements Observer {
private float temperature;
private float humidity;
public CreateConditionsDisplay(Weather weather) {
this.temperature = weather.temperature();
this.humidity = weather.humidity();
}
@Override
public void update(Weather weather) {
temperature = weather.temperature();
humidity = weather.humidity();
}
public CreateConditionsDisplay(float temperature, float humidity) {
this.temperature = temperature;
this.humidity = humidity;
}
}
맨 처음 pull 방식과 구현 했을 때와 다른점은 옵저버 구현 클래스가 주제 구현 클래스를 포함하고 있지 않다. 그렇기 때문에 양방향이 아닌 단방향으로 되었다.
2. pull 방식으로 하되, 옵저버 구현 클래스가 주제 클래스가 아닌 주제 클래스의 정보에 의존하기
📌 Weather
public class Weather implements Subject {
private final Observers observers = new Observers();
private WeatherInformation weatherInformation;
public Weather(WeatherInformation weatherInformation) {
this.weatherInformation = weatherInformation;
}
@Override
public void notifyObservers() {
observers.forEach(observer -> observer.update());
}
...
}
📌 CreateConditionDisplay
public class CreateConditionsDisplay implements Observer {
private float temperature;
private float humidity;
private WeatherInformation weatherInformation;
public CreateConditionsDisplay(WeatherInformation weatherInformation) {
this.temperature = weatherInformation.temperature();
this.humidity = weatherInformation.humidity();
}
@Override
public void update() {
temperature = weatherInformation.temperature();
humidity = weatherInformation.humidity();
}
public CreateConditionsDisplay(float temperature, float humidity) {
this.temperature = temperature;
this.humidity = humidity;
}
}
옵저버 구현 클래스가 주제 클래스인 Weahter 클래스에 의존하기 않고, 주제 클래스의 정보를 담당하고 있는 WeatherInformation을 의존하고 있습니다. 이렇게 하면 양방향이 아닌 단방향으로 할 수 있습니다. 하지만 뭔가 뭔가 주제 클래스와 협력하는 것이 아닌 주제 클래스의 정보 클래스와 협력하고 있다는 것이 뭔가 뭔가 마음이 걸립니다.
3. 주제 인터페이스를 추상 클래스로 정의하기
📌 Subject
public abstract class Subject {
private final Observers observers = new Observers();
public void registerObserver(Observer observer) {
observers.register(observer);
}
public void removeObserver(Observer observer) {
observers.remove(observer);
};
public void notifyObservers() {
observers.forEach(observer -> observer.update());
};
}
주제 클래스를 인터페이스가 아닌 추상 클래스로 정의합니다.
옵저버를 관리하는 추상 클래스입니다.
완성된 클래스가 아닌 추상 클래스로 정의한 이유는 주제 클래스를 객체로 만들 수 없도록 하기 위함입니다.
📌 Weather
public class Weather extends Subject {
private WeatherInformation weatherInformation;
public Weather(WeatherInformation weatherInformation) {
this.weatherInformation = weatherInformation;
}
public void measureTheWeather(WeatherInformation weatherInformation) {
this.weatherInformation = weatherInformation;
notifyObservers();
}
public float temperature() {
return weatherInformation.temperature();
}
public float humidity() {
return weatherInformation.humidity();
}
public float pressure() {
return weatherInformation.pressure();
}
}
주제 클래스가 인터페이스가 아닌 추상 클래스로 바뀌었으므로 구현이 아닌 상속으로 바뀌어야 합니다.
📌 CreateConditionsDisplay
public class CreateConditionsDisplay implements Observer {
private float temperature;
private float humidity;
private Weather weather;
public CreateConditionsDisplay(Weather weather) {
this.temperature = weather.temperature();
this.humidity = weather.humidity();
}
@Override
public void update() {
temperature = weather.temperature();
humidity = weather.humidity();
}
public CreateConditionsDisplay(float temperature, float humidity) {
this.temperature = temperature;
this.humidity = humidity;
}
}
Subject 클래스를 추상 클래스로 하여 옵저버들을 관리하도록 하고, Weather 클래스는 날씨 정보만 관리하도록 하여, 관심사를 분리하도록 하면 옵저버와 주제는 서로 양방향 참조가 아닌 단방향으로 할 수 있습니다. 그리고 옵저버가 Weather 정보를 pull 방식으로 가져오도록 할 수 있습니다.
하지만 이 방식은 상속을 사용한다는 단점이 있습니다. 상속은 부모의 캡슐화를 깨드리면서, 부모와 자식간의 강한 결합이 되도록 만듭니다.
결론
해결 방안인 1~3번을 다 살펴보았습니다.사실 어느 한 가지 방법이 끌리는 방법이 없습니다. 그래도 그나마 마음에 드는 건 3번이 아닌가 싶습니다.