개요
TodoMate라는 프로젝트를 진행하면서 다음과 같은 기존 상황에서 리팩토링을 진행하고 있었다
- TodosInMonthView
- 특정 유저(userId)의 이번달 [Todo]를 보여주는 뷰
- TodoListView
- 특정 유저(userId)의 오늘날짜의 [Todo]를 보여주는 뷰
- TodoManager
- 실시간 업데이트가 적용되는 [Todo]를 가지고 filter를 통해 각각의 userId에 알맞은 TodoListView에 전달

기존에는 그냥 간단하게 TodoManager의 todos에 filter를 적용하여 TodoListView에 [Todo]를 전달했다
TodoManager는 실시간 업데이트와 CRUD 작업을 수행하면서 상태를 가지는데, 다시보니 너무 많은 역할을 가지고 있다고 생각했고
TodoListView도 todos를 상태로 관리하는게 향후 유지보수에 좋다고 생각을 하고, 실시간 업데이트가 TodosInMonthView에도 이뤄지기를 생각하며 리팩토링을 시작하게 되었다

그런데 기존과 다르게 TodoManager에서 모든 userId의 [Todo]를 한 번에 관리하지 않으면서, 어떻게 하면 리스닝에 감지된 데이터를 알맞은 뷰로 전달할 수 있을까?
또한, TodosInMonthView는 특정 유저의 이번달 [Todo]를 사용하는데, 실시간 변경사항이 여기도 수월하게 적용되게 하기 위해 어떻게 할까?
힌트는 놀랍게도, 이 글의 제목에 있다
실시간 변화를 감지하는 객체에 옵저버를 등록하여, 관련있는 옵저버의 상태를 업데이트하여 최신정보를 유지하면 된다
Observer Pattern
Observer Pattern은 객체 간의 일대다 종속성을 정의하는 디자인 패턴이다
한 객체의 상태가 변경되면 그 객체에 의존하는 모든 객체들이 자동으로 통지받고 갱신된다
이때 통지하는 요소를 Subject(주체)라고 하며, 의존하는 객체를 Observer(관찰자)라고 한다
Observser Pattern을 적용하기 위해서는 다음과 같은 과정을 거친다
- Observer Interface 정의
- Subject가 Notify 시, 수행할 수 있는 Observer의 동작을 정의
- Subject 구현
- Subject는 [Observer]를 관리하며, 특정 상태 변화에 대해 알맞은 Observer들을 호출
- Observer 등록/해제 메서드 구현
- Observer 구현
- 정의한 Observer Interface를 구현
- Subject에서 구현한 Observer 등록/해제 메서드 이용하여 Observer를 등록 및 해제 구현
Delegate Pattern과 유사성?
Delegate Pattern은 애플 프레임워크 개발환경에서 엄청나게 이용되는 패턴이다
주로 프로토콜로 동작들을 정의하고, 이를 채택하여 구현한 객체를 Delegatee라고 하며
이 객체를 .delegate로 설정하여 사용하는 객체를 Delegator라고 한다(실제로는 Delegatee의 동작을 수행하기에 위임자다)
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
if let window = NSApplication.shared.windows.first {
window.delegate = self
}
}
}
extension AppDelegate: NSWindowDelegate {
func windowWillClose(_ notification: Notification) {
NSApplication.shared.terminate(nil)
}
}
예를 들어, 위의 코드처럼 NSApplication.shared.windows는 Delegator로 윈도우와 관련된 작업을 하는데
Delegate로 등록된 AppDelegate를 통해 윈도우를 닫는 이벤트에 대한 처리를 위해서
NSWindowDelegate인 delegate.windowWillClose 메서드를 호출한다
이처럼 뭔가 미리 등록해놓고, 이벤트가 발생하면 Delegate를 수행하는 것이 옵저버 패턴이랑 비슷한 것 같았다
그래서 둘의 차이를 찾아보니 다음과 같은 일반적인 차이들이 존재했다
우선 Observer는 일대다 관계지만, Delegate는 보통 일대일 관계다
또한, Observer는 상태 변경 통지에 주로 이용되지만, Delegate는 특정 작업의 책임을 다른 객체에 위임할 때 사용한다
그리고 Observer는 여러 옵저버를 관리하는 리스트 등으로 관리되지만, Delegate는 대게 단일 .delegate 객체만 참조한다
Pub/Sub Pattern과 관계

Observer Pattern은 Subject의 변화를 Observer가 바로 감지하지만,
Pub/Sub Pattern은 비슷하지만 Event Channel을 거쳐서 연결을 우회하여 느슨한 결합을 형성한다(이 패턴은 Combine의 기반이며, 이에대한 자세한 내용은 Combine 에 정리했다)
Pub/Sub Pattern은 전통적인 Observer Pattern의 개념을 확장하고 현대적인 프로그래밍 패러다임에 맞게 발전시킨 형태라고 볼 수 있다
Pub/Sub Pattern은 Event Channel이 존재하다보니 처리할 수 있는 데이터의 양을 제어할 수 있는 백프레셔 처리가 가능하다
또한, Observer Pattern에서는 일반적으로 모든 Observer가 모든 알림을 받지만, Pub/Sub Pattern에서 Subscribers는 특정 주제나 채널을 구독하여 원하는 메시지만 받을 수 있다
리팩토링
개요에서 언급한 부분들을 해결하기 위한 내용을 다음과 같이 정리했다
기존 TodoManager는 다음과 같이 역할을 나눴다
- TodoRealtimeService
- 실시간 데이터 감지를 수행 + Observer들을 관리하는 Subject
- TodoService
- CRUD 작업을 수행

TodoListView와 TodosInMonthView는 각각 ViewModel을 가지는데 ViewModel은 [Todo] 상태를 가진다
TodoRealtimeService는 Subject고, 각 ViewModel은 Observer로
Subject에 Observer들을 등록하여 실시간 데이터가 감지되면, 이를 Observer에 전달하여 처리하고 뷰모델의 [Todo]를 알맞게 업데이트한다
이때, Observer가 수행하게 될 동작은 TodoObserver 프로토콜을 통해 구현하도록 한다
Subject의 observers는 Observer들이 존재하는데, Observer는 TodoObserver를 준수하기에 메서드를 Subject에서 수행시키는 코드를 작성할 수 있다(이러한 점이 Delegate 같다고 이전에 언급한 것이다)
Service와 Manager 그리고 DIContainer
이번 리팩토링을 진행하면서 Service라는 명칭과 Manager라는 명칭을 언제 사용해야 하는가에 대한 나름의 기준을 만들며 좀 더 잘 이해할 수 있었다
우선, Manager는 ViewModel과 같은 느낌이지만, 좀 더 광범위한 뷰에 적용되는 ViewModel 느낌이다
즉, 내부적으로 상태를 가지는데 이는 한 뷰만을 위한 상태가 아닌 여러 뷰들에서 공통으로 사용될 만한 상태라는 것이다
이와 달리 Service는 상태를 가지지않는다
생각해 보면 MVC 구조의 SpringBoot 프로젝트에서, Controller에서 사용되는 Serivce는 Repository를 주입받아 데이터 변환 및 비지니스 로직을 처리한 결과를 반환할 뿐 따로 값을 가지고 있지는 않았다
앱의 MVVM 구조에선 ViewModel에서 비지니스 로직을 수행하는 데 사용하기에 마찬가지로 주입이 필요한데,
상태 관리(변화감지)의 필요가 없다보니 이와같은 Service들을 DIContainer에 묶어 Environment로 등록해서 처리하면 간편하다
@Observable
class DIContainer {
var todoService: TodoServiceType
var todoRealtimeService: TodoRealtimeServiceType
init(todoService: TodoServiceType = TodoService(),
todoRealtimeService: TodoRealtimeServiceType = TodoRealtimeService()) {
self.todoService = todoService
self.todoRealtimeService = todoRealtimeService
}
}
@main
struct TodoMateApp: App {
@State private var container: DIContainer = .init()
var body: some Scene {
WindowGroup {
ContentView()
.environment(container)
}
}
}
위의 예시처럼, 서비스 인터페이스를 따로 정의해두면 확장성 및 유지보수 그리고 테스트 시 아주 유용하다
구현
Subject(TodoRealtimeService)의 observers는 weak으로 선언해야 한다(순환참조방지)
실제로는 아래 예시에서 TodoObserver를 준수하는 Observer 클래스를 weak으로 래핑하는 과정을 추가해서 코드를 작성해야 한다
protocol TodoObserver: AnyObject {
func todoAdded(_ todo: Todo)
func todoModified(_ todo: Todo)
func todoRemoved(_ todo: Todo)
}
class TodoRealtimeService: TodoRealtimeServiceType {
private let todoRepository: TodoRepositoryType
private var task: Task<Void, Never>?
private var observers: [String: [TodoObserver]] = [:]
init(todoRepository: TodoRepositoryType = FirestoreTodoRepository(reference: .shared)) {
self.todoRepository = todoRepository
setupRealtimeUpdates()
}
deinit {
task?.cancel()
}
}
/// TodoRealtimeService(Subject)에 Observer가 등록되었다면,
/// 관련 Observer에 데이터 업데이트
extension TodoRealtimeService {
private func setupRealtimeUpdates() {
task = Task {
for await change in todoRepository.observeTodoChanges() {
if !observers.isEmpty {
handleDatabaseChange(change)
}
}
}
}
private func handleDatabaseChange(_ change: DatabaseChange<TodoDTO>) {
let todo = change.todoDTO.toModel()
guard let observers = observers[todo.uid], !observers.isEmpty else {
return
}
/// Observer는 TodoObserver를 준수하기에 해당 메서드들이 존재
switch change {
case .added:
for observer in observers {
observer.todoAdded(todo)
}
case .modified:
for observer in observers {
observer.todoModified(todo)
}
case .removed:
for observer in observers {
observer.todoRemoved(todo)
}
}
}
}
/// Observer 등록과 제거에 사용하는 메서드
extension TodoRealtimeService {
func addObserver(_ observer: TodoObserver, for userId: String) {
observers[userId, default: []].append(observer)
}
func removeObserver(_ observer: TodoObserver, for userId: String) {
observers[userId, default: []].removeAll(where: { ObjectIdentifier($0) == ObjectIdentifier(observer) })
}
}
@Observable
class TodoListViewModel {
private let service: TodoRealtimeServiceType
private let userId: String
var todos: [Todo] = []
init(
service: TodoRealtimeServiceType,
userId: String
) {
self.service = service
self.userId = userId
}
/// 옵저버 등록 - View에 연동
func onAppear() {
service.addObserver(self, for: userId)
}
/// 옵저버 제거 - View에 연동
func onDisappear() {
service.removeObserver(self, for: userId)
}
}
/// TodoObserver 준수 및 구현
extension TodoListViewModel: TodoObserver {
func todoAdded(_ todo: Todo) {
/// 알맞은 동작 구현
}
func todoModified(_ todo: Todo) {
/// 알맞은 동작 구현
}
func todoRemoved(_ todo: Todo) {
/// 알맞은 동작 구현
}
}
마무리
- Manager 명명
- ViewModel - 특정 View를 위한 상태와 로직 관리
- Manager - 더 넓은 범위의 상태와 기능을 관리
- Service의 역할
- Service는 상태 관리가 필요 없는 순수 기능 컴포넌트(데이터 변환 및 특정 비즈니스 로직 수행)
- DIContainer를 통해 Service들을 효율적으로 관리할 수 있음
- Observer Pattern 적용
- Observer Interface 정의
- Subject 구현
- Observer 배열 속성
- Observer 등록/해제 메서드
- Observer 구현
- Observer Interface 구현
- Observer 등록/해제 메서드 이용
기존에 클린 아키텍쳐에 대해 정리하면서 계층의 요소 명칭에 대해 대략적으로만 이해했었는데
리팩토링 과정을 통해 각 계층의 역할과 책임에 대한 깊이 있는 이해를 얻게 되었다
또한, 결국 실시간 데이터를 감지하는 서비스에 옵저버를 등록하여, 감지되는 Todo와 관련된 옵저버들에게 데이터를 전달하고
ViewModel에서는 적절한 동작을 하는 TodoObserver를 구현하여 원하는 동작을 깔끔하게 구현한 것 같다
'CS' 카테고리의 다른 글
공유기는 라우터인가? 스위치인가? (0) | 2024.12.01 |
---|---|
비동기 프로그래밍 (2) | 2024.06.08 |