레이어 구조
Presentation (UI, ViewModel) | v depends on Domain <— 의존성 역전 (UseCase, (Dependency Repository Inversion) Protocol) ^ implements | Data (LocalRepository, API, DB)
핵심 학습 포인트
  • Domain은 Foundation만 import — 순수 로직
  • UseCase 안에 비즈니스 규칙 집중
  • Repository Protocol(Domain) vs 구현체(Data) 분리
  • DIP: 고수준이 저수준 구현에 의존하지 않음
  • 레이어별 단독 테스트 가능
내장 디자인 패턴
Repository Adapter Facade

Repository — 데이터 접근을 Protocol로 추상화.

Adapter — DTO ↔ Domain Entity 변환.

Facade — UseCase = 복잡한 비즈니스를 단순 인터페이스로.

 05-CleanArchitecture/Finish/Domain/UseCases.swift
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)
    }
}
 05-CleanArchitecture/Finish/Data/LocalTaskRepository.swift
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) 구현에 의존하지 않습니다.
핵심 개념
의존성 역전 원칙 (DIP)
고수준 모듈(UseCase)이 저수준 모듈(LocalRepository) 구현에 직접 의존하지 않습니다. 둘 다 추상화(Protocol)에 의존합니다. 실제 DB를 교체해도 Domain은 변경 없습니다.
UseCase = Facade
AddTaskUseCase.execute()는 중복 체크 → 저장이라는 복잡한 과정을 단순한 인터페이스 뒤로 숨깁니다. Presentation이 비즈니스 규칙을 알 필요가 없습니다.
순수 Domain 레이어
Domain 파일을 열면 import Foundation만 있습니다. UIKit, SwiftUI, CoreData가 없어 macOS 테스트나 다른 플랫폼 이식이 쉽습니다.
Repository 패턴
데이터가 UserDefaults에 있든, CoreData에 있든, REST API에 있든, UseCase는 fetchAll()만 호출합니다. 저장소 교체가 Protocol 구현체 교체로 끝납니다.