Observer Pattern (옵저버 패턴)
옵저버 패턴은 특정 객체를 관찰(Observe)하다가, 해당 객체의 상태가 변경된다면
이를 관측하고 있는 객체들에게 연락을 하는 일대다(one-to-many) 의존성을 정의합니다.
사실 자바 Swing, 안드로이드, 프론트엔드 등 GUI 프로그래밍을 하신 분들이라면
버튼을 하나 만들고, 이를 클릭하면 특정 이벤트를 발생시키기 위해 onClickListenr 등을 사용해본 경험이 있으실텐데요.
이와 같은 Listener들은 버튼을 관측하고 있다가, 사용자가 버튼을 누루면 이벤트를 발생시키는
Observer 패턴의 일종인 Listener 패턴입니다.
버튼 클릭 리스너를 떠올리니,
'한 객체를 관측하고 있다가 해당 객체의 상태가 변화되면 이벤트를 발생시킨다'는게 어떤 의미인지 와닿죠?
pull 기반의 프로그래밍
public class Main {
public static void main(String[] args) {
int value = 0;
System.out.println("value = " + value);
value = 10;
System.out.println("value = " + value);
value = 20;
System.out.println("value = " + value);
}
}
// 출력
0
10
20
value의 값이 변경될 때 마다 값을 출력하려면,
변경될 때 마다 print문을 넣어줬습니다.
매번 값이 바뀔 때 마다 값을 불러온다(pull)고 하여 pull형 프로그래밍으로 불립니다.
하지만 int value의 값이 바뀔 때 마다 print문이 실행되게 하려면 어떻게 해야 할까요?
push 기반으로 다시 만들어보기
Subscriber 인터페이스
public interface Subscriber {
void notifyUpdated(int value);
}
구독(Subscribe) / 관찰(Observe) 하는 역할을 할 Subscriber 인터페이스를 정의하였습니다.
해당 인터페이스를 구체화하여 알람을 받을 수 있을 겁니다.
Subscriber를 implements한 Person 클래스
public class Person implements Subscriber {
private final String name;
public Person(String name) {
this.name = name;
}
@Override
public void notifyUpdated(int value) {
System.out.println(name + " : 업데이트 된 값을 받았어요 -> " + value);
}
}
위 인터페이스를 implements한 Person 클래스를 하나 만들었습니다.
value 값이 업데이트 할 때 마다 notifyUpdated 메소드가 실행될 예정입니다.
해당 메소드를 override하여 value 값이 업데이트 될 때 마다 어떤 동작을 할지 코드를 작성하였습니다.
ObservableInt 클래스
import java.util.ArrayList;
import java.util.List;
public class ObservableInt {
private int value;
private final List<Subscriber> subscribers;
public ObservableInt(int value) {
this.value = value;
subscribers = new ArrayList<>();
}
public void setValue(int value) {
this.value = value;
subscribers.forEach(subscriber -> subscriber.notifyUpdated(value));
}
public int getValue() {
return value;
}
public void addSubscriber(Subscriber subscriber) {
subscribers.add(subscriber);
}
}
관측가능한 Int라는 의미로 OberableInt 클래스를 정의하였습니다.
Subscriber를 받으면,
value 값이 업데이트 될 때 마다 Subscriber들의 notifyUpdated 메소드를 실행하여 value 값과 함께 알림을 줍니다.
Main 클래스에서 실행해보기
public class Main {
public static void main(String[] args) {
ObservableInt observableInt = new ObservableInt(0);
Person person1 = new Person("철수");
Person person2 = new Person("영희");
Person person3 = new Person("민수");
observableInt.addSubscriber(person1);
observableInt.addSubscriber(person2);
observableInt.addSubscriber(person3);
observableInt.setValue(10);
observableInt.setValue(20);
observableInt.setValue(30);
}
}
// 출력
철수 : 업데이트 된 값을 받았어요 -> 10
영희 : 업데이트 된 값을 받았어요 -> 10
민수 : 업데이트 된 값을 받았어요 -> 10
철수 : 업데이트 된 값을 받았어요 -> 20
영희 : 업데이트 된 값을 받았어요 -> 20
민수 : 업데이트 된 값을 받았어요 -> 20
철수 : 업데이트 된 값을 받았어요 -> 30
영희 : 업데이트 된 값을 받았어요 -> 30
민수 : 업데이트 된 값을 받았어요 -> 30
이제 ObserableInt의 값이 변경될 때 마다,
Subscriber의 notifyUpdated 메소드가 호출되며, 해당 메소드 안에 작성한 print문이 실행됩니다.
값이 변경될 때 마다 각 객체들에게 잘 전달이 되는 모습입니다.
Observer 패턴 + Template Callback 패턴 섞어보기
앞서 알아보았던 전략(Strategy) 패턴의 파생 패턴인 Template Callback Pattern을 섞어봅시다.
혹시 잘 모르시겠다면 아래 포스팅을 참고해주세요.
https://uknowblog.tistory.com/343
public class Main {
public static void main(String[] args) {
ObservableInt observableInt = new ObservableInt(0);
observableInt.addSubscriber(new Subscriber() {
@Override
public void notifyUpdated(int value) {
System.out.println("익명 클래스 : 업데이트 된 값을 받았어요 -> " + value);
}
});
observableInt.setValue(1000);
observableInt.setValue(2000);
observableInt.setValue(3000);
}
}
// 출력
익명 클래스 : 업데이트 된 값을 받았어요 -> 1000
익명 클래스 : 업데이트 된 값을 받았어요 -> 2000
익명 클래스 : 업데이트 된 값을 받았어요 -> 3000
addSubscriber에 Subscriber 인터페이스를 구체화한 클래스를 미리 생성해 넘겨주는 것이 아닌,
익명 클래스를 하나 만들어 notifyUpdated 메소드를 즉석에서 오버라이딩 하였습니다.
myButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this, "버튼이 클릭되었습니다.", Toast.LENGTH_SHORT).show();
}
});
자바 Swing 혹은 안드로이드의 SetOnclickListener와 다소 비슷해보이죠?
OnClickListener, OnKeyListener, TextWatcher등 Listener들은
Observer 패턴과 Strategy / (파생)Template Callback 패턴을 섞어 사용했다고 볼 수 있을 것 같습니다.
Observer + Template Callback Pattern + 코틀린
-- Kotlin --
fun main() {
val observableInt = ObservableInt(10)
observableInt.addSubscriber {
println("알람을 받았어요! 값 : $it")
}
observableInt.value = 20
observableInt.value = 30
observableInt.value = 40
}
// 출력
알람을 받았어요! 값 : 20
알람을 받았어요! 값 : 30
알람을 받았어요! 값 : 40
코틀린에서는 오버라이딩 할 메소드가 하나뿐일 땐 클래스명과 메소드명을 생략해도 되기 때문에
addSubscriber { } 라는 아주 간단하게 표현할 수 있습니다.
Pull 기반 프로그래밍 vs Push 기반 프로그래밍
Pull 기반 프로그래밍
기존의 코드는 데이터가 변경되면,
변경될 때 마다 혹은 일정 주기마다 관찰자가 해당 객체에게 필요한 데이터를 가져(Pull)옵니다.
Push 기반 프로그래밍
반면 Push 기반의 프로그래밍은 데이터가 변경될 때 마다 관찰자에게 데이터를 밀어주는(Push) 방식입니다.
주체의 데이터가 변경될 때 마다 관찰자에게 알림을 보냄으로써,
이벤트 기반 시스템에서 주로 사용됩니다.
Pull vs Push : 우편 배달 📪
우편 시스템 📪 을 예로 들면,
우편 배달부가 제 우편함에 우편을 놓고 가더라도, 저는 모릅니다.
때문에 기다리는 우편이 있을 경우, 저는 몇 분 혹은 몇 시간마다 우편함에 가서 확인해야 합니다.
즉 제가 우편함(주체)의 상태 변화를 체크하여 데이터(우편)을 가져오는(Pull) 방식이죠.
하지만 시대가 좋아져서 우체국이 알림톡 시스템을 개시했습니다!
우편배달부가 우편을 두고가면, 알림톡을 준다네요!
우편함(주체) 의 상태가 변했을 때, 우편배달부가 알림을 보냅(Push)니다!
저는 이제 알림을 받았을 때에만 우편을 가지러 간다는 특정 행동을 하면 됩니다! 😃
옵저버 패턴과 Push 기반의 프로그래밍의 원리로써,
리액티브 프로그래밍(Reactive Programming)의 기본 원리 중 하나이기도 합니다.
리액티브 프로그래밍은 디자인 패턴 시리즈에서 다루기엔 조금 맞지 않는 내용이니,
추후에 관련 포스팅을 올릴 예정입니다.
옵저버 패턴의 장점
- 느슨한 결합(Loose Coupling)과 확장에 유리
- 주체와 옵저버는 서로 독립적으로 존재하기 때문에 느슨한 결합이 유지됩니다.
- 클래스의 결합도가 낮아지면 유지보수와 확장에 용이하며,
- OCP(Open-Closed Principle)를 만족시킬 수 있습니다.
- 분리된 관심사
- 주체는 데이터의 상태 관리에 집중하며, 각각의 옵저버들은 자체적인 작업에 집중합니다
- 이는 주체에 영향을 주지 않고, 새로운 기능 혹은 이벤트를 추가할 수 있습니다.
- Push / 이벤트 기반 프로그래밍
- 주체에서 발생하는 이벤트를 옵저버가 전달받고, 작업하기 때문에 이벤트 기반 시스템에서 주로 사용됩니다.
후기
Strategy 패턴을 준비할 때, 생각보다 다소 오래걸려 다음껀 이것보다는 쉽겠지...라고 생각했으나
Observer 패턴 편은 준비하는데 더 오래 걸렸던 것 같습니다 🤣
전체 코드는 아래 Github를 참고해주세요.
https://github.com/yoon6763/design-pattern
'CS 지식 > 디자인 패턴' 카테고리의 다른 글
[디자인 패턴] Adaptor(어댑터) 패턴 (1) | 2024.05.08 |
---|---|
[디자인 패턴] Singleton(싱글톤) 패턴 (1) | 2024.03.16 |
[디자인 패턴] Factory(팩토리) 패턴 (0) | 2024.03.10 |
[디자인 패턴] Decorator (데코레이터) 패턴 (1) | 2023.09.12 |
[디자인 패턴] Strategy(전략) 패턴 + Template Callback Pattern (0) | 2023.06.20 |