Domain 레이어가 가장 안쪽에 위치하며 외부를 전혀 import하지 않습니다. UseCase에 비즈니스 규칙이 집중되고, Repository Protocol로 Data 레이어와 완전히 분리됩니다.
Repository — 데이터 접근을 Protocol로 추상화.
Adapter — DTO ↔ Domain Entity 변환.
Facade — UseCase = 복잡한 비즈니스를 단순 인터페이스로.
import Foundation
// ⬆ Foundation만 import. UIKit도, Data 레이어도 모름. 순수 비즈니스 로직.
// MARK: - Use Cases (완성)
// ---- Filter UseCase ----
protocol FilterTasksUseCaseProtocol {
func execute(tasks: [TaskItem], searchText: String,
priority: TaskItem.Priority?) -> [TaskItem]
}
struct FilterTasksUseCase: FilterTasksUseCaseProtocol {
func execute(tasks: [TaskItem], searchText: String,
priority: TaskItem.Priority?) -> [TaskItem] {
tasks.filter { task in
let matchSearch = searchText.isEmpty ||
task.title.localizedCaseInsensitiveContains(searchText)
let matchPriority = priority == nil || task.priority == priority
return matchSearch && matchPriority
}
}
}
// ---- Domain Error ----
enum TaskError: LocalizedError {
case emptyTitle, duplicateTitle
var errorDescription: String? {
switch self {
case .emptyTitle: "Title cannot be empty"
case .duplicateTitle: "A task with this title already exists"
}
}
}
// ---- Add UseCase ----
protocol AddTaskUseCaseProtocol {
func execute(title: String, priority: TaskItem.Priority) async throws
}
struct AddTaskUseCase: AddTaskUseCaseProtocol {
let repository: TaskRepositoryProtocol // ← Protocol만 알고 있음 (DIP)
func execute(title: String, priority: TaskItem.Priority) async throws {
let trimmed = title.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { throw TaskError.emptyTitle }
// 비즈니스 규칙: 중복 제목 불허
let existing = await repository.fetchAll()
guard !existing.contains(where: { $0.title.lowercased() == trimmed.lowercased() })
else { throw TaskError.duplicateTitle }
await repository.save(TaskItem(title: trimmed, priority: priority))
}
}
// ---- Toggle UseCase ----
protocol ToggleTaskUseCaseProtocol {
func execute(task: TaskItem) async
}
struct ToggleTaskUseCase: ToggleTaskUseCaseProtocol {
let repository: TaskRepositoryProtocol
func execute(task: TaskItem) async {
var updated = task
updated.isCompleted.toggle()
await repository.update(updated)
}
}import Foundation
// MARK: - Repository 구현체 (Data 레이어)
// Domain의 TaskRepositoryProtocol을 구현합니다.
// Domain은 이 구현체를 전혀 모릅니다 — 의존성 역전(DIP)!
final class LocalTaskRepository: TaskRepositoryProtocol {
private let storage = TaskStorage.shared
func fetchAll() async -> [TaskItem] {
storage.loadAll()
}
func save(_ task: TaskItem) async {
storage.add(task)
}
func update(_ task: TaskItem) async {
storage.update(task)
}
func delete(id: UUID) async {
storage.delete(id: id)
}
}
// ---- Domain Protocol (참고용) ----
// 05-CleanArchitecture/Finish/Domain/TaskRepositoryProtocol.swift
// protocol TaskRepositoryProtocol {
// func fetchAll() async -> [TaskItem]
// func save(_ task: TaskItem) async
// func update(_ task: TaskItem) async
// func delete(id: UUID) async
// }
//
// Domain이 이 Protocol을 정의하고,
// Data 레이어가 구현합니다.
// Presentation은 Protocol을 통해 Data에 접근합니다.
// → 고수준(Domain)이 저수준(Data) 구현에 의존하지 않습니다.AddTaskUseCase.execute()는 중복 체크 → 저장이라는 복잡한 과정을 단순한 인터페이스 뒤로 숨깁니다. Presentation이 비즈니스 규칙을 알 필요가 없습니다.import Foundation만 있습니다. UIKit, SwiftUI, CoreData가 없어 macOS 테스트나 다른 플랫폼 이식이 쉽습니다.fetchAll()만 호출합니다. 저장소 교체가 Protocol 구현체 교체로 끝납니다.