🧩 먼저 요약 답부터
🔹 HappinessService → 프로토콜을 채택해야 하는 쪽
🔹 HappinessViewModel → 프로토콜을 의존해야 하는 쪽
즉,
ViewModel이 프로토콜을 “채택”하는 게 아니라,
프로토콜 타입을 “의존성(Dependency)” 으로 받아야 합니다.
🧠 왜냐면?
이걸 한 문장으로 표현하면 이렇게 돼요 👇
“ViewModel은 네트워크의 세부 구현을 몰라야 하기 때문이에요.”
🔍 구체적으로 살펴보자
✅ 1️⃣ 지금 상황: HappinessService 구조
final class HappinessService {
func fetchRandomQuote() -> AnyPublisher<HappinessQuote, Error> {
// 네트워크 요청
}
}
현재 이 클래스는 API 통신이라는 ‘구현’을 담당합니다.
즉, ViewModel 입장에서는 “데이터를 어떻게 가져오는지”는 몰라도 되는 영역이에요.
그냥 “명언을 가져와줘” 라고만 요청하면 됩니다.
✅ 2️⃣ 그래서 프로토콜을 “서비스 역할”로 추상화
protocol HappinessServiceProviding {
func fetchRandomQuote() -> AnyPublisher<HappinessQuote, Error>
}
→ 이 프로토콜은 “명언을 가져올 수 있는 객체라면 누구든 사용 가능하다”는 계약(Contract)을 정의한 거예요.
그 다음 실제 구현체(HappinessService)가 이걸 채택합니다.
final class HappinessService: HappinessServiceProviding {
func fetchRandomQuote() -> AnyPublisher<HappinessQuote, Error> { ... }
}
✅ 3️⃣ ViewModel은 프로토콜을 ‘의존’하는 구조로 설계
final class HappinessViewModel: ObservableObject {
private let service: HappinessServiceProviding
init(service: HappinessServiceProviding = HappinessService()) {
self.service = service
}
}
이렇게 하면
- ViewModel은 네트워크의 세부 구현을 몰라도 되고,
- 나중에 테스트 시엔 MockHappinessService를 끼워넣을 수 있습니다.
즉, “확장 가능하고 테스트 가능한 구조” 완성 🎯
🧠 반대로 “ViewModel에 프로토콜을 채택”하면?
예를 들어 이런 구조를 상상해볼 수 있죠 👇
protocol HappinessProviding {
func fetchRandomQuote() -> AnyPublisher<HappinessQuote, Error>
}
final class HappinessViewModel: ObservableObject, HappinessProviding {
...
}
이건 ViewModel이 스스로 데이터를 제공하는 역할을 떠안게 됩니다.
즉, “데이터 요청(서비스)”과 “UI 상태 관리(ViewModel)”가 뒤섞이게 돼요 ❌
→ SRP(단일 책임 원칙, Single Responsibility Principle)를 위반하게 됩니다.
ViewModel은 데이터를 직접 가져오는 주체가 아니라, 가져온 데이터를 보여주는 주체여야 해요.
그렇기 때문에, ViewModel이 직접 프로토콜을 채택하면 “역할이 잘못된 추상화”가 되어버립니다.
💡 그럼 “DiaryStore”는 왜 프로토콜을 채택했나?
DiaryStore가 프로토콜(DiaryProviding)을 채택한 이유는
이 구조가 SSOT(Single Source of Truth) 로 작동하기 때문이에요.
정리하면 👇
| 클래스 | 역할 | 프로토콜을 채택한 이유 |
| HappinessService | 외부 API에서 데이터를 “가져오는 역할” | 네트워크 호출을 Mocking / 교체하기 위함 |
| DiaryStore | Core Data 기반으로 “데이터를 보관하고 동기화하는 역할” (앱 내 SSOT) | 데이터 소스 전체를 인터페이스화해서 MockStore, PreviewStore로 교체 가능하게 하기 위함 |
즉,
1. DiaryStore는 앱 내부의 데이터 허브
2. HappinessService는 외부 API 데이터 공급자
둘 다 “데이터를 제공하는 역할”이기 때문에,
“프로토콜 채택 = 데이터 공급자 인터페이스 구현” 이라는 점에서는 같지만,
적용 맥락이 다릅니다.
✅ 결론 요약
| 항목 | 올바른 방향 |
| 프로토콜을 채택해야 하는 곳 | 데이터 제공자(Service, Store) |
| 프로토콜을 의존해야 하는 곳 | ViewModel |
| ViewModel이 직접 채택하면? | SRP(단일 책임 원칙) 위반 ❌ |
| DiaryStore의 경우 | 내부 데이터 공급자(Single Source of Truth) 역할이라 채택이 합리적 ✅ |
| HappinessService의 경우 | 외부 API 공급자 역할이라 프로토콜로 추상화 ✅ |
💬 한 줄 요약
💡 “프로토콜은 ‘역할’을 정의하는 것이지 ‘누가 쓸지’를 정의하는 게 아니다.
데이터 공급자(Service, Store)가 프로토콜을 채택하고,
ViewModel은 그 프로토콜에 의존성 주입을 받는 구조가 가장 깔끔하다.”
'감정일기(가칭)' 카테고리의 다른 글
| 🏠 HomeViewModel에 HappinessViewModel을 통합한 이유와 구조 정리 (0) | 2025.10.28 |
|---|---|
| 📘 Combine 기반 ViewModel 테스트: 시행착오로 배우는 HappinessViewModelTests 완성기 (0) | 2025.10.27 |
| 📘 단위 테스트 vs 통합 테스트– HappinessViewModel 사례로 배우는 Combine 테스트 구조 (0) | 2025.10.27 |
| ✅ ViewModel을 만들기 전에, Service 계층이 제대로 동작하는지 단위 테스트로 검증하자 (0) | 2025.10.27 |
| 🍋 iOS에서 외부 API 호출하기 – HappinessService 완전 해부 (0) | 2025.10.25 |