"헤더퍼스트 디자인패턴" + "면접을 위한 CS 전공 지식 노트"에 개인적인 의견과 생각들을 추가하여 작성한 포스팅이므로 틀린 내용이 있을 수 있습니다. (있다면 지적 부탁 드립니다.)
📕 목차
1. Observer Pattern
2. 기상 스테이션 코드
3. Functional Interface & Observer
1. Observer Pattern
📌 옵저버 패턴
- 한 객체의 상태가 바뀌면, 그 객체에 의존하는 다른 객체에게 연락을 취해 자동으로 내용이 갱신되는 일대다 의존성
- 주체가 어떤 객체(Subject)의 상태 변화를 감지하면, 상태 변화가 있을 때마다 메서드 등을 통해 관찰자(Observer)들에게 통지하고, 관찰자들은 알림을 받아 조치를 취한다.
- 주체: 객체의 상태 변화를 보고 있는 관찰자
- 옵저버: 객체 상태 변화에 따라 추가 변화 사항이 생기는 객체들
- 주체와 객체가 분리되거나, 분리되지 않은 경우도 있다.
- 주로 분산 이벤트 핸들링 시스템에 사용하며, MVC 패턴에도 사용된다.
- 모델(Model)의 변경사항을 method()로 뷰(View)에 알려주고, 이를 기반으로 컨트롤러(Controller)가 동작을 수행
- 트위터나 인스타그램, 유튜브 등에서 발행자(Subject)가 피드를 올리면, 구독자(Observer)들은 알림을 받는 원리다.
- 구독자는 해당 Subject의 Observer에 등록할 수 있다.
- 구독자는 해당 Subject의 Observer에서 탈퇴할 수 있다.
- Observer 패턴의 표현은 "관찰자가 변화를 바라보고 있다"고 하지만, 실제로는 변화 정보를 수동적으로 전달받는 형태에 가깝다.
📌 Class Diagram
- Subject: 관찰 대상자를 정의하는 인터페이스 (관찰 대상과 Subject가 합쳐지거나, 분리되어 있다.)
- ConcreteObserver: 관찰 당하는 대상자, 발생자, 게시자
- Observer들을 Collection으로 모아 합성(Composition)하여 필드로 가지고 있다. (일반적으로 알림 순서를 고려하지 않으므로 List로 충분하다.)
- Observer들을 등록/삭제 하는 메서드를 제공한다.
- Subject가 상태를 변경하거나, 이벤트 트리거를 발생할 때 Observer들에게 notify를 하는 메서드를 제공한다.
- Observer: 구독자들을 묶는 인터페이스 (다형성)
- ConcreteObserver: 관찰자, 구독자, 수신자
- Observer들은 Subject가 발행한 알림에 대한 현재 상태를 얻는다.
- 수신한 알림에 대해 변화하는 방식을 정의한 update() 메서드를 갖는다.
📌 예시 코드
1️⃣ Subject
interface Subject {
void registerObserver(Observer o);
void removeObserver(Observer o);
void notifyObservers();
Object getUpdate(Observer o);
}
class Topic implements Subject {
private List<Observer> observers;
private String message;
public Topic() {
this.observers = new ArrayList<>();
}
@Override
public void registerObserver(Observer o) {
if (o == null) throw new NullPointerException("Null Observer");
if (observers.contains(o)) throw new IllegalArgumentException("Observer already registered");
synchronized (observers) {
observers.add(o);
}
}
@Override
public void removeObserver(Observer o) {
synchronized (observers) {
observers.remove(o);
}
}
@Override
public void notifyObservers() {
List<Observer> observersLocal = null;
synchronized (observers) {
observersLocal = new ArrayList<>(this.observers);
}
for (Observer o : observersLocal) o.update();
}
@Override
public Object getUpdate(Observer o) {
return this.message;
}
public void postMessage(String msg) {
System.out.println("Message Posted to Topic: " + msg);
this.message = msg;
notifyObservers();
}
}
- 관찰 대상 Subject의 상태(여기선 msg)가 바뀌면 변경 사항을 observer에게 통보(notifyObservers())한다.
- register/removeObserver 메서드로 관찰자를 추가/삭제할 수 있다.
2️⃣ Observer
interface Observer {
void update();
}
class MyTopicSubscriber implements Observer {
private String name;
private Subject topic;
public MyTopicSubscriber(String name, Subject topic) {
this.name = name;
this.topic = topic;
}
@Override
public void update() {
String msg = (String) topic.getUpdate(this);
if (msg == null) System.out.println(name + ":: No new message");
else System.out.println(name + ":: Consuming message::" + msg);
}
}
- Subject로 통보받은 Observer는 update()에 정의된 동작 방식대로 변경 사항에 적절히 대응한다.
3️⃣ 호출
public class Main {
public static void main(String[] args) {
Topic topic = new Topic();
Observer obj1 = new MyTopicSubscriber("Obj1", topic);
Observer obj2 = new MyTopicSubscriber("Obj2", topic);
Observer obj3 = new MyTopicSubscriber("Obj3", topic);
topic.registerObserver(obj1);
topic.registerObserver(obj2);
topic.registerObserver(obj3);
obj1.update();
topic.postMessage("New Message");
}
}
Obj1:: No new message
Message Posted to Topic: New Message
Obj1:: Consuming message::New Message
Obj2:: Consuming message::New Message
Obj3:: Consuming message::New Message
- 처음 obj1는 Subject의 변경 사항을 확인하고, 아무런 변경 사항이 없는 것을 보고 "No new message"를 출력한다.
- Subject에 변경 사항을 반영하면, 모든 Observer에서 변경 사항을 감지하고 출력한다.
🟡 Push vs Pull
- Push
- Subject가 Observer에게 변경 사항을 전달할 때, 데이터를 매개변수로 넘겨주는 방법
- Subject의 추적 정보가 변경되면, 모든 Observer의 코드를 수정해야 한다.
- 특정 Observer에게는 필요없는 정보까지 받아야 한다.
- Pull
- Subject는 Observer에게 변경 알림만 보내고, Observer가 직접 Subject의 변경 사항을 getter 메서드로 얻는 방법
- Observer는 자신이 등록된 Subject를 합성(composition)하여 참조한다.
- 각 Observer는 자신이 원하는 정보만 Subject에게서 받을 수 있다.
- Subject의 추적 정보가 변경되어도 모든 Observer의 코드를 수정할 필요가 없다.
구현 방식의 차이일 뿐이지만, 이유가 없다면 Pull 방식이 보기에도 낫다.
📌 사용 시기
- 애플리케이션이 한정된 시간, 특정 케이스에만 다른 객체를 관창해야 하는 경우
- 대상 객체 상태가 변경될 때마다 다른 객체 동작을 트리거해야 하는 경우
- 한 객체의 상태가 변경되면 다른 객체도 변경해야 하지만, Subject는 어떤 Observer들이 변경되어야 하는 지 몰라도 될 때 (Subject는 변경 사항을 전달만 할 뿐, Observer가 어떤 일을 하는지는 관심이 없기 때문)
- MVC 패턴으로 변형할 수도 있다.
📌 장점
- Subject의 상태 변경을 주기적으로 조회하지 않고 자동으로 감지할 수 있다.
- 발행자 코드를 변경하지 않아도 새 구독자 클래스를 도입 가능하다. (OCP 원칙)
- Runtime 시점에서 발행자와 구독자의 관계를 맺을 수 있다.
- 상태를 변경하는 주체(Subjcet)와 변경을 감시하는 Observer 관계를 느슨하게 유지할 수 있다.
✒️ 느슨한 결합(Loose Coupling)
💡 상호작용하는 객체 사이에는 가능하면 느슨한 결합을 사용하라
- 느슨한 결합은 객체들이 상호작용할 수 있지만, 서로를 잘 모르는 관계를 의미한다.
- 주제는 Observer가 특정 인터페이스(Observer)를 구현한다는 사실만을 안다.
- Observer는 언제든지 추가/제거할 수 있다.
- 새로운 형식의 Observer를 추가하더라도 Subject를 변경할 필요가 없다.
- Subject와 Observer는 서로 독립적으로 재사용할 수 있다.
- Subject나 Observer가 달라져도 서로에게 영향을 주지 않는다.
📌 단점
- 구독자는 알림 순서를 제어할 수 없다. (Observer는 알림 순서에 의존하면 안 된다는 JDK 권고 사항)
- Observer 패턴을 자주 구성하면, 구조와 동작을 이해하기가 힘들어진다.
- 다수의 Observer 객체를 등록 이후 해지하지 않으면, memory leak이 발생할 수 있다.
2. 기상 스테이션 코드
🤔 Use case
• WeatherData 객체는 현재 기상 조건(온도, 습도, 기압)을 추적한다.
• 현재 조건, 기상 통계, 기상 예보 3가지 항목은 WeatherData 객체에서 최신 측정치를 수집한다.
• 3가지 항목은 최신 측정치를 수집할 때마다 실시간으로 화면에 출력한다.
• 확장 가능성이 용이해야 한다.
📌 As-is. Legacy Code
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);
}
...
}
- WeatherData는 구체 클래스에 의존하고 있으므로, 다른 디스플레이 항목을 추가할 확장성을 가지고 있지 않다.
- 3가지 항목은 적어도 update()라는 공통된 인터페이스를 가지고 있다.
📌 Observer Pattern 적용
- 주체인 WeatherData는 Subject 인터페이스를 구현한다.
- 관찰자인 3가지 항목은 Observer를 구현하고, 화면에 출력하기 위한 DisplayElement 인터페이스도 구현한다.
📌 Subject : Pull
interface Subject {
void registerObserver(Observer o);
void removeObserver(Observer o);
void notifyObservers();
}
class WeatherData implements Subject {
private List<Observer> observers;
private float temperature;
private float humidity;
private float pressure;
public WeatherData() {
observers = new ArrayList<>();
}
@Override
public void registerObserver(Observer o) {
observers.add(o);
}
@Override
public void removeObserver(Observer o) {
observers.remove(o);
}
@Override
public void notifyObservers() {
observers.forEach(o -> o.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();
}
}
- Observer를 등록/삭제 하는 메서드를 가진다.
- 감시하고 있는 대상을 필드로 가지고 있다.
- setMeasurements()로 변경 값이 반영되면 Observer들에게 통지한다. (Pull 방식)
📌 Observer
interface Observer {
void update(float temp, float humidity, float pressure);
}
class CurrentConditionsDisplay implements Observer, DisplayElement {
private float temperature;
private float humidity;
@Override
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
display();
}
@Override
public void display() {
System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity");
}
}
class StatisticsDisplay implements Observer, DisplayElement {
private float temperature;
private float humidity;
@Override
public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
display();
}
@Override
public void display() {
System.out.println("Statistics: " + temperature + "F degrees and " + humidity + "% humidity");
}
}
- Subject가 update로 변경 데이터를 전달하면, 각 Observer에서 적절히 변경사항에 대응한다.
- StatisticsDisplay의 경우, pressure라는 불필요한 데이터까지 전달받아야 한다.
- 새로운 변경 사항이 추가되면(해당 데이터가 필요 없음에도), 모든 Observer의 update를 수정해야 한다.
📌 실행
public class WeatherStation {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
weatherData.setMeasurements(80, 65, 30.4f);
weatherData.setMeasurements(82, 70, 29.2f);
weatherData.setMeasurements(78, 90, 29.2f);
}
}
Current conditions: 80.0F degrees and 65.0% humidity
Statistics: 80.0F degrees and 65.0% humidity
Current conditions: 82.0F degrees and 70.0% humidity
Statistics: 82.0F degrees and 70.0% humidity
Current conditions: 78.0F degrees and 90.0% humidity
Statistics: 78.0F degrees and 90.0% humidity
📌 Pull 방식으로 변경하기
1️⃣ Subject
class WeatherData implements Subject {
...
@Override
public void notifyObservers() {
observers.forEach(o -> o.update());
}
@Override
public float getTemperature() {
return temperature;
}
@Override
public float getHumidity() {
return humidity;
}
@Override
public float getPressure() {
return pressure;
}
...
}
- 각 데이터에 대한 getter 메서드를 추가한다.
- Subject는 변경 사항이 존재한다는 트리거만 전달하고, 정보를 직접 전달하지 않는다.
2️⃣ Observer
class CurrentConditionsDisplay implements Observer, DisplayElement {
private float temperature;
private float humidity;
private Subject weatherData;
public CurrentConditionsDisplay(Subject weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this);
}
@Override
public void update() {
this.temperature = weatherData.getTemperature();
this.humidity = weatherData.getHumidity();
display();
}
@Override
public void display() {
System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity");
}
}
- 각 Observer는 자신이 관찰하는 Subject를 composition한다.
- 변경 사항이 발생하면, 해당 Observer 구체 클래스에서 필요한 정보만 수집한다.
- 새로운 추적 사항이 생겨도, 해당 데이터가 필요없는 Observer의 코드를 수정할 필요가 없어진다.
3. Functional Interface & Observer
📌 내부 클래스로 정의한 람다 전달 방법
@FunctionalInterface
interface Observer<E> {
void added(ConcreteSubject<E> set, E element);
}
interface Subject<E> {
void addObserver(Observer<E> o);
void removeObserver(Observer<E> o);
void notifyElementAdded(E element);
}
class ConcreteSubject<E> implements Subject<E> {
// 내부 클래스로 리스너를 정의하는 Subject
private final List<Observer<E>> observers = new ArrayList<>();
@Override
public void addObserver(Observer<E> o) {
synchronized (observers) {
observers.add(o);
}
}
@Override
public void removeObserver(Observer<E> o) {
synchronized (observers) {
observers.remove(o);
}
}
@Override
public void notifyElementAdded(E element) {
List<Observer<E>> snapshot = null;
synchronized (observers) {
snapshot = new ArrayList<>(observers);
}
snapshot.forEach(o -> o.added(this, element));
}
public void add(E element) {
notifyElementAdded(element);
}
class SetObserverImpl1 implements Observer<E> {
@Override
public void added(ConcreteSubject<E> set, E element) {
System.out.println("added1: " + element);
}
}
class SetObserverImpl2 implements Observer<E> {
@Override
public void added(ConcreteSubject<E> set, E element) {
System.out.println("added2: " + element);
}
}
}
public class Main {
public static void main(String[] args) {
ConcreteSubject<String> subject = new ConcreteSubject<>();
ConcreteSubject<String>.SetObserverImpl1 ob1 = subject.new SetObserverImpl1();
subject.addObserver(ob1);
ConcreteSubject<String>.SetObserverImpl2 ob2 = subject.new SetObserverImpl2();
subject.addObserver(ob2);
subject.add("hello");
subject.add("world");
}
}
added1: hello
added2: hello
added1: world
added2: world
너무 대충 만들었나. ㅋㅋ
위의 코드처럼 내부 클래스를 사용해 함수를 Observer로 등록할 수도 있다.
하지만 내부 클래스를 사용하면 코드가 너무 많아지므로 코드 유지보수 관점에서 좋은 방법은 아니다.
📌 람다 표현식을 이용한 전달 방법
public class Main {
public static void main(String[] args) {
ConcreteSubject<String> subject = new ConcreteSubject<>();
subject.addObserver((set, element) -> System.out.println("added1: " + element));
subject.addObserver((set, element) -> System.out.println("added2: " + element));
subject.add("hello");
subject.add("world");
}
}
- 람다 표현식을 이용하면 간단하게 전달할 수 있다.