[Pattern] 옵저버(Observer) 패턴
굉장히 유명한 디자인 패턴이다. 꼭 알고 넘어가야한다.
이벤트를 관찰하는 패턴
옵저버 패턴은 한 클래스가 다른 클래스의 상태 변화를 관찰(Observe)하고자 할 때 구현하는 패턴이다. 예를 들어 클래스 A가 클래스 B를 인스턴스화 한 객체를 가지고 있고, 이 객체의 상태변화(Event)를 감지하여 그 데이터를 활용하고자 할 때 쓰인다. 이때 A클래스는 옵저버가 되기위해 옵저버를 구현(implement)하고, 그 옵저버는 B클래스에 등록(register)이 되어있어야 상태변화의 전달이 가능하다.
Java의 Swing 라이브러리나 안드로이드를 사용해 봤다면 다양한 영역에서 이벤트 리스너를 사용해 보았을 것이다. 이것이 대표적 사례로 거의 모든 GUI 툴킷에 쓰이는 편이다. 분산 이벤트 핸들링 시스템을 구현할 때 사용되며 발행/구독 모델이라고 하기도 한다. 옵저버 패턴은 모델-뷰-컨트롤러(MVC) 패러다임과 자주 결합되며, 모델과 뷰 사이의 느슨한 연결을 위해 쓰인다.
예시에서 쓰이는 용어를 먼저 정리하자면,
이벤트(Event) : 특정 변수 값의 변화
클래스 B : 관찰 당하는 대상으로 '이벤트를 발생시키는 주체', 자신을 구독한 옵저버들에게 이벤트를 알려준다.
클래스 A : B를 관찰하는 클래스로 옵저버를 구현하고 있다.
초기 상황은 다음과 같다. A는 B의 상태변화를 알 방법이 없다.
코드로 보면 다음과 같다.
fun main(args: Array<String>) {
EventPrinterA().start()
}
class EventPrinterA {
fun start(){
var eventGenerator = EventGeneratorB()
eventGenerator.generate()
}
}
class EventGeneratorB {
private var data = 0
fun generate(){
for(num in 1..10){
data = num
}
}
}
A에서 B클래스를 인스턴스화 하여 객체를 생성하였지만, B의 data 변수값의 변화를 A에서 출력하고자한다면 방법이 없다.
(여기서 B에서 그냥 출력하면 안되냐? 라고 할 수 있지만, 이건 이벤트의 발생 상황을 매우 간단한 예시로 보이는 것이다. 예를 들어 Swing 라이브러리나 안드로이드에서 클릭 이벤트를 감지하고자 할때, 직접 그 라이브러리 내부를 파고들어 어느 위치의 어느 클래스에서 print 함수를 두어야할지 바로 알아낼 수 있을까? 이런 시도 자체는 다소 무식한 방식일 뿐더러 시간의 낭비를 초래한다. 객체지향 프로그래밍을 하는 본질 자체를 무시한 것)
이제 우리는 A가 B의 상태변화를 감지하기 위해 A가 옵저버를 구현하도록 해아한다. 그림으로 그리면 다음과 같다.
구독을 하려면 A가 옵저버를 구현해야한다. 이제부터 리스너(Listener)라는 용어를 쓸 것이다. 리스너는 옵저버와 동일하게 쓰이는 말로 Swing이나 안드로이드를 접해본 사람이라면 아마 리스너라는 말이 더 익숙할 것이다.
이런 투박한 그림을 UML 스럽게 바꿔보면 다음과 같다.
여기서 콜백(Callback)이라는 용어가 등장한다. 리스너의 onEvent() 와 같은 함수를 통해 이벤트가 발생하면 각 옵저버는 이벤트 콜백을 받는다.
fun main(args: Array<String>) {
EventPrinterA().start()
}
class EventPrinterA: EventListener{
override fun onEvent(event: Int) {
print("이벤트 수신 : $event \n")
}
fun start(){
var eventGenerator = EventGeneratorB(this) // A가 B를 리스너를 통해 구독하는 시점
eventGenerator.generate()
}
}
interface EventListener {
fun onEvent(event: Int)
}
class EventGeneratorB(var listener: EventListener){ // B는 생성자를 통해 구독받는다
private var data = 0
fun generate(){
for(num in 1..10){
data = num
listener.onEvent(num)
}
}
}
A가 리스너를 구현하여 이것을 B의 생성자에 넘겨준다. B는 생성자를 통해 자신을 구독하고자 하는 리스너를 등록한다. B는 상태 변화(Event)가 발생할 때 리스너의 onEvent() 함수를 호출하여 변화된 상태 정보를 전달한다. 리스너를 구현하고 있는 A는 Override 한 onEvent() 함수를 통해 상태 변화시마다 데이터를 전달 받아 활용한다.
여기서 A클래스는 리스너를 implement 하는 방법으로만 할 필요는 없다. 다음과 같이 익명 객체를 활용해도 된다.
class EventPrinterA{
fun start(){
EventGeneratorB(object:EventListener {
override fun onEvent(event: Int) {
print("이벤트 수신 : $event \n")
}
}).generate()
}
}
느슨한 결합(Loose Coupling)
이제 옵저버 패턴의 UML 다이어그램을 살펴보자
리스너(옵저버)는 notify()라는 콜백함수를 가지고, 이러한 옵저버는 특정 이벤트 발생 주체를 구독한다. 이때 Subject로 불리는 이벤트 발생 주체는 자신을 구독하는 리스너를 Collection으로 관리한다. 우리는 예시를 통해 생성자로 리스너를 등록했지만 정석대로 한다면 register 메서드와 unregister 메서드로 등록/제거를 할 수 있도록 구현해야 맞다.
이렇게 옵저버 패턴을 이용하면 클래스 A와 클래스 B가 느슨하게 결합(Loose Coupling)되었다고 한다. 느슨하게 결합되었다는 건 뭔 소릴까? 느슨해서 안좋은거 아니야? 라고 생각할 수 있지만 설계에 있어서 강하게 결합된게 안좋다. (안좋다기 보단 개발 복잡성이 증가한다) 간단히 말하면 A클래스와 B클래스가 상호작용은 하지만 A와 B는 서로에 대해 의존성이 낮아 하나의 컴포넌트가 변경되더라도 다른 하나는 크게 영향 받지 않는다.
즉 느슨한 결합은 시스템에서 내부 의존성을 줄이기 위한 것으로 프레임워크의 안정성과 유연성이 증가한다.
옵저버 패턴에 비추어 보면,
1. Subject(이벤트 발생 주체)는 어떤 리스너가 자신을 구독했는지만 알 뿐, 리스너를 구현하는 구상 클래스(Concrete Observer) A가 무엇을 하는지는 알 필요도 없고 영향을 받지도 않는다.
2. 새로운 옵저버를 추가/변경해도, Subject는 그냥 늘 보내던 방식으로 데이터를 보내면 된다. Subject를 바꿀 필요가 없다.
3. Subject나 옵저버 모두 독립적으로 재사용 가능하다.
끝.
'Architecture & Engineering > Design Pattern' 카테고리의 다른 글
[GoF Design Pattern] 3. 데코레이터(Decorator) 패턴 (0) | 2023.06.18 |
---|---|
[GoF Design Pattern] 2. 옵저버(Obsever) 패턴 (0) | 2023.05.29 |
[GoF Design Pattern] 1. 전략(Strategy) 패턴 (0) | 2023.05.14 |
[Pattern] Android의 설계 패턴 2 : MVP by Java (0) | 2022.04.17 |
[Pattern] Android의 설계 패턴 1 : MVC by Java (0) | 2022.04.01 |
댓글