ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • (2) Observer Pattern
    프로그래밍/Design Pattern 2023. 3. 5. 17:27

    새로운 알림이 올 때마다 하단 탭의 badge 숫자들이 갱신되어야 한다고 합니다. 이때 알림이 올 때마다 FCM 라이브러리의 특정 함수가 불려진다고 한다면 아래처럼 구현할 수 있을 것입니다.

     

    class ReceiveData {
        var 채팅탭_뱃지카운트: Int
        var 알림탭_뱃지카운트: Int
        var 전체_뱃지카운트: Int
        ...
    }
    
    class FCM {
        let chattingTab = ChattingTab()
        let alarmTab = AlarmTab()
        
        /// 라이브러리에 의해서 새로운 알림이 올 때마다 호출되어지는 함수
        func receiveNewAlert(data: ReceiveData) {
            let (채팅탭_뱃지카운트, 알림탭_뱃지카운트, 전체_뱃지카운트) = data
            chattingTab.update(채팅탭_뱃지카운트, 알림탭_뱃지카운트, 전체_뱃지카운트)
            alarmTab.update(채팅탭_뱃지카운트, 알림탭_뱃지카운트, 전체_뱃지카운트)
        }
        ...
    }
    
    class ChattingTab {
        func update(채팅탭_뱃지카운트, 알림탭_뱃지카운트, 전체_뱃지카운트) {
            // 뱃지 수를 업데이트
        }
    }
    class AlarmTab {
        func update(채팅탭_뱃지카운트, 알림탭_뱃지카운트, 전체_뱃지카운트) {
            // 뱃지 수를 업데이트
        }
    }

    위 코드에서는 FCM 클래스 내부의 receiveNewAlert함수 안에서 각 Tap화면 객체를 사용하여 각 객체에 구현된 update 함수를 직접 호출하고 있습니다. 여기서 개선할 수 있는 부분은 무엇일까요?

    우선 첫 번째로 1. FCM클래스 내부에서 각 화면 인스턴스의 함수를 직접 호출하고 있습니다. 이 말은 뱃지 수 표현이 필요한 새로운 화면이 추가될 때마다 새로운 화면을 위해서 FCM이 아래처럼 수정되어야 한다는 것을 의미합니다.

    class FCM {
        ...
        // 새로운 화면이 추가됨
        let moreTab = MoreTab()
        
        /// 라이브러리에 의해서 새로운 알림이 올 때마다 호출되어지는 함수
        func receiveNewAlert(data: ReceiveData) {
            let (채팅탭_뱃지카운트, 알림탭_뱃지카운트, 전체_뱃지카운트) = data
            chattingTab.update(채팅탭_뱃지카운트, 알림탭_뱃지카운트, 전체_뱃지카운트)
            alarmTab.update(채팅탭_뱃지카운트, 알림탭_뱃지카운트, 전체_뱃지카운트)
            
            // 뱃지 수 표현이 필요한 새로운 화면이 추가
            moreTab.update(채팅탭_뱃지카운트, 알림탭_뱃지카운트, 전체_뱃지카운트)
        }
        ...
        
    }
    
    class MoreTab {
        func update(채팅탭_뱃지카운트, 알림탭_뱃지카운트, 전체_뱃지카운트) {
            // 더보기탭 "More" 뱃지 수 = 전체 - (채팅탭 + 알림탭)로 계산...
        }
    }

     

    두 번째로 현재 코드에서는 모든 update함수 호출에 필요한 매개변수가 동일한 상태이기 때문에 2. 같은 인터페이스를 공유할 수 있게 할 수도 있습니다. 굳이 지금처럼 ReceiveData 값을 해제하고 함수호출할 때 넣지 않아도 될 것 같습니다. 하지만 여전히 남아있는 문제가 있습니다. moreTab인 경우에 뱃지 수를 계산하기 위해서 ReceiveData의 모든 값들이 필요하지만, 3. alarmTab의 경우 자신에게 필요하지 않은 값들을 (채팅탭뱃지수, 전체_뱃지카운트)를 가져간다는 문제가 있습니다.

     

    Observer 패턴은 한 객체의 상태가 바뀔 때 다수의 구독 객체에게 전파되는 방식을 가진 구조의 패턴입니다. 위 경우에서 FCM 객체는 전파객체(Subject), 각 Tap화면들은 FCM를 구독하는 객체에 해당됩니다.

    기존 개선 전 코드도 FCM내부에서 객체들의 update를 직접호출 하는 방법으로 위 구조를 따라간다고 볼 수도 있지만, Observer 패턴의 핵심은 Subject는 각 Observer들이 무엇인지 얼마나 있는지 관심이 없어야 한다는 점입니다.

    코드로 구현하면 아래와 같습니다.

     

    protocol Observer {
        func update(채팅탭_뱃지카운트, 알림탭_뱃지카운트, 전체_뱃지카운트)
    }
    
    class FCM {
        /// 구독자 배열을 선언
        var observerList = [Observer]()
        
        /// 라이브러리에 의해서 새로운 알림이 올 때마다 호출되어지는 함수
        func receiveNewAlert(data: ReceiveData) {
            let (채팅탭_뱃지카운트, 알림탭_뱃지카운트, 전체_뱃지카운트) = data
            observerList.forEach { observer in
                observer.update(채팅탭_뱃지카운트, 알림탭_뱃지카운트, 전체_뱃지카운트)
            }
        }
        /// 외부에서 Observer를 추가할 수 있게 됨
        func addObserver(_ observer: Observer) {
            observerList.append(observer)
        }
        /// 외부에서 Observer를 삭제할 수 있게 됨
        func removeObserver(_ observer: Observer) {
            for i, _observer in observerList.enumerated() {
                if _observer === observer {
                    observerList.remove(at: i)
                    break
                }
            }
        }
    }
    class ChattingTab: Observer {
        func update(채팅탭_뱃지카운트, 알림탭_뱃지카운트, 전체_뱃지카운트) {
            // 뱃지 수를 업데이트
        }
    }
    class AlarmTab: Observer {
        func update(채팅탭_뱃지카운트, 알림탭_뱃지카운트, 전체_뱃지카운트) {
            // 뱃지 수를 업데이트
        }
    }
    class MoreTab: Observer {
        func update(채팅탭_뱃지카운트, 알림탭_뱃지카운트, 전체_뱃지카운트) {
            // More 뱃지 수 = 전체 - (채팅탭 + 알림탭)
        }
    }

     

    1. FCM클래스 내부에서 각 화면 인스턴스의 함수를 직접 호출 하는 부분은 Observer 프로토콜을 구현하는 객체 배열을 순회하는 부분으로 바뀌면서 구독자가 얼마나 추가/삭제 되는지 상관없이 바뀌지 않는 영역으로 수정되었습니다.

     

    객체지향 원칙
    원칙1) 달라지는 부분을 찾아내어, 달라지지 않는 부분으로부터 분리한다.
    원칙2) 행동을 구현이 아닌, 인터페이스(프로토콜)에 "맞추어서 사용" 한다.
    원칙3) 상속이 아닌 구성(Composition)을 활용한다.

    즉 객체지향 원칙 중 하나인 "달라지는 부분을 찾아내어, 달라지지 않는 부분으로부터 분리"하였습니다.

    또 동시에 2. 같은 인터페이스를 공유되도록 하는 부분도 같이 수정되었네요.

     

    여전히 alarmTab의 경우 자신에게 필요하지 않은 변수(채팅탭뱃지수)를 가져가는 문제가 남아 있습니다. 같은 인터페이스를 공유하면서 발생된 문제이기 때문인데요,

    지금까지의 데이터 갱신 방법은 모두 데이터 갱신이 구독자 방향으로 이루어지는 push 방식이었습니다. push 과정에서 update함수의 시그니쳐가 update(채팅탭_뱃지카운트, 알림탭_뱃지카운트, 전체_뱃지카운트)로  동일했기 때문에 발생한 문제입니다. 

    이 경우 구독자가 직접 자기가 필요한 데이터를 다시 요청하게 (pull) 하면 됩니다.

    아래 그림에서 notify 부분이 push에 해당하고, get~~BadgeCount() 함수부분이 pull에 해당합니다.

     

    코드로 구현하면 대략 아래와 같습니다. Subject인 FCM에서는 파라미터가 없는 update함수만을 호출하고 있고, 각 Observer의 update안에서 필요한 값들만 Subject에게 요청하여 가져오고 있습니다.

    이 방법으로 3. 자신에게 필요하지 않은 값들을 (채팅탭뱃지수, 전체_뱃지카운트)를 가져간다는 문제 도 해결 할 수 있습니다.

     

    protocol Subject {
        // 채팅탭 뱃지 수를 가져오는 함수
        func getChattingBadgeCount() -> Int
        // 알림탭 뱃지 수를 가져오는 함수
        func getAlarmBadgeCount() -> Int
        // 더보기탭 뱃지 수를 가져오는 함수
        func getMoreBadgeCount() -> Int
    }
    class FCM: Subject {
        var observerList: [Observer]
        var newAlertReceive: Bool = false
        
        private var data = ReceiveData(.zero)
        ...
        
        /// 라이브러리에 의해서 새로운 알림이 올 때마다 호출되어지는 함수
        func receiveNewAlert(data: ReceiveData) {
            //
            self.data = data
            
            observerList.forEach { observer in
                observer.update()
            }
        }
        func addObserver(_ observer: Observer) {
            observerList.append(observer)
        }
        func removeObserver(_ observer: Observer) {
            for i, _observer in observerList.enumerated() {
                if _observer === observer {
                    observerList.remove(at: i)
                    break
                }
            }
        }
        func getChattingBadgeCount() -> Int {
            return data.채팅탭_뱃지카운트
        }
        func getAlarmBadgeCount() -> Int {
            return data.알림탭_뱃지카운트
        }
        func getMoreBadgeCount() -> Int {
            return data.전체_뱃지카운트 - (data.채팅탭_뱃지카운트 + data.알림탭_뱃지카운트)
        }
    }
    
    class AlarmTab: Observer {
        ...
        func update(subject: Subject) {
            let badgeCount = subject.getAlarmBadgeCount()
            // 이후 획득한 뱃지 수로 업데이트
            ...
        }
    }
    class ChattingTab: Observer {
        ...
        func update(subject: Subject) {
            let badgeCount = subject.getChattingBadgeCount()
            // 이후 획득한 뱃지 수로 업데이트
            ...
        }
    }
    class MoreTab: Observer {
        ...
        func update(subject: Subject) {
            let badgeCount = subject.getMoreBadgeCount()
            // 이후 획득한 뱃지 수로 업데이트
            ...
        }
    }

     

    참고로 Foundation 프레임워크의 NotificationCenter클래스가 위 Observer 패턴의 추상화된 구현체에 해당합니다.

    위 코드의 Subject가 ObserverList를 관리하고 직접 notify 했다면, NotificationCenter는 notify(dispatch)하는 부분도 제어할 수 있습니다.

     

    정리

    Observer Pattern은 객체의 상태 변화를 관찰하는 Observer들, 즉 Observer들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 객체가 직접 목록의 각 Observer에게 알려주하도록 하는 패턴.

     

    반응형

    '프로그래밍 > Design Pattern' 카테고리의 다른 글

    (3) Decorator Pattern  (0) 2023.03.13
    (1) Strategy Pattern  (0) 2023.03.05

    댓글

Designed by Tistory.