Skip to content

Architecture

JEONG edited this page Jun 29, 2026 · 2 revisions

Architecture

Feature 기반 Clean Architecture + Observation. 계층 구조, ViewModel/View 패턴, Router를 다룹니다. 에러 처리 상세는 Error Handling 참고.

전체 그림

Feature-Based Modular + Clean Architecture + Observation

View ←→ ViewModel(@Observable) → UseCase(Protocol) → Repository → DataSource
                                    ↑
                   DIContainer가 Protocol 구현체 주입
  • Presentation → Domain → Data 단방향 의존
  • 상위 계층은 하위 계층의 Protocol에만 의존 (DIP)
  • Tab별 독립 NavigationStack으로 상태 보존

계층 원칙

계층 역할 의존 방향
Presentation View, ViewModel → Domain
Domain UseCase, Model, Interface(Protocol) ← Data가 구현
Data Repository, Router, DTO Domain Protocol 구현
  • Presentation → Domain: View/ViewModel은 UseCase Protocol에만 의존
  • Domain → Data: UseCase는 Repository Protocol 사용, 구현체를 모름
  • Protocol 기반 주입: DIContainer가 런타임에 구현체 결정

Feature 폴더 구조

Features/{Feature}/
├── Presentation/
│   ├── Views/           # SwiftUI View
│   ├── ViewModels/      # @Observable ViewModel
│   ├── Components/      # Feature 전용 컴포넌트
│   └── Router/          # Feature Router
├── Domain/
│   ├── UseCases/        # Protocol + Implementations/
│   ├── Models/          # Entity
│   └── Interfaces/      # Repository Protocol
└── Data/
    ├── Repositories/    # Repository 구현체
    └── DataSources/     # API, Local Storage

SOLID 적용

원칙 적용
SRP View(렌더링) / ViewModel(상태) / UseCase(로직) / Repository(데이터) 분리
OCP Protocol 기반 설계로 기존 코드 수정 없이 새 구현체 추가
LSP Protocol 구현체는 언제든 교체 가능 (Mock / Real / Stub)
ISP 큰 Protocol보다 작고 명확한 Protocol 여러 개로 분리
DIP 상위 모듈이 하위 모듈 구현체가 아닌 Protocol에 의존
// DIP 예시: UseCase는 Protocol에만 의존
protocol UserRepositoryProtocol {
    func fetchUser(id: String) async throws -> User
}

final class FetchUserUseCase {
    private let repository: UserRepositoryProtocol  // 구현체가 아닌 Protocol

    init(repository: UserRepositoryProtocol) {
        self.repository = repository
    }
}

DIContainer

// 등록
container.register(UserRepositoryProtocol.self) { UserRepository() }
container.register(LoginUseCaseProtocol.self) {
    LoginUseCase(repository: container.resolve(UserRepositoryProtocol.self))
}

// 사용
let useCase = container.resolve(LoginUseCaseProtocol.self)
  • @Observable 기반으로 SwiftUI Environment 주입 가능
  • resolve() 호출 시 캐싱 (싱글톤처럼 동작)
  • resetCache(): 로그아웃 시 전체 초기화

Router (Hierarchical Router Pattern)

  • AppRouter: 모듈 간 전환, Deep Link 처리 (조율자)
  • Feature Router: 각 Feature 내부 화면 전환
  • Tab별 독립 NavigationStack으로 상태 보존

Observation 패턴

ViewModel 규칙

@Observable
final class ChallengerAttendanceViewModel {
    private var container: DIContainer
    private var useCase: ChallengerAttendanceUseCaseProtocol

    // Loadable로 비동기 상태 관리
    private(set) var attendanceState: Loadable<Attendance> = .idle

    @MainActor
    func attendanceBtnTapped(userId: UserID) async {
        attendanceState = .loading
        do {
            let result = try await useCase.requestGPSAttendance(...)
            attendanceState = .loaded(result)
        } catch let error as DomainError {
            attendanceState = .failed(.domain(error))  // 인라인 에러
        } catch {
            errorHandler.handle(error, context: ...)   // Alert 에러
        }
    }
}
  • @Observable 매크로 사용 (NOT @StateObject, @ObservedObject, @Published)
  • 예외: 앱 생명주기 연결 전역 상태 관리자 (AppFlowViewModel)

View 규칙

struct ChallengerAttendanceView: View {
    @State private var viewModel: ChallengerAttendanceViewModel

    init(container: DIContainer, ...) {
        _viewModel = State(initialValue: ChallengerAttendanceViewModel(
            container: container,
            ...
        ))
    }

    var body: some View { ... }
}
  • @State private var viewModel 패턴으로 소유권 명시
  • Action 기반 단방향 데이터 흐름

관련 문서: Coding Conventions · Error Handling · Module Structure

Clone this wiki locally