[얼죽아] Tuist Project 구조 설계하기

Tuist Project 구조 설계하기

Tuist 버전 관리

Tuist Version: 4.27.0 버전 관리 도구: mise

mise를 통해 Tuist 버전을 통일하여 관리하고 있습니다.

프로젝트 구조

프로젝트의 구조는 Clean Architecture를 참고하여 설계하였고, 다음과 같이 구성되어 있습니다.

1. Core Layer

HotCoffeeHaterCore: 프로젝트의 핵심 기능을 담당하는 프레임워크 HotCoffeeHaterTestCore: 테스트 관련 공통 기능을 제공하는 프레임워크

2. Data Layer

StoresFeatureEntity: 데이터 모델 정의 StoresFeatureRepositoryImplementation: Repository 인터페이스 구현체

3. Domain Layer

StoresFeatureRepository: Repository 인터페이스 정의

4. Presentation Layer

StoresFeatureUIKit: UIKit 기반의 매장 관련 UI 구현 StoresFeatureUIKitTests: 매장 기능 관련 테스트

5. Application Layer

HotCoffeeHaterUIKit: UIKit 기반의 메인 애플리케이션

프로젝트 구조의 특징

이러한 구조를 통해 코드의 유지보수성과 테스트 용이성을 확보하고, 향후 새로운 기능 추가나 변경에 유연하게 대응할 수 있습니다.

Dependency

한계

1. 과도한 모듈 수

기능 1개 추가 시 4 ~ 5개의 모듈이 필요.

해결 방법

Feature 기반 모듈화를 통해서 하나의 feature와 관련된 코드를 하나의 모듈로 관리하는게 좋아보임.

2. Composition Root의 복잡성

Feature가 늘어날 수록 Factory method가 계속해서 늘어나 클래스가 비대해진다.

해결 방법
  1. Factory 객체들을 계층화한다.
protocol FeatureFactory {
    func create() -> Feature
}

class StoresFeatureFactory: FeatureFactory {
    private let dependencies: DependencyContainer

    init(dependencies: DependencyContainer) {
        self.dependencies = dependencies
    }

    func create() -> Feature {
        let repository = StoresRepositoryImplementation(
            networking: dependencies.networking
        )
        return StoresFeatureUIKit(repository: repository)
    }
}

class CompositionRoot {
    private let dependencies = DependencyContainer()
    private var factories: [FeatureFactory] = []

    func setupFactories() {
        factories = [
            StoresFeatureFactory(dependencies: dependencies),
            PaymentFeatureFactory(dependencies: dependencies)
            // ...
        ]
    }
}
  1. 모듈별 Coordinator 패턴 활용
protocol Coordinator: AnyObject {
    func start()
}

final class StoresCoordinator: Coordinator {
    private let navigationController: UINavigationController
    private let factory: StoresFeatureFactory

    init(navigationController: UINavigationController, factory: StoresFeatureFactory) {
        self.navigationController = navigationController
        self.factory = factory
    }

    func start() {
        showStoresList()
    }

    private func showStoresList() {
        let viewController = factory.makeStoresViewController { [weak self] action in
            switch action {
            case .showDetail(let storeId):
                self?.showStoreDetail(storeId: storeId)
            }
        }
        navigationController.pushViewController(viewController, animated: true)
    }

    private func showStoreDetail(storeId: String) {
        let viewController = factory.makeStoreDetailViewController(storeId: storeId)
        navigationController.pushViewController(viewController, animated: true)
    }
}

protocol StoresFeatureFactory {
    func makeStoresViewController(
        actionHandler: @escaping (StoresListAction) -> Void
    ) -> UIViewController
    func makeStoreDetailViewController(storeId: String) -> UIViewController
}

enum StoresListAction {
    case showDetail(storeId: String)
}

느낀점

모듈화된 구조로 프로젝트 설계를 고민하면서 디자인 패턴의 중요성을 깊이 체감했습니다. 여러 기능이 추가됐을 때를 생각하면 수평적인 확장이 아닌 수직적인 확장이 눈에 훤했고, 이를 해결하려면 여러 디자인 패턴을 활용해야 한다는 것을 깨달았습니다.

모듈화된 아키텍처를 설계할 때는 단순히 코드를 여러 모듈로 나누는 것을 넘어서, 각 모듈이 어떻게 상호작용할지, 그리고 그 상호작용을 어떻게 효과적으로 관리할지에 대한 깊은 고민이 필요합니다. 이 과정에서 적절한 디자인 패턴의 활용은 필수적이며, 이는 결국 더 유지보수하기 좋은 코드를 만드는 길이 된다는 것을 배웠습니다.