안드로이드 Observer(관찰자) 패턴

Lee Jun Hyeong·2022년 12월 26일
0

📌 Observer(관찰자) 패턴이란

관찰자 패턴은 객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록해서 상태변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 알리도록 하는 디자인 패턴이다. 이 패턴의 핵심은 옵저버 또는 리스너라 불리는 하나 이상의 객체를 관찰 대상이 되는 객체에 등록시킨다. 그리고 각각의 옵저버들은 관찰 대상인 객체가 발생시키는 이벤트를 받아 처리한다. 이벤트가 발생하면 각 옵저버는 콜백을 받는다.

옵저버 패턴은 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들에게 연락이가고 자동으로 정보가 갱신되는 1:N의 관계를 정의하고, 주체와 옵저버가 느슨하게 결합된 디자인 패턴이다.

✅ 예를 들어

  1. 사용자가 키보드를 눌렀을 때
  2. 사용자가 어떤 버튼을 터치했을 때
  3. 호출한 API의 응답 데이터가 수신됐을 때

정리하자면, 아무도 함수로 직접 요청한 적 없지만 시스템에 의해 발생하는 동작을 이벤트라고 한다.
옵저버 패턴을 활용하면 다른 객체의 상태 변화를 별도의 함수 호출 없이 즉각적으로 알 수 있기 때문에, 이벤트에 대한 처리를 자주 해야 하는 프로그램이라면 매우 효율적인 프로그램으로 작성할 수 있다.

📌 옵저버 패턴 구현 원리

이벤트를 발생하는 클래스 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의 배수 이벤트 만들기

이 때, 생성자로 '리스너' 를 받게 된다. 이벤트 발생 시마다 전달받은 리스너의 메소드를 호출해줄 것이다.

// 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
profile
"왜" 사용하며, "어떻게" 사용하는지에 대해

0개의 댓글