2017년 3월 21일 화요일

Notification Snippets

복잡한 뷰 컨트롤러들을 다루면서 여기 저기 소속된 데이터를 다루려고 할 때 소유권 때문에 곤란함을 느낄 때가 많았다. 이럴 때 NotificationCenter 를 이용해 알림(Notification)을 던지는 방식으로 컨트롤러 객체들끼리 통신을 하기도 하는데, 뭐 하여간 이런 저런 여러 사유로 노티피케이션(Notification 혹은 NSNotification)을 사용할 일이 종종 있다.

하지만 항상 쓸 때 마다 느끼는데, 이 알림(Notification)의 이름(Name)을 찾는데 곤혹을 느끼곤 한다. 왜나하면 한군데에 정리된 것이 아닌 전역 상수 형태로 선언된 것이 대부분이기 때문이다.

물론 개인이 구현하는 프로젝트에서 쓸 Notification 은 굳이 이런 전통을 따를 필요는 없다. 누구나 한번 쯤은 해 볼 만한 Notification Snippets 를 한번 만들어 보자.

enum 방식의 구현 실험

Notification 의 목록을 정의하는 전통적인 방법은 앞서 이야기한 그냥 전역 상수 형태이다. 하지만 이 방식은 찾는데 귀찮다는 단점이 있다. 모든 것을 이름으로 구분해야 하고 그래서 그룹을 구분지을 때도 난감할 때가 있다.

이런 그룹화, 즉 개별 Notification 이 어떤 그룹에 묶여있는 경우라면 아마도 좋은 방법이 떠오를 것이다. 바로 열거형(enumeration) 이다.

아래처럼 두 가지 알림 방식을 구현해 봤다.
enum MyNotification {
  case good, bad
}
물론 아직 아니다. 이것 만으로는 그냥 MyNotification 이라는 특수한 타입만 정의된 것일 뿐 기존의 환경이 바뀐 것은 아니다.

필자는 이 열거형을 Notification 의 이름, 즉 Notification.Name 을 대체하기 위해 만들었다. 따라서 'MyNotification 의 실데이터(raw value) 타입을 Notification.Name 으로 고르면 될 것 같다!' 라고 생각할 수 있겠지만, 불행히도 이 특수한 타입은 enum 에는 쓸 수가 없다.

대신 name 을 가져올 수 있는 다른 방법을 만들어보자.
enum MyNotification {
  case good, bad

  var name: Notification.Name {
    switch self {
    case .good: return Notification.Name("good-notification")
    case .bad: return Notification.Name("bad-notification")
    }
  }
}
이제 name 을 가져올 수 있게 되면서 어떻게든 쓸 수 있는 방법이 생겼다. 아마도 옵저버(Observer)를 등록 할때는 아래처럼 쓰게 될 것이다.
// Register Notification Observer
NotificationCenter.default.addObserver(forName: MyNotification.good.name,
                                       object: nil,
                                       queue: OperationQueue.main) {
  (notification) in
  print("Get Notification: \(notification)")
  // ...
}
역시 Notification 을 쏠(Post) 때는 아래처럼 할 수가 있다.
// Post Notification
NotificationCenter.default.post(name: MyNotification.good.name, object: nil)
이 쯤 되면 왜 내가 이런 병신같은(???) 짓을 하고 있는가 느낄 수 있다. 굳이 enum 으로 만들어 쓰는게 그다지 편하지는 않다. 심지어 하나의 이름을 추가할 때 마다 name 게터도 수정해야 하니 해야 할 짓(?)이 두배다. 단지 이런 이름들을 하나의 네임스페이스에 몰아둠으로써 에디터(IDE)로 편집할 때 찾아 쓰기 편하다는 장점 하나만 있고 나머진 단점 같다. :-S

굳이 그 타입에 엮일 필요가 없다

​자 이제 불편하다는 점에 대해 생각하고 고쳐보자. 굳이 Notification.Name 이라는 타입에 연연할 필요 없이 그냥 자신만의 타입으로 만들어 버리자. 다만 실제값(raw value)의 타입을 문자열로 정의해보자.
enum MyNotification: String {
  case good = "good-notification"
  case bad = "bad-notification"
}
그냥 문자열 타입의 값을 가질 수 있는 열거형으로 정의했다. 이제 그냥은 못 쓴다. 다시 처음 입장으로 돌아온 것이나 다름없다.

대신 좀 더 생산적인 방법을 만들어 보자. 나만의 감시(observe) 및 포스팅(post) 시스템이다.
enum MyNotification: String {
  case good = "good-notification"
  case bad = "bad-notification"
  
  static func observe(_ name: MyNotification, 
                      handler: @escaping (Notification) -> Void) {

    NotificationCenter.default.addObserver(forName: Notification.Name(name.rawValue), 
                                           object: nil, 
                                           queue: OperationQueue.main) { 
      (notification) in
      handler(notification)
    }
  }
  
  static func post(_ name: MyNotification, object: Any?) {
    NotificationCenter.default.post(name: NSNotification.Name(name.rawValue), 
                                    object: object)
  }
}
아예 열거형이 정적 메소드(static method)를 가짐으로써 NotificationCenter 를 직접 건드릴 필요가 없도록 만들어 버렸다. 즉 아래처럼 쓸 수 있게 되었다.
// Register Notification Observer
MyNotification.observe(.good) {
  (notification) in
  print("received good notification")
}

// Post Notification
MyNotification.post(.good, object: nil)
사용하기가 놀랍도록 단순하고 직관적으로 변했다.

잠깐! 그건 이제 내꺼야!

그런데 왜 굳이 정적메소드를 만들었을까 하는 생각이 드는가? 이게 왠지 어울릴 것 같아서 그렇게 만들었다. 물론 짧은 생각일지도 모른다. 아래처럼 static 을 모조리 떼어버릴 수도 있을테니까.
enum MyNotification: String {
  case good = "good-notification"
  case bad = "bad-notification"

  func observe(handler: @escaping (Notification) -> Void) {
    NotificationCenter.default.addObserver(forName: Notification.Name(self.rawValue), 
                                           object: nil, 
                                           queue: OperationQueue.main) { 
      (notification) in
      handler(notification)
    }
  }

  func post(object: Any?) {
    NotificationCenter.default.post(name: NSNotification.Name(self.rawValue), 
                                          object: object)
  }
}
이제 이 녀석을 이용하면 아래처럼 쓸 수 있게 되었다.
// Register Notification Observer
MyNotification.good.observe {
  (notification) in
  print("received good notification")
)

// Post Notification
MyNotification.good.post(object: nil)
어떤 모양이 더 마음에 드는가는 역시 개인 취향일 것 같다. 만약 영어권 사람이라면 다르게 구현할테지만 우린 한국어를 쓰니까 이렇게 써도 부담 없이 읽혀진다.

여기까지가 내가 생각해본 Notification 의 개인 취향화(?) 였다.

Bonus: English Friendly(?)

앞서 영어권 사람이라면 이상하게 느낄지도 모른다는 말이 계속 가슴에 맴돌아서 구현해본 방식이다. 그냥 전역 함수(function)로 구현했다는 차이만 있다.
enum MyNotification: String {
  case goodNotificatio = "good-notification"
  case badNotification = "bad-notification"
}

func post(_ notification: MyNotification, object: Any?) {
  NotificationCenter.default.post(name: Notification.Name(notification.rawValue), 
                                  object: object)
}

func observe(_ notification: MyNotification, handler: @escaping (Notification) -> Void) {
  NotificationCenter.default.addObserver(forName: Notification.Name(notification.rawValue),
                                         object: nil, 
                                         queue: OperationQueue.main) { 
    (receivedNotification) in
    handler(receivedNotification)
  }
}
대신 사용할 때 표현을 보면 영어권 사람들에게 좀 더 직관적일지도 모르겠다.
// Register Notification Observer
observe(.goodNotification) { (notification) in
  print("received good notification")
}

// Post Notification
post(.goodNotification, object: nil)
물론 개인적으로 이런 방식은 그다지 선호하지 않는다. 한국인이니까​ ;-)

부록: 구조체로 변신시키기

애초의 enum 과 어울린다는 생각을 하지 않았다면 다른 방법을 생각할 수가 있다. 바로 struct 로 구조화 시켜 버리는 것이다. 물론 enum 을 쓰는 것과 큰 차이는 없을지도 모르겠지만 한번 보자.
struct MyNotification {
  let rawName: String
  var name: Notification.Name {
    return Notification.Name(rawName)
  }
  
  static let good = MyNotification(rawName: "good-notification")
  static let bad = MyNotification(rawName: "bad-notification")

  static func observe(_ noti: MyNotification, handler: @escaping (Notification) -> Void) {
    NotificationCenter.default.addObserver(forName: noti.name, 
                                           object: nil, 
                                           queue: OperationQueue.main) { 
      (notification) in
      handler(notification)
    }
  }
  
  static func post(_ noti: MyNotification, object: Any?) {
    NotificationCenter.default.post(name: noti.name, object: object)
  }
}
이 버전은 MyNotification 의 구조체 버전이다. 그것도 앞서 enum 으로 post 와 observe 메소드를 정적으로 구현했던 그 모양과 거의 동일하다. 그냥 struct 로 구현했다는 것만 차이가 있다.

심지어 사용법조차 enum 으로 구현했던 것과 동일하다.
// Register Notification Observer
MyNotification.observe(.good) {
  (notification) in
  print("received good notification")
}

// Post Notification
MyNotification.post(.good, object: nil)
물론 이 코드도 정적 메소드가 아닌 동적 메소드 형태로 바꾸면 역시나 enum 의 두 번째 예제와 같은 방식으로 사용 가능해질 것이다. 뭐가 좋은지야 개인 취향이니 넘어가자. :-)

마무리

물론 이런 구현이나 개인이 생각하는 나름에 따라 여러 방법으로 나누어질 수 있다. 중요한건 그게 아니라 용도에 맞게 설계하고 사용하는 것이다. 내 글도 관점에 따라 병신같이 보일 수도 있다.

이보다 중요한 것은, 지금까지 remove observer 를 하는 과정을 몽땅 빼먹었다는 점이다. 물론 지금까지 나온 코드에 이걸 추가로 구현하는건 어려운 일이 아니니 생략해도 무리는 없겠지만, 혹시나 잊어먹었다면 observer 를 반드시 제거해 주는 코드를 적재적소에 집어넣자. 안그러면 사라져야할 객체가 사라지지 않거나 이런 저런 똥(?)싸고 죽는 오류를 겪게 될 것이다.

[관련글] NSNotification, NSNotificationCenter 기초 가이드​(Objective-C 버전)
[관련글] 스위프트(Swift) 가이드​

댓글 없음 :