반응형

 iOS 개발자가 많이 하는 실수 - NotificationCenter addObserver 후 removeObserver 누락 문제

 

NotificationCenter는 iOS에서 매우 강력한 브로드캐스트 메커니즘이지만,
종종 observer를 제거하지 않아서 예상치 못한 동작, 중복 호출, 심지어 메모리 누수까지 경험하게 됩니다.


1. 문제 패턴: addObserver만 하고 removeObserver를 하지 않음

대표적인 코드:

class MyViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleNotification(_:)),
            name: .someNotification,
            object: nil
        )
    }

    @objc private func handleNotification(_ notification: Notification) {
        print("Received")
    }
}

여기서 removeObserver를 호출하지 않으면 다음과 같은 문제가 발생할 수 있습니다.

  • ViewController가 사라졌는데도 여전히 notification을 받으려고 시도
  • 동일 객체가 여러 번 addObserver되면 중복으로 콜백이 여러 번 호출
  • 내부적으로 참조가 남아서 메모리 릭 가능성 증가 (특히 커스텀 NotificationCenter 등)

2. 문제 증상

  1. 화면을 여러 번 열었다 닫으면 동일 이벤트가 여러 번 실행됨
    • print("Received")가 1번이 아니라 2번, 3번씩 찍힘
  2. 이미 해제되었어야 할 객체가 여전히 콜백에 반응
  3. 의도하지 않은 타이밍에 콜백이 발생하여 상태가 꼬임
  4. 전체적인 흐름을 파악하기 매우 어려운 “유령 이벤트” 발생

3. 전통적인 add/remove 패턴

3-1. addObserver + removeObserver

class MyViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleNotification(_:)),
            name: .someNotification,
            object: nil
        )
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    @objc private func handleNotification(_ notification: Notification) {
        print("Received")
    }
}
  • 최소한 deinit에서 removeObserver(self)를 호출해주면
    객체가 해제될 때 자동으로 구독이 해제됩니다.

다만, iOS 9 이후 block 기반 API가 추가되면서

패턴 자체가 조금 변했습니다.


4. block 기반 addObserver 사용 시 주의사항

4-1. block 기반 API

let id = NotificationCenter.default.addObserver(
    forName: .someNotification,
    object: nil,
    queue: .main
) { notification in
    print("Received")
}

이 방식은 기존 selector 기반과 달리 토큰(id)을 반환합니다.

이 토큰은 나중에 remove할 때 필요합니다.

NotificationCenter.default.removeObserver(id)

4-2. self를 캡처할 때의 문제 (10번 항목과 연계)

class MyViewController: UIViewController {
    private var token: Any?

    override func viewDidLoad() {
        super.viewDidLoad()

        token = NotificationCenter.default.addObserver(
            forName: .someNotification,
            object: nil,
            queue: .main
        ) { notification in
            self.handle(notification)   // ❌ self 강한 캡처
        }
    }

    deinit {
        if let token {
            NotificationCenter.default.removeObserver(token)
        }
    }

    private func handle(_ notification: Notification) {
        print("Received")
    }
}
  • self → token을 strong으로 참조
  • NotificationCenter → token을 strong으로 참조
  • 클로저 → self를 strong으로 캡처

상황에 따라 retain cycle이 형성될 수 있으므로,

클로저에서는 [weak self] 패턴을 주로 사용합니다.

token = NotificationCenter.default.addObserver(
    forName: .someNotification,
    object: nil,
    queue: .main
) { [weak self] notification in
    self?.handle(notification)
}

5. iOS 9+에서의 일반적인 권장 패턴

Apple은 iOS 9 이후로 block 기반 API를 선호하며,

이 방식은 자동으로 observer 해제를 처리해주는 장점이 있습니다.

하지만 여전히 다음은 개발자가 설계해야 합니다.

  • self 캡처 방식(weak/strong)
  • token을 어디에 저장하고 언제 해제할 것인지

실무에서 많이 쓰는 패턴:

class MyViewController: UIViewController {
    private var token: Any?

    override func viewDidLoad() {
        super.viewDidLoad()

        token = NotificationCenter.default.addObserver(
            forName: .someNotification,
            object: nil,
            queue: .main
        ) { [weak self] notification in
            self?.handle(notification)
        }
    }

    deinit {
        if let token {
            NotificationCenter.default.removeObserver(token)
        }
    }

    private func handle(_ notification: Notification) {
        print("Received:", notification)
    }
}

6. selector 기반 addObserver에서 흔한 실수

6-1. 동일 객체에 중복 addObserver

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    NotificationCenter.default.addObserver(
        self,
        selector: #selector(handleNotification),
        name: .someNotification,
        object: nil
    )
}

만약 viewWillAppear에서 계속 addObserver만 하고

viewWillDisappear / deinit에서 removeObserver를 하지 않으면:

  • 화면을 다시 들어올 때마다 observer가 하나씩 추가
  • Notification 발생 시 콜백이 N번 호출 → 디버깅 난이도 매우 높음

→ 이 케이스는 실무에서 자주 터지는 패턴입니다.

해결책

  • observer 등록 위치를 viewDidLoad 등 한 번만 호출되는 지점으로 옮기거나
  • viewWillAppear에서 등록했다면 viewWillDisappear에서 반드시 해제
override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    NotificationCenter.default.removeObserver(self)
}

7. Notification을 쓸 때의 대안과 설계 관점

Notification은 전역 브로드캐스트이기 때문에:

  • 흐름 추적이 어렵고
  • 남용 시 전체 코드베이스를 복잡하게 만들 수 있습니다.

가능하다면:

  • TCA, Combine, RxSwift, Delegation, Closure 콜백 등으로 대체
  • Notification은 진짜 전역 브로드캐스트가 필요한 상황에서만 사용

예: 로그인 상태 변경, 테마 변경, 큰 앱 구조에서의 전역 이벤트 등


8. 실무용 체크리스트

  1. addObserver를 호출한 위치는 어디인가? 
    • viewDidLoad / viewWillAppear / init 등
  2. removeObserver는 언제 호출되는가? 
    • deinit / viewWillDisappear / 종료 시점 등
  3. observer가 중복으로 등록될 가능성은 없는가? 
    • 반복적으로 add만 하고 있지 않은지
  4. block 기반 API를 쓴다면 token을 어디에 저장하고 해제하는가?
  5. 클로저에서 self를 어떻게 캡처하는가? ([weak self] 고려)

9. 요약

  • addObserver만 하고 removeObserver를 하지 않으면:
    • 중복 이벤트
    • 예측 불가 콜백
    • 메모리 누수 가능성
      이 발생한다.
  • selector 기반:
    • deinit 또는 적절한 라이프사이클 메서드에서 removeObserver(self) 호출
  • block 기반:
    • 반환된 token을 보관 → 해제 시 removeObserver(token) 호출
    • 클로저에서는 [weak self] 사용

핵심 문장:

Notification을 구독했다면, 어디에서 해제할지까지 항상 함께 설계해야 한다.

반응형
Posted by 까칠코더
,