포켓몬빵으로 이해하는 ObserverPattern & NotificationCenter

2025. 8. 27. 16:59·iOS

작년인가? 포켓몬 빵 대란이 일어진 사건을 기억하시나요???

다들 띠부씰을 얻기 위해서 포켓몬빵 오픈런을 하면서 오박사님이 "ㅅㄱ!"
를 외치거나 다른 사람들과 가위바위보까지 하는 사건이 일어났습니다 😱

 

그러면 포켓몬 빵 사려고 온 사람들은 어쩔수 없이 다시 집으로 갔다가 계속 "아 언제나와!!!" 하면서 주변 편의점 알바들을 괴롭혔죠.

 

갑자기 왜 포켓몬 빵이냐!! 바로 Observer Pattern을 사용했다면 당신은 오픈런을 하지 않아도 되었기 때문이죠!!!

Observer Pattern

 

왼쪽이 이전에 계속 편의점들을 괴롭혔던 포켓몬 빵 사냥꾼들이었다면, 오른쪽은 편의점에서 지인들에게

"야 포켓몬빵 나왔어 빨리 가져가!!" 하고 알려주는 방식이에요.

 

이게 바로 Observer Pattern의 핵심입니다!

 

정의와 목적

Observer pattern은 객체들 간에 일대다(one-to-many) 의존성을 정의하면서도 객체들을 강하게 결합시키지 않는 것을 목표로 합니다. 한 객체의 상태가 변경되면, 의존하는 모든 객체들이 자동으로 알림을 받고 업데이트됩니다.

 

Observer Pattern이 해결하는 문제

// 문제 상황: 강한 결합 - 편의점이 모든 사냥꾼을 알아야 함
class ConvenienceStore {
    var pokemonBreadStock: Int = 0 {
        didSet {
            if pokemonBreadStock > 0 {
                // 모든 사냥꾼들을 하드코딩으로 알림
                hunter1.notifyPokemonBreadAvailable()
                hunter2.notifyPokemonBreadAvailable()
                hunter3.notifyPokemonBreadAvailable()
                // 새로운 사냥꾼이 생기면? 이 코드를 수정해야 함!
            }
        }
    }
    
    var hunter1: PokemonBreadHunter!
    var hunter2: PokemonBreadHunter!
    var hunter3: PokemonBreadHunter!
}

이 코드의 문제점이 뭘까요?

  1. 강한 결합: 편의점이 모든 사냥꾼들을 알아야 함
  2. 확장성 부족: 새로운 사냥꾼 추가 시 편의점 코드 수정 필요
  3. 유지보수 어려움: 사냥꾼이 포기하면 관련 코드도 모두 수정해야 함

Observer Pattern은 이런 문제를 해결하기 위해 subscription mechanism을 제공합니다.

여러 객체가 관찰하고 있는 객체에서 발생하는 이벤트에 대해 알림을 받을 수 있도록 하죠.

Observer Pattern의 핵심 구성 요소

Observer Pattern을 포켓몬빵 상황으로 설명해볼게요! 🍞🍞

1. Subject (주체/발행자) - 편의점

protocol PokemonBreadStore {
    func subscribe(hunter: PokemonBreadHunter)
    func unsubscribe(hunter: PokemonBreadHunter)
    func notifyHunters()
}

편의점은 포켓몬빵 재고 상태를 가지고 있는 객체입니다. 재고가 들어오면 등록된 모든 사냥꾼들에게 알림을 보내죠.

2. Observer (관찰자/구독자) - 포켓몬빵 사냥꾼

protocol PokemonBreadHunter {
    func update(store: PokemonBreadStore)
    func getName() -> String
}

포켓몬빵을 노리는 헌터들은 편의점의 재고 상태에 관심이 있는 객체입니다. 편의점으로부터 알림을 받으면 빛의 속도로 달려가야하죠! 💨

3. ConcreteSubject (구체적인 주체) - 실제 편의점

class ConvenienceStore: PokemonBreadStore {
    private var hunters: [PokemonBreadHunter] = []
    private var pokemonBreadStock: Int = 0
    private let storeName: String

    init(storeName: String) {
        self.storeName = storeName
    }
    
    //1. 구독을 하면
    func subscribe(hunter: PokemonBreadHunter) {
        hunters.append(hunter)
        print("🏪 \(storeName): \(hunter.getName())님이 알림을 신청하셨습니다!")
    }
    
    //2. 입고가 될때마다 구독자들에게 공지를 해줍니다
    func notifyHunters() {
        print("📢 \(storeName): 포켓몬빵이 입고되었습니다! (재고: \(pokemonBreadStock)개)")
        
        for hunter in hunters {
            hunter.update(store: self)
        }
    }
}

4.  ConcreteObserver(구체적인 관찰자)

class RegularHunter: PokemonBreadHunter {
    private let name: String
    private let desiredQuantity: Int
    
    init(name: String, desiredQuantity: Int = 1) {
        self.name = name
        self.desiredQuantity = desiredQuantity
    }
    
    func update(store: PokemonBreadStore) {
        guard let convenienceStore = store as? ConvenienceStore else { return }
        
        print("🏃‍♂️ \(name): 알림 받았다! \(convenienceStore.getStoreName())로 달려간다!")
}}

5. 상황 시뮬레이션

// 편의점 생성
let store = ConvenienceStore(storeName: "GS25 강남점")

// 다양한 사냥꾼들 생성
let hunter1 = RegularHunter(name: "포켓몬 매니아 철수")
let hunter2 = RegularHunter(name: "직장인 영희", desiredQuantity: 2)
store.subscribe(hunter: hunter1)
store.subscribe(hunter: hunter2) 

// 포켓몬빵 입고
store.restockPokemonBread(quantity: 8)
🏪 GS25 강남점: 포켓몬 매니아 철수님이 알림을 신청하셨습니다!
🏪 GS25 강남점: 직장인 영희님이 알림을 신청하셨습니다!
📢 GS25 강남점: 포켓몬빵이 입고되었습니다! (재고: 8개)
 포켓몬 매니아 철수님이 포켓몬빵 1개를 구매했습니다
 직장인 영희님이 포켓몬빵2개를 구매했습니다

이런 형식으로 예시 출력이 이루어집니다

 

이제 어느정도 Observer Pattern 에 대해 감이 오셧죠?? 그러면 iOS에서 이를 구현한 NotificationCenter에 대해 파보겠습니다

NotificationCenter

위에서 보면 편의점에서 누가 구독을 했는지를 알아야 공지를 띄워줄수 있었잖아요???

class PokemonBreadStore {
    func restockBread() {
        // 편의점은 누가 관심있는지 몰라도 됨!
        NotificationCenter.default.post(name: .pokemonBreadRestocked, object: self)
    }
}

NotificationCenter의 핵심 장점은 바로 분리입니다. 편의점은 누가 포켓몬빵에 관심있는지 알 필요가 없어요! 

NotificationCenter 내부 동작 원리

NotificationCenter가 어떻게 동작하는지 내부 구조를 추측해보면서 이해해볼게요

1. Hash Table 구조 사용

NotificationCenter는 내부적으로 Hash Table 구조를 사용해서 O(1) 검색 성능을 보장합니다:

// 내부적으로 이런 구조
[Notification.Name: [Observer]] = [
    "pokemonBreadRestocked": [사냥꾼1, 사냥꾼2, 사냥꾼3],
    "pokemonBreadSoldOut": [사냥꾼1, 사냥꾼4]
]

2. 동기적 실행

등록된 순서대로 동기적으로 실행됩니다. 즉, 모든 사냥꾼들이 알림을 받고 처리가 끝나야 다음 코드가 실행돼요.

3. 스레드 안전성

여러 스레드에서 동시에 observer를 추가하거나 notification을 post해도 안전하게 동작합니다.

내부적으로 lock을 사용해서 동시성 문제를 해결하기 때문이에요.

 

NotificationCenter 사용법

1. 네이밍분리

extension Notification.Name {
    // 네임스페이싱으로 충돌 방지
    enum PokemonBread {
        static let didRestock = Notification.Name("com.myapp.pokemonbread.didRestock")
        static let didSellOut = Notification.Name("com.myapp.pokemonbread.didSellOut")
        static let priceChanged = Notification.Name("com.myapp.pokemonbread.priceChanged")
    }
}

물론 바로 저렇게 extension 없이 바로 사용이 가능해요.

하지만 여러군데에서 긴 스트링을 입력하다 보면 오타가 발생할수 있으니 저렇게 하는 것을 추천드려요!!

 

2. 알림을 받기 위해 Observer 등록

// 기본 observer 등록
NotificationCenter.default.addObserver(
    self,
    selector: #selector(breadRestocked(_:)),
    name: .pokemonBreadRestocked,
    object: nil // 모든 편의점에서 오는 알림을 받음
)
  • observer: 알림을 받을 주체(대상 객체)를 의미합니다.
    주로 self를 넘겨서 현재 뷰컨트롤러나 클래스가 알림을 받도록 해요.
  • selector: 알림을 받았을 때 실행할 메서드를 지정 → 즉, “이 알림이 오면 이 메서드를 호출해줘!” 인거죠
  • name: 어떤 알림을 받을지 구분하는 식별자
    문자열을 직접 쓰기보다는 Notification.Name 타입으로 정리해 두면 오타를 방지하고, 네임스페이스 관리가 쉬워져요.
  • object: 누가 보낸 알림인지를 한정할 떄 사용
    • nil: 모든 발송자에게서 오는 해당 알림을 받을게~~@
    • 특정 객체: “이 객체에서 보낸 알림만 받고 싶다”라는 조건.

예를 들어, 여러 개의 UITextField가 .textDidChangeNotification을 보낼 때,

텍스트필드 하나에서만 발생한 알림만 받고 싶다면 object를 그 텍스트필드로 지정하면 됩니다.

 

3. Notification 발송

  // 상세 정보와 함께 발송
NotificationCenter.default.post(
    name: .pokemonBreadRestocked,
    object: self,
    userInfo: [
        "storeName": "GS25 강남점",
        "quantity": quantity,
        "totalStock": stock,
        "timestamp": Date()
    ]
)
  • name: 알림의 종류를 구분하는 식별자입니다.
    Observer 등록 시 지정한 name과 일치해야 합니다.
  • object: 알림을 보낸 주체를 지정
    예를 들어, PokemonStore 객체에서 알림을 보냈다면 object에 self를 넘겨줄 수 있어요.
  • userInfo: 알림과 함께 추가적인 데이터를 딕셔너리 형태로 전달할 수 있습니다.
    • 예: "storeName": "GS25 강남점", "quantity": 30, "timestamp": Date()
    • 주로 알림의 맥락을 더 풍부하게 전달할 때 사용합니다.
    • 단, 키 값은 문자열이므로 개발자가 사전에 잘 정리해 두지 않으면 충돌이나 오타가 발생하기 쉽습니다. 그래서 보통 상수 정의나 enum을 통해 관리하는 방식을 씁니다.

언제  object  vs  userInfo 를 쓰면 좋을까요?

  • object → “누가 보냈는지” 한정 지을 때 유용합니다
    • 예: 특정 버튼 인스턴스, 특정 모델 객체, 특정 뷰컨트롤러
  • userInfo → “어떤 내용이 바뀌었는지” 구체적인 데이터를 담을 때 유용
    • 예: 수량, 이름, 시간, 상태값 등 알림에 딸려오는 정보들

Closure로 사용하는 방법

앞서 포켓몬빵 예시에서는 Selector 방식을 사용했는데, 실제로는 클로저 방식도 많이 사용됩니다. 두 방식의 차이점을 비교해보겠습니다! 

 

1. Selector

// Observer 등록
NotificationCenter.default.addObserver(
    self,
    selector: #selector(breadRestocked(_:)),
    name: .pokemonBreadRestocked,
    object: nil
)

// 메서드 구현
@objc func breadRestocked(_ notification: Notification) {
    print("포켓몬빵이 입고되었습니다!")
}

// 해제
NotificationCenter.default.removeObserver(self)

사실 기존에 앞에서 했던 것은 Objective - C 기반의 전통적인 방식이라 @objc 키워드가 필요했죠/

2. 클로저 방식 (iOS 9.0+)

// Observer 등록 (클로저 방식)
let token = NotificationCenter.default.addObserver(
    forName: .pokemonBreadRestocked,
    object: nil,
    queue: .main
) { notification in
    print("포켓몬빵이 입고되었습니다!")
    // 클로저 내부에서 바로 처리 가능
}

// 해제 (토큰 사용)
NotificationCenter.default.removeObserver(token)

특징:

 

  • 인라인 처리 가능 (별도 메서드 불필요)
  • 반환된 토큰으로 관리
  • Queue 지정 가능

 

클로저 방식을 사용할 때 가장 중요한 부분이 바로 토큰 관리입니다.

NSObjectProtocol인 이유

저도 처음에 이게 왜 NSObjectProtocol인지 궁금했어요.

NotificationCenter가 내부적으로 생성한 observer 객체를 반환하는데 이 객체는 NSObject를 상속받고 있습니다

따라서 NSObjectProtocol 타입으로 반환되는 거죠

 

 

 

배열로 관리하는 이유

뷰컨트롤러에서 여러 알림을 구독하는 경우가 많기 때문이에요:

이렇게 하면 deinit에서 한 번에 모든 observer를 정리할 수 있어서 메모리 누수를 방지할 수 있습니다.

 

또 주의해야 하는 것이 순환 참조입니다

클로저는 주변 context를 캡처하기 때문에 항상 순환 참조를 조심해야합니다

class BreadViewController: UIViewController {
    private var token: NSObjectProtocol?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        token = NotificationCenter.default.addObserver(...) { notification in
            self.updateUI() // 여기서 self를 강하게 캡처
        }
    }
    
    deinit {
        print("❌ 과연 이게 호출이 안될까?")
    }
}

 

순환 참조 구조

  1. BreadViewController → 강한 참조 → token (프로퍼티로 보관)
  2. token 내부 클로저 → 강한 참조 → self(BreadViewController)
  3. NotificationCenter → 강한 참조 → token

➡️ 이렇게 되면 BreadViewController가 deinit 시점에 해제되지 않고 살아있게 됩니다.

NotificationCenter with MVC

MVC 패턴에서 NotificationCenter는 주로 모델(Model) → 컨트롤러(Controller) 로의 이벤트 전달에 쓰입니다.

모델은 변화가 생겼을 때 직접 뷰를 알지 않아야 합니다. 느슨한 결합이죠
→ 뷰와의 강한 결합을 피하고, “나는 바뀌었다”라는 사실만 알리면 됩니다

 

여기서 NotificationCenter는 모델이 컨트롤러에게 신호를 보내는 중간 다리 역할을 하는거죠

 

이렇게 MVC에서 모델과 뷰/컨트롤러의 결합도를 낮출 수 있습니다

 

NotifcationCenter의 단점

지금까지 장점만 이야기했다면 당연히 모든 기술에는 Side-Effect가 있겠죠?

제가 실제로 사용해보면서 느낀 것은 코드 품질을 해칠 수 있다는 것이였습니다

 

1. 관계가 불분명해집니다

  • Delegate는 “A → B”처럼 1:1 명확한 관계를 보여줍니다.
  • 반면, NotificationCenter는 여러 곳으로 흩어져서 전파되기 때문에, “이 알림을 누가 듣고 있는지” 추적하기 어려워집니다.

2. 이는 자연스럽게 디버깅과 추적이 어려워집니다

  • 이벤트 발생지와 수신지가 분산되어 있어 플로우를 추적하기가 어려웠습니다
  • 런타임에서까지 어떤 객체가 알림을 받는지 예측하기가 어려웟죠

3. 성능이슈

  • 모든 Observer가 알림을 받으므로 필터링 로직이 각 Observer에서 중복 실행 -> 대량일 경우? 성능 저하
1:N 브로드캐스트 특성상 백그라운드 뷰들도 모두 이벤트를 받아 의도치 않은 상태 변경이 발생하고, 추후 해당 뷰 진입 시 예상치 못한 동작을 야기할 수 있으므로 NotificationCenter 남발은 피해야 합니다.

참고문헌

https://refactoring.guru/design-patterns/observer

 

Observer

/ Design Patterns / Behavioral Patterns Observer Also known as: Event-Subscriber, Listener Intent Observer is a behavioral design pattern that lets you define a subscription mechanism to notify multiple objects about any events that happen to the object t

refactoring.guru

https://noah-ios.dev/observer-pattern/

 

Observer Pattern

안녕하세요 Noah입니다 :) 오늘은 Gang of Four 소프트웨어 디자인 패턴 중 행위 패턴에 속하는 Observer Pattern 에 대해서 알아보도록 하겠습니다. iOS에서 Observer Pattern은 Foundation framework의 NotificationCenter

noah-ios.dev

 

'iOS' 카테고리의 다른 글

객체 간 통신(delegate, Closure, NotificationCenter, KVO)  (0) 2025.09.04
PHPickerController의 UTI 활용법  (4) 2025.08.29
내 아이폰에 터치를 해보았다(Reverse-preorder DFS)  (3) 2025.08.26
SceneDelegate가 UIResponder를 상속하는 이유  (2) 2025.08.25
근본으로돌아가자(7)-String,Array으로 시작해서 Sequence까지  (0) 2025.04.04
'iOS' 카테고리의 다른 글
  • 객체 간 통신(delegate, Closure, NotificationCenter, KVO)
  • PHPickerController의 UTI 활용법
  • 내 아이폰에 터치를 해보았다(Reverse-preorder DFS)
  • SceneDelegate가 UIResponder를 상속하는 이유
2료일
2료일
좌충우돌 모든것을 다 정리하려고 노력하는 J가 되려고 하는 세미개발자의 블로그입니다. 편하게 보고 가세요
  • 2료일
    GPT에게서 살아남기
    2료일
  • 전체
    오늘
    어제
    • 분류 전체보기 (133)
      • SWIFT개발일지 (28)
        • ARkit (1)
        • Vapor-Server with swift (3)
        • UIkit (2)
      • 알고리즘 (25)
      • Design (6)
      • iOS (42)
        • 반응형프로그래밍 (12)
      • 디자인패턴 (6)
      • CS (3)
      • 도서관 (2)
  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
2료일
포켓몬빵으로 이해하는 ObserverPattern & NotificationCenter
상단으로

티스토리툴바