이전 글에서 우리는
DiaryStore가 SSOT(Single Source of Truth) 로서
앱 전역의 감정일기 데이터를 관리하는 구조를 설계했어요 🍋
그런데 여기서 중요한 원칙이 하나 있습니다.
👉 “모든 ViewModel이 DiaryStore를 직접 아는 것은 위험하다”
왜냐하면 Store는 내부적으로
- Core Data 접근 로직
- Combine Subject 관리
- 쓰기 함수(save/update/delete)
등을 모두 포함하고 있어서, UI에서 접근이 무분별해질 위험이 있기 때문이에요.
그래서 등장한 것이 바로,
🧩 DiaryProviding 프로토콜 입니다.
🎯 DiaryProviding이란?
“ViewModel이 감정일기 데이터를 읽기 전용으로 접근하기 위한 인터페이스”
@MainActor
protocol DiaryProviding: AnyObject {
// ✅ 실시간 변경을 반영하는 Publisher
var diariesPublisher: AnyPublisher<[EmotionDiaryModel], Never> { get }
// ✅ 즉시 접근 가능한 스냅샷
var snapshot: [EmotionDiaryModel] { get }
// ✅ 특정 일기 조회
func diary(with id: String) -> EmotionDiaryModel?
// ✅ 특정 주(week)의 일기들
func diaries(inWeekOf date: Date) -> [EmotionDiaryModel]
// ✅ 주간 감정별 통계
func countByEmotion(inWeekOf date: Date) -> [EmotionCategory: Int]
}
🧠 설계 의도
이 프로토콜은 다음 두 가지 철학을 반영합니다.
1️⃣ “읽기 전용” 접근만 허용한다
ViewModel이 Store의 내부를 몰라도 데이터를 사용할 수 있게 합니다.
즉,
ViewModel은 DiaryStore가 어떻게 데이터를 저장하는지,
어디서 불러오는지( Core Data, Firebase 등 ) 전혀 몰라도 돼요.
그냥 아래처럼만 쓰면 됩니다 👇
final class DiaryListViewModel: ObservableObject {
@Published var diaries: [EmotionDiaryModel] = []
private var cancellables = Set<AnyCancellable>()
// ✅ DiaryProviding 타입만 알고 있음
private let diaryProvider: DiaryProviding
init(provider: DiaryProviding = DiaryStore.shared) {
self.diaryProvider = provider
// ✅ 실시간 데이터 구독
provider.diariesPublisher
.receive(on: DispatchQueue.main)
.assign(to: &$diaries)
}
}
💬 이렇게 하면 ViewModel은 오직 DiaryProviding 프로토콜만 알기 때문에,
Store 내부가 어떻게 생겼는지는 완전히 감춰집니다.
이걸 인터페이스 캡슐화(Interface Encapsulation) 라고 해요.
2️⃣ “느슨한 결합(Decoupling)”으로 유연한 구조를 만든다
이 프로토콜이 중요한 이유는 단 하나의 클래스(DiaryStore)에만 의존하지 않기 때문이에요.
예를 들어 나중에 Core Data 대신 Firebase를 쓸 수도 있죠.
final class FirebaseDiaryStore: DiaryProviding { ... }
이렇게 새 저장소를 구현해도 ViewModel은 변경 없이 그대로 동작합니다.
그저 주입할 객체를 바꿔주면 끝 👇
// Core Data 사용 시
let vm = DiaryListViewModel(provider: DiaryStore.shared)
// Firebase 사용 시
let vm = DiaryListViewModel(provider: FirebaseDiaryStore())
💡 결론:
“데이터의 공급자가 바뀌어도, ViewModel은 영향을 받지 않는다.”
→ 이것이 바로 “느슨한 결합(Loose Coupling)”의 핵심이에요.
⚙️ 각 프로퍼티/메서드 설명
| 프로퍼티 / 메서드 | 설명 |
| 🧩 var diariesPublisher | Combine 기반 실시간 데이터 스트림. ViewModel이 구독하여 UI를 자동 갱신함. → “반응형 데이터”를 위한 핵심 통로 |
| 🪞 var snapshot | 현재 메모리 상의 최신 데이터 복제본. Combine 없이 단발성 접근이 필요할 때 사용 (위젯, 테스트 등) |
| 🔍 func diary(with id:) | 특정 ID의 감정일기를 즉시 조회 |
| 📆 func diaries(inWeekOf:) | 주간 단위로 감정일기 목록을 필터링 |
| 📊 func countByEmotion(inWeekOf:) | 주별 감정 통계 계산 (예: 행복 3개, 슬픔 2개 등) |
💬 snapshot vs Publisher의 차이
| 구분 | snapshot | diariesPublisher |
| 목적 | “지금 이 순간의 데이터만 보고 싶을 때” | “데이터 변경을 실시간으로 감지할 때” |
| 방식 | 즉시 접근 (동기적) | 구독 기반 (비동기적) |
| 사용 예 | 테스트, 위젯, 알림 | 일반적인 UI 화면(ViewModel) |
| 예시 코드 | DiaryStore.shared.snapshot | store.diariesPublisher.sink { ... } |
🧩 프로토콜 + @MainActor 조합
@MainActor
protocol DiaryProviding: AnyObject { ... }
이 선언이 중요한 이유는,
- Store의 상태(diariesSubject)는 MainActor(메인 스레드) 에서만 안전하게 접근해야 하고
- ViewModel도 대부분 메인 스레드에서 동작하기 때문이에요.
따라서 이 프로토콜을 @MainActor로 선언하면,
→ Store와 ViewModel 간의 스레드 안전성(Thread Safety) 을 자동으로 보장할 수 있습니다 ✅
✅ 정리 — DiaryProviding의 역할
| 핵심 포인트 | 설명 |
| 🔒 캡슐화(Encapsulation) | Store 내부(Core Data, Combine Subject 등)를 완전히 숨김 |
| 🔁 느슨한 결합(Decoupling) | Core Data → Firebase 교체에도 ViewModel은 그대로 |
| 🧩 인터페이스 중심 설계 | “무엇을 할 수 있는가”만 노출, “어떻게 하는가”는 감춤 |
| 🧠 @MainActor 보장 | UI/Store 간의 스레드 안정성 확보 |
| 📡 읽기 전용 인터페이스 | ViewModel은 데이터를 수정할 수 없고 오직 구독/조회만 가능 |