옵저버 패턴과 NotificationCenter
- Swift
옵저버 패턴(Observer Pattern)
[Design Pattern] 내가 보려고 정리하는 Swift 디자인 패턴 (5) - 옵저버 패턴(Observer Pattern with NotificationCenter)
옵저버 패턴은 한 객체에서 일어나는 정보를 다른 특정한 여러 객체들에게 전달할 때 사용하는 디자인 패턴이다.
여기서 발생하는 정보, 일어나는 정보는 어떤 모델이나 타입의 추가, 수정, 삭제와 같은 변경사항들, 타입에서 발생하는 다양한 이벤트들을 의미한다고 생각하면 쉽다.
옵저버 패턴에 대하여 간략하게 개념을 좀 정리해보면 아래와 같다.
- 한 객체의 상태가 변경될 때
해당 객체(Object, Subject, Publisher)가 자신의 상태 변경을 등록한 여러구독자(Observer)에게 알린다. - 객체 간의
1:N관계를 통해 타입의 추가, 수정, 삭제 및 이벤트 발생과 같은 변화를 구독자들에게브로드캐스팅한다. - 각각의
구독자(Observer)들은 필요시 Subject에 등록하여(addObserver, subscribe 등) Publisher가 변경 사항을 Notify할 때 자동으로 다양한 메서드들을 활용해 변경 내용을 수신할 수 있다.
백문이 불여일견. 일단 직접 해보자.
직접 해보면서 옵저버 패턴 이해하기
[Design Pattern] 내가 보려고 정리하는 Swift 디자인 패턴 (5) - 옵저버 패턴(Observer Pattern with NotificationCenter)
옵저버 패턴에 대해 쉽게 이해하기 위해서 유튜브라는 예시를 들어보자.
대곰이라는 유튜브 채널이 있고, 그 채널에 대해 _구독_한 다른 사람들이대곰이 채널에 새로운 영상이 올라올 때마다알람을 받고자한다.
그렇다면, 아래와 같이 용어가 정리될 것이다.
Subject, Publisher (주체) =
대곰이채널
→ 옵저버들이 관심있게 보는 주체이자 객체. (특정한 이벤트가 발생되거나 상태가 변경되는 객체)Observer, Subsciber (관찰자, 구독자) = 대곰이 채널을 구독한 다른 사람들
→ 관심있는 주제의 상태변화를 관찰하고, 반응하는 객체attach, addObserve, Subscibe = 구독, 등록의 행위
→ 관찰자가 특정 주체를 관찰하게 되는 것 (Observer 객체 내의 어떤 저장 공간에 포함되는 것)Notify = 알림 (새로운 영상이 등록되었어요!)
→주체가 상태 변화를 관찰자에게 알려주는 것.
Subject(Publisher) 주체 정의하기 (대곰이 채널)
protocol Subject {
func addObserver(_ observer: Observer)
func notifyObserver(_ post: Post)
}
옵저버 패턴은 1:N이 기본이다. 발행자는 1명, 구독자는 N명이 될 수 있기 때문에 자주 사용되는 메서드를 추상화시키자.
addObserver(_:): 대곰이 채널을 구독합니다! (Subject를 Observe)notifyObserver(_:): 구독자들에게 새로운 영상 업로드 사실을 보냅니다! (Observer 들에게 상태 변화 전송)
그 후 실제 대곰이 채널을 구현해보자
class DaeGom: Subject {
// 구독자(옵저버)들의 리스트
private var observers = [Observer]()
// 새로운 구독자 추가
func addObserver(_ observer: Observer) {
observers.append(observer)
}
// 유튜브 영상 업로드 시 알람 메서드를 호출하여 업로드 사실 전송
func addVideo(_ post: Post) {
notifyObserver(post)
}
// 보관중인 모든 옵저버들에게 영상 업로드 알림 발송
func notifyObserver(_ post: Post) {
observers.forEach {
$0.update(post: post)
}
}
}
// 간단한 Post 모델
struct Post {
let content: String
}
Observer 정의하기 (구독자들)
// 옵저버들이 사용할 공통 메서드
protocol Observer {
func update(post: Post)
}
class Subscriber: Observer {
private let name: String
init(name: String) {
self.name = name
}
func update(post: Post) {
print("\(name)님, 대곰이 채널에 새로운 영상이 업로드 되었어요!")
}
}
실제 실행
let daegom = DaeGom() // 주체 생성
let user1 = Subscriber(name: "대현") // 구독자 생성
let user2 = Subscriber(name: "길동")
daegom.addObserver(user1) // 구독자 추가
daegom.addObserver(user2) // 구독자 추가
let newVideo = Post(content: "재미있는 옵저버 공부하기")
daegom.addVideo(newVideo) // 발행
// 대현님, 대곰이 채널에 새로운 영상이 업로드 되었어요!
// 길동님, 대곰이 채널에 새로운 영상이 업로드 되었어요!
- 주체(Subject)가 돨 주체 객체를 생성한다.
- user1과 user2는 Subject에 대해 알림 설정을 신청하였다.
addObserver() - Subject에서 새로운 영상이 추가된 순간(
addVideo) 연결된 구독자들에게 모두 알람이 전달된다.
다양한 옵저버 패턴들
Swift 디자인패턴 Observer Pattern (옵저버 패턴)
Observation | Apple Developer Documentation
NotificationCenter | Apple Developer Documentation
위에 있는 예시 코드는 옵저버 패턴을 간단하게 설명해주는 코드이다.
Swift 생태계에서는 대표적인 옵저버 패턴 구현체(도구)가 존재한다.
1. Combine (Apple, iOS 13 +)
Combine은 Apple에서 제공하는 옵저버 패턴의 공식적인 프레임워크이다.
Publisher→Subscriber구조- 데이터의 흐름을 선언적으로 표현할 수 있다. (map, filter, combineLatest, assign, sink 등 다양한 연산자를 지원한다.)
- SwiftUI와 매우 긴밀하다. (@Published, @ObservableObject)
import Combine
class ViewModel: ObservableObject {
@Published var text: String = ""
}
let vm = ViewModel()
var cancellables = Set<AnyCancellable>()
vm.$text
.sink { newValue in
print("텍스트 변경됨: \(newValue)")
}
.store(in: &cancellables)
vm.text = "Hello, Combine!"
// 출력: 텍스트 변경됨: Hello, Combine!
2. Observation (iOS 17+, Swift 5.9+)
Observation은 Swift에서 가장 최근이 출시된 프레임워크이다.
- KVO와 Combine의 후속 프레임워크이다.
- @Observable 기반이다.
- 단순하고 가볍다!
import Observation
@Observable
class Profile {
var name: String = "대곰"
}
let profile = Profile()
// SwiftUI View
struct ContentView: View {
@Bindable var profile: Profile
var body: some View {
Text(profile.name) // name 바뀌면 자동 UI 업데이트
}
}
3. RxSwift (서드파티)
RxSwift는 Combine보다 다양한 기능을 가지고 있다.
- Observable + Observer 패턴 기반이며 연산자가 300개 넘게 있다..
- iOS 커뮤니티에서 가장 오래된 인지도와 생태계를 가지고 있다!
- 대신 러닝 커브가 높고.. 서드파티다 보니 외부 라이브러리에 의존성이 필요하다.
import RxSwift
let disposeBag = DisposeBag()
let subject = PublishSubject<String>()
subject
.subscribe(onNext: { value in
print("새로운 값: \(value)")
})
.disposed(by: disposeBag)
subject.onNext("Hello RxSwift!")
// 새로운 값: Hello RxSwift!
4. NotificationCenter (Foundation)
NotificationCenter는 Foundation에 내장되어있는 가장 기본적인 옵저버 패턴 구현체 이다.
- 앱 전역에 구독자들을 대상으로 이벤트 브로드캐스팅이 가능하다.
- 문자열 기반 키(Notification.name)을 사용한다.
- 하지만, 문자열 기반 키다보니 타입의 안정성이 낮다..
- 메모리 관리와 해제 시점 관리에 주의가 필요하다.
- 작은 앱이나 간단한 이벤트 전달에 유용하다.
let notificationName = Notification.Name("NewPost")
// 구독자
let observer = NotificationCenter.default.addObserver(
forName: notificationName,
object: nil,
queue: .main
) { notification in
if let content = notification.userInfo?["content"] as? String {
print("새 영상: \(content)")
}
}
// 발행자
NotificationCenter.default.post(
name: notificationName,
object: nil,
userInfo: ["content": "새로운 영상이 추가되었어요!"]
)
// 새 영상: 새로운 영상이 추가되었어요!
다양한 옵저버 패턴들을 정리해보면?
- 간단한 전역 이벤트: NotificationCenter
- SwiftUI + 최신 프로젝트: Observation
- iOS 13~ 지원 + MVVM 구조: Combine
- 복잡한 리액티브/멀티플랫폼 경험: RxSwift
NotificationCenter 사용 예시
한 번 직접 NotificationCenter를 UIKit 기반으로 사용해보자.
시나리오는 아래와 같다.
- 사용자가 버튼을 누르면 Notification을 발행한다. (Post)
- 다른 VC에서 해당 알람을 받으면 UILabel을 업데이트한다.
1. Notification 정의
우선, 발행자가 구독자에게 어떤 이름으로 변경사항을 보낼 것인지 정의해보자.
extension Notification.Name {
static let didTapButton = Notification.Name("didTapButton")
}
- 사용자가 버튼을 클릭하면 구독자에게
didTapButton이라는 이름의 알림을 전송한다.
2. 발행자 정의
// MARK: - 발행자 (Publisher)
class FirstViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let button = UIButton(type: .system)
button.setTitle("알림 보내기", for: .normal)
button.addTarget(self, action: #selector(postNotification), for: .touchUpInside)
button.center = view.center
button.frame.size = CGSize(width: 200, height: 50)
view.addSubview(button)
}
@objc private func postNotification() {
NotificationCenter.default.post(
name: .didTapButton,
object: nil,
userInfo: ["message": "버튼이 눌렸습니다!"]
)
}
}
스토리보드가 아닌 코드 베이스로 간단하게 작성해보았다.
- 사용자는 버튼을 클릭할 시
postNotification()이 실행되어 구독자들에게didTapButton이라는 이름으로message: 버튼이 눌렸습니다!라는 데이터가 전달된다.\
3. 구독자 정의
class SecondViewController: UIViewController {
private let label = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemYellow
label.text = "아직 알림 없음"
label.textAlignment = .center
label.frame = view.bounds
view.addSubview(label)
// 옵저버 등록
NotificationCenter.default.addObserver(
self,
selector: #selector(handleNotification(_:)),
name: .didTapButton,
object: nil
)
}
@objc private func handleNotification(_ notification: Notification) {
if let message = notification.userInfo?["message"] as? String {
label.text = message
}
}
deinit {
// 옵저버 해제 (메모리 누수 방지)
NotificationCenter.default.removeObserver(self)
}
}
구독자는 viewDidLoad() 시점에 addObserver를 사용하여 발행자를 등록한다.
그 후 등록한 발행자에게 새로운 알람이 수신된다면 selector가 실행되어 handleNotification 이 실행되어 Label을 업데이트한다.