-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
JEONG edited this page Jun 29, 2026
·
2 revisions
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가 런타임에 구현체 결정
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
| 원칙 | 적용 |
|---|---|
| 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
}
}// 등록
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(): 로그아웃 시 전체 초기화
- AppRouter: 모듈 간 전환, Deep Link 처리 (조율자)
- Feature Router: 각 Feature 내부 화면 전환
- Tab별 독립
NavigationStack으로 상태 보존
@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)
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