Skip to content

SongTaehwan/somniary

Repository files navigation

Somniary

SwiftUI 기반 iOS 애플리케이션

🏗️ 아키텍처 설계 구조

핵심 원칙

  • Clean Architecture + Hexagonal (Ports & Adapters) Architecture
  • Anti-corruption Layer (ACL): 컨텍스트 간 경계 보호
  • Repository & Data Source 분리: 명확한 Data Layer 책임 분리

📐 계층 구조

┌────────────────────────────────────────────────────────────┐
│                    Presentation Layer                      │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │ Coordinator  │  │  ViewModel   │  │    Views     │      │
│  │   (Flow)     │──│   (State)    │──│     (UI)     │      │
│  └──────────────┘  └──────────────┘  └──────────────┘      │
└────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌────────────────────────────────────────────────────────────┐
│                      Domain Layer                          │
│  ┌──────────────┐  ┌──────────────┐  ┌────────────────┐    │
│  │   Entities   │  │   UseCases   │  │    Errors      │    │
│  │  (Models)    │  │  (Logic)     │  │ (SomniaryError)│    │
│  └──────────────┘  └──────────────┘  └────────────────┘    │
└────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌────────────────────────────────────────────────────────────┐
│                       Data Layer                           │
│  ┌──────────────────────┐       ┌──────────────────────┐   │
│  │    Repository        │       │   Domain Mapping     │   │
│  │  (AuthReposable)     │◄──────│   Error Mapping      │   │
│  └──────────────────────┘       └──────────────────────┘   │
└────────────────────────────────────────────────────────────┘
                            │
                            ▼
┌────────────────────────────────────────────────────────────┐
│                    Infrastructure Layer                    │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │   Network    │  │   Storage    │  │  External    │      │
│  │ (HTTP Client)│  │  (Keychain)  │  │ (Apple Auth) │      │
│  └──────────────┘  └──────────────┘  └──────────────┘      │
└────────────────────────────────────────────────────────────┘

📂 디렉토리 구조 & 책임

Somniary/
│
├── 🎯 Modules/                    # 기능 모듈 (Feature Modules)
│   ├── Login/
│   │   ├── Coordinator/          # 화면 전환 로직
│   │   ├── ViewModel/            # 상태 관리 & 비즈니스 로직 연결
│   │   ├── Views/                # UI 컴포넌트
│   │   ├── Reducer/              # 상태 변경 순수 함수
│   │   ├── Effect/               # Side Effect 실행
│   │   ├── Domain/               # 모듈별 도메인 엔티티 & 에러
│   │   └── DataSources/          # Repository 구현체
│   │
│   ├── Settings/
│   └── TabBar/
│
├── 🧩 Domain/                     # 공통 도메인 레이어
│   ├── Entities/                 # 도메인 엔티티
│   ├── UseCases/                 # 비즈니스 유스케이스
│   └── Errors/                   # 도메인 에러 정의
│       ├── SomniaryError.swift   # 범용 에러 구조
│       ├── DomainError.swift     # 에러 프로토콜
│       └── ErrorContext.swift    # 에러 컨텍스트 정보
│
├── 🌐 Network/                    # Infrastructure - 네트워크
│   ├── Interfaces/               # 프로토콜 (Ports)
│   │   ├── SomniaryNetworking.swift
│   │   └── Endpoint.swift
│   ├── Endpoints/                # API 엔드포인트 정의
│   ├── Entities/                 # 네트워크 응답 DTO
│   ├── SomniaryHTTPClient.swift  # HTTP 클라이언트 구현
│   └── TransportError.swift      # 네트워크 에러
│
├── 🧰 Common/                     # 공통 유틸리티
│   ├── TokenRepository/          # 토큰 관리
│   ├── KeychainStorage.swift     # Keychain 저장소
│   ├── LocalStorage.swift        # UserDefaults 저장소
│   └── ViewModelType.swift       # ViewModel 프로토콜
│
├── 🧭 Coordinator/                # 화면 전환 패턴
│   ├── Coordinator.swift         # 기본 프로토콜
│   ├── FlowCoordinator.swift     # Flow 관리
│   └── NavigationFlowView.swift  # SwiftUI 통합
│
├── 🎨 SharedUI/                   # 공통 UI 컴포넌트
│   ├── BaseButton.swift
│   ├── TextInput.swift
│   └── Modifiers/
│
└── 🔧 Helpers/                    # 확장 & 유틸리티
    ├── Array+Extensions.swift
    ├── Publisher+Extensions.swift
    └── Result+Extensions.swift

🔄 의존성 흐름

┌─────────────────────────────────────────────────────────────┐
│                    Dependency Rule                          │
│                                                             │
│   Presentation  ──►  Domain  ──►  Data  ──►  Infrastructure │
│                                                             │
│       역방향 의존 금지 (Dependency Inversion Principle)         │
└─────────────────────────────────────────────────────────────┘

구체적 흐름 예시 (로그인)

┌──────────────┐      ┌────────────────┐      ┌───────────────┐      ┌───────────────┐
│  LoginView   │─────►│ LoginViewModel │─────►│ AuthReposable │─────►│ SomniaryHTTP  │
│   (SwiftUI)  │      │     (State)    │      │   (Protocol)  │      │    Client     │
└──────────────┘      └────────────────┘      └───────────────┘      └───────────────┘
                              │
                              ▼
                      ┌─────────────────┐
                      │ LoginEffectPlan │ ◄─── Side Effect 분리
                      │  (Async Tasks)  │
                      └─────────────────┘

🎭 핵심 패턴

1. MVI (Model-View-Intent) + Effect Pattern

프레젠테이션 레이어는 MVI 아키텍처를 기반으로 구현되어 있습니다.

┌───────────────────────────────────────────────────────────────────┐
│                          MVI Cycle                                │
│                                                                   │
│   ┌───────────┐   Intent    ┌────────────┐                        │
│   │   View    │────────────►│  Reducer   │                        │
│   │ (SwiftUI) │             │   (Pure)   │                        │
│   └───────────┘             └────────────┘                        │
│        ▲                      │         │                         │
│        │                      │         │                         │
│      render          New State│         │ Effect Plan 생성         │
│        │                      │         │                         │
│        │                      ▼         ▼                         │
│   ┌──────────┐         ┌───────────────────┐                      │
│   │  State   │◄────────│   ViewModel       │                      │
│   └──────────┘   할당   │ (Effect 분류/처리)  │                      │
│                        └───────────────────┘                      │
│                              │         │                          │
│                     Navigation/        │                          │
│                     UI Effect     API Effect                      │
│                              │         │                          │
│                              ▼         ▼                          │
│                        Coordinator  ┌──────────────┐              │
│                        / UIEvent    │   Executor   │              │
│                                     │ (API, 로깅 등) │              │
│                                     └──────────────┘              │
│                                            │                      │
│                                            │                      │
│                                     결과를 Intent로 환류             │
│                                            │                      │
│                                            └──────►Intent (재순환)  │
│                                                                   │
└───────────────────────────────────────────────────────────────────┘

MVI 구성 요소

요소 역할 구현 예시
Model 불변 상태 LoginState
View UI 렌더링 & Intent 발행 LoginView
Intent 사용자/시스템 이벤트 LoginIntent (User, System, Navigation)
Reducer 순수 함수: (State, Intent) → (State, [Effect]) combinedReducer
Effect Side Effect 데이터 LoginEffectPlan
Executor Effect 실행기 LoginExecutor

Intent 유형

Intent는 시스템에서 발생하는 모든 이벤트를 표현하며, 출처에 따라 분류됩니다.

유형 역할 예시
User Intent 사용자 인터랙션 .emailChanged(), .submitLogin(), .signUpTapped()
Lifecycle Intent View 생명주기 .appeared, .disappeared
System External 외부 시스템 이벤트 .appleLoginRequest, .networkReachable(), .deepLink()
System Internal Effect 실행 결과 .loginResponse(), .verifyResponse()
Navigation Intent 화면 전환 요청 .routeToHome, .routeToSignUp
// LoginIntent.swift - Intent 계층 구조
enum LoginIntent {
    case user(UserIntent)                    // 사용자 액션
    case lifecycle(LifecycleIntent)          // View 생명주기
    case systemExtenral(SystemExtenralIntent) // 외부 시스템
    case systemInternal(SystemInternalIntent) // Effect 결과
    case navigation(NavigationIntent)         // 네비게이션
}

// 각 Intent는 명확한 출처를 가짐
enum UserIntent {
    case emailChanged(String)
    case submitLogin
    // ...
}

enum SystemInternalIntent {
    case loginResponse(Result<VoidResponse, AuthError>)
    case verifyResponse(Result<TokenEntity, AuthError>)
    // ...
}

실제 흐름 예시

// 1. User가 로그인 버튼 클릭
View: Button("로그인") { viewModel.send(.user(.submitLogin)) }

// 2. Reducer가 순수 함수로 새로운 상태와 Effect 반환
Reducer: (oldState, .user(.submitLogin))
     (newState.isLoading = true,
       [.verify(email, otp, requestId)])

// 3. Executor가 Effect 실행 (API 호출)
Executor: .verify(...)
     Repository.verify(...)
     Intent 환류: .systemInternal(.verifyResponse(result))

// 4. 다시 Reducer로 순환
Reducer: (state, .systemInternal(.verifyResponse(.success(token))))
     (state, [.storeToken(token), .route(.navigateHome)])

특징

  • 단방향 데이터 흐름: View → Intent → Reducer → State → View
  • 순수 함수 Reducer: 테스트 용이, 예측 가능
  • Side Effect 격리: Effect로 분리하여 추적 가능
  • 시간 여행 디버깅: 모든 Intent와 State 변화 기록 가능

2. Coordinator Pattern

FlowCoordinator
  └── 화면 전환 로직을 ViewModel에서 분리
  └── 중앙화된 네비게이션 관리

3. Repository Pattern

Protocol (AuthReposable)           # Domain Layer
     implements
Repository (RemoteAuthRepository)  # Data Layer
     uses
Data Source (SomniaryHTTPClient)   # Infrastructure Layer

4. Error Mapping (Anti-corruption Layer)

ACL 패턴을 통해 인프라 계층의 에러를 도메인 에러로 변환하여 비즈니스 로직을 보호합니다.

┌─────────────────────────────────────────────────────────────┐
│                    Error Flow & Mapping                     │
└─────────────────────────────────────────────────────────────┘

Infrastructure Layer
├─ TransportError              # 네트워크 에러 (HTTP, 연결 등)
└─ Data Source Error           # 디코딩, 변환 에러
         │
         │ Repository가 ACL 역할 수행
         ▼
Domain Layer
└─ DomainError                 # 비즈니스 컨텍스트 에러
   (SomniaryError)             # - AuthError
                               # - UserError 등
         │
         │ ViewModel/View가 사용자 친화적으로 변환
         ▼
Presentation Layer
└─ User Message                # "이메일을 확인해주세요"

Repository의 ACL 역할

// RemoteAuthRepository.swift
func verify(email: String, otpCode: String) async throws -> TokenEntity {
    let result = await client.request(.verify(email: email, otpCode: otpCode))

    switch result {
    case .success(let response):
        do {
            // Data Source Error 가능 (디코딩 실패 등)
            let data = try JSONDecoder().decode(NetAuth.Verify.Response.self,
                                                  from: response.body)
            return TokenEntity(accessToken: data.accessToken,
                              refreshToken: data.refreshToken)
        } catch {
            // 디코딩 에러 → Domain Error 매핑
            throw mapToDomainError(error)
        }
    case .failure(let transportError):
        // Transport Error → Domain Error 매핑
        throw mapToDomainError(transportError)
    }
}

private func mapToDomainError(_ error: Error) -> DomainError {
    // Infrastructure 에러를 Domain 에러로 변환
    // 외부 시스템 변경이 도메인에 영향을 주지 않도록 보호
}

에러 유형별 처리

계층 에러 타입 책임 예시
Infrastructure TransportError HTTP/네트워크 에러 .networkFailure, .unauthorized(401)
Data Decoding/Parsing Error 데이터 변환 실패 DecodingError, DateParsingError
Domain DomainError 비즈니스 에러 AuthError.invalidCredentials
Presentation User Message 사용자 친화적 메시지 "로그인 정보가 올바르지 않습니다"

ACL의 장점

  • 외부 의존성 격리: 네트워크 라이브러리 변경이 도메인에 영향 없음
  • 명확한 에러 컨텍스트: 비즈니스 관점의 에러 표현
  • 테스트 용이성: 도메인 레이어는 Infrastructure 에러를 몰라도 됨

About

SwiftUI 기반 앱

Resources

Stars

Watchers

Forks

Releases

No releases published

Languages