관찰자 패턴은 객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록해서 상태변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 알리도록 하는 디자인 패턴이다. 이 패턴의 핵심은 옵저버 또는 리스너라 불리는 하나 이상의 객체를 관찰 대상이 되는 객체에 등록시킨다. 그리고 각각의 옵저버들은 관찰 대상인 객체가 발생시키는 이벤트를 받아 처리한다. 이벤트가 발생하면 각 옵저버는 콜백을 받는다.
옵저버 패턴은 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들에게 연락이가고 자동으로 정보가 갱신되는 1:N의 관계를 정의하고, 주체와 옵저버가 느슨하게 결합된 디자인 패턴이다.
정리하자면, 아무도 함수로 직접 요청한 적 없지만 시스템에 의해 발생하는 동작을 이벤트라고 한다.
옵저버 패턴을 활용하면 다른 객체의 상태 변화를 별도의 함수 호출 없이 즉각적으로 알 수 있기 때문에, 이벤트에 대한 처리를 자주 해야 하는 프로그램이라면 매우 효율적인 프로그램으로 작성할 수 있다.
이벤트를 발생하는 클래스 B 가 있고, 이 B 클래스가 발생하는 이벤트를 수신받고 싶어하는 클래스 A 가 있는 상황을 가정해보자.
아래 그림 처럼, 먼저 클래스 A 에서 B 의 이벤트를 수신받기 위해 클래스 B 를 인스턴스화 한 뒤, B 가 자신에게 이벤트가 발생할 때마다 클래스 A 가 갖고있는 메소드를 호출하도록 시키는 것이다.
하지만 이 구조는 틀렸다.
클래스 B 는 이벤트를 정상적으로 발생하고 있지만, A 가 B 를 일방적으로 인스턴스화 한 상황이기 때문에, B 가 자신을 인스턴스화 한 대상에게 접근을 할 방법이 전혀 없다는 점이다. 사실 당연한 이야기이다. 따라서 B 는 A 의 메소드를 호출하지 못 한다.
위 예시에서는 A가 중간에 종을 만들어, B 가 이벤트가 발생할 때마다 A 가 만들어둔 종을 울리도록 한다. A 는 종이 울릴 때마다 이를 알아차리고 이벤트가 감지됐을 때 수행할 동작을 자연스럽게 하는 플로우가 나온다.
즉, 둘 사이에 인터페이스
를 하나 끼워넣는 방식이다. A 는 인터페이스를 상속하여 이벤트가 발생할 때마다 실행되게 할 메소드를 구현해둔다. 그리고 B 를 생성할 때 인터페이스 구현체를 전달하여, 이벤트가 발생할 때마다 생성자로 전달받은 A가 구현한 인터페이스 메소드를 호출하면 된다.
1부터 100까지 하나씩 세면서 5의 배수를 만날 때마다 이벤트를 발생하는 녀석과, 5의 배수 이벤트를 관찰하고 있다가 이벤트가 수신될 때마다 그 5의 배수를 출력하는 녀석을 구현해볼 것이다.
이벤트를 수신받는 녀석과 이벤트를 발생하는 녀석을 이어줄 리스너를 만들어준다.
// 이 인터페이스를 상속받아, 이벤트가 발생할 때마다 호출할 메소드를 구현하면 됨
interface EventListener {
fun onEvent(count: Int)
}
이 때, 생성자로 '리스너' 를 받게 된다. 이벤트 발생 시마다 전달받은 리스너의 메소드를 호출해줄 것이다.
// 5의 배수가 감지되면 이벤트를 발생하는 Counter
class Counter(var listener: EventListener) { // 생성자로 EventListener 넘겨받음
fun count() {
for (i in 1..100) { // 1부터 100까지 숫자 세기
if (i % 5 == 0) {
listener.onEvent(i)
}
}
}
}
EventListener
를 상속받아,onEvent()
메소드를 구현해준다. 이렇게 되면,start()
가 호출되었을 때 자신이 구현한EventListener
구현부가Counter()
의 생성자로 전달되고, 5의 배수가 발생할 때마다print("${count}-")
가 실행되어 화면에 5의 배수가 연이어 출력되게 된다.
// 이벤트를 수신받았을 때 화면에 5의 배수를 출력하는 EventPrinter
class EventPrinter: EventListener {
// 리스너를 상속받아 콜백 메소드를 구현함 (5의 배수 출력)
override fun onEvent(count: Int) {
print("${count}-")
}
fun start(){
// this 를 통해 EventListener 구현부를 넘겨줌 (다형성 활용!)
Counter(this).count() //
}
}
main() 메소드에서 EventPrinter() 를 인스턴스화 하여 start() 를 호출
// EventPrinter : 이벤트를 수신해서 출력하는, 인터페이스 구현체
// Counter : 숫자를 카운트 하면서 5의 배수가 감지되면 이벤트를 발생시키는 클래스
// EventListener : 위 두 요소를 연결지어줄 옵저버 (리스너)
fun main() {
EventPrinter().start()
}
구현한 EventPrinter 클래스를 보면 EventListener 를 상속받아 구현하고 있다.
하지만 코틀린에서는 '익명 객체' 를 통해, 임시로EventListener
를 상속받아 구현한 객체를 만들어 생성자로 넘겨줄 수 있다.
class EventPrinter {
fun start(){
val counter = Counter(object: EventListener {
override fun onEvent(count: Int) {
print("${count}-")
}
})
counter.count()
}
}
옵저버 패턴은 다른 객체의 상태변화를 감지함에 있어 객체끼리 느슨하게 결합되는 형태이다.
의존성을 제거하고 시스템을 보다 유연하게 해주는 효과가 있다.
상태가 변화 (이벤트 발생) 한 것을 감시하고 있는 객체들에게 직접 알리는 것이 아니기 때문에 불필요한 코드를 줄이는 효과가 있다.
옵저버 패턴만 봐도 알 수 있듯 다양한 디자인 패턴의 구현과 유연한 코드를 위해 인터페이스
에 대한 높은 이해가 필요하다.
- 참고
https://onlyfor-me-blog.tistory.com/306
https://velog.io/@haero_kim/%EC%98%B5%EC%A0%80%EB%B2%84-%ED%8C%A8%ED%84%B4-%EA%B0%9C%EB%85%90-%EB%96%A0%EB%A8%B9%EC%97%AC%EB%93%9C%EB%A6%BD%EB%8B%88%EB%8B%A4