본문 바로가기
감정일기(가칭)

🧭 DiaryProviding 프로토콜 — 읽기 전용 인터페이스로 ViewModel을 보호하기

by 밤새는 탐험가89 2025. 10. 23.
728x90
SMALL

이전 글에서 우리는
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은 데이터를 수정할 수 없고 오직 구독/조회만 가능
728x90
LIST