Skip to content

멀티 모듈 프로젝트

SPM(Swift Package Manager)을 사용한 멀티 모듈 Swift 프로젝트에서 WeaveDI를 사용하는 방법을 학습합니다.

개요

멀티 모듈 아키텍처는 다음을 제공합니다:

  • 관심사의 더 나은 분리
  • 더 빠른 컴파일 (변경된 모듈만 다시 빌드)
  • 향상된 코드 재사용성
  • 명확한 의존성 경계

WeaveDI는 타입 안전성과 성능을 유지하면서 모듈 경계를 넘나들며 원활하게 작동하도록 설계되었습니다.

프로젝트 구조

일반적인 멀티 모듈 설정

MyApp/
├── Package.swift
├── App/                    # 메인 애플리케이션 타겟
│   └── Sources/
│       └── MyApp.swift
├── Features/
│   ├── UserFeature/       # 기능 모듈
│   │   └── Sources/
│   ├── OrderFeature/      # 기능 모듈
│   │   └── Sources/
│   └── PaymentFeature/    # 기능 모듈
│       └── Sources/
├── Core/
│   ├── Networking/        # 인프라 모듈
│   │   └── Sources/
│   ├── Database/          # 인프라 모듈
│   │   └── Sources/
│   └── SharedModels/      # 공유 타입
│       └── Sources/
└── DI/                    # 의존성 주입 모듈
    └── Sources/

Package.swift 구성

swift
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "MyApp",
    platforms: [
        .iOS(.v15),
        .macOS(.v14)
    ],
    products: [
        .executable(name: "MyApp", targets: ["App"])
    ],
    dependencies: [
        .package(url: "https://github.com/Roy-wonji/WeaveDI.git", from: "3.2.0")
    ],
    targets: [
        // 앱 타겟
        .executableTarget(
            name: "App",
            dependencies: [
                "UserFeature",
                "OrderFeature",
                "PaymentFeature",
                "DI"
            ]
        ),

        // 기능 모듈
        .target(
            name: "UserFeature",
            dependencies: [
                "Networking",
                "SharedModels",
                .product(name: "WeaveDI", package: "WeaveDI")
            ]
        ),
        .target(
            name: "OrderFeature",
            dependencies: [
                "Networking",
                "SharedModels",
                .product(name: "WeaveDI", package: "WeaveDI")
            ]
        ),
        .target(
            name: "PaymentFeature",
            dependencies: [
                "Networking",
                "SharedModels",
                .product(name: "WeaveDI", package: "WeaveDI")
            ]
        ),

        // 코어 모듈
        .target(
            name: "Networking",
            dependencies: [
                .product(name: "WeaveDI", package: "WeaveDI")
            ]
        ),
        .target(
            name: "Database",
            dependencies: [
                .product(name: "WeaveDI", package: "WeaveDI")
            ]
        ),
        .target(
            name: "SharedModels",
            dependencies: []
        ),

        // DI 모듈
        .target(
            name: "DI",
            dependencies: [
                "UserFeature",
                "OrderFeature",
                "PaymentFeature",
                "Networking",
                "Database",
                .product(name: "WeaveDI", package: "WeaveDI")
            ]
        ),

        // 테스트
        .testTarget(
            name: "UserFeatureTests",
            dependencies: ["UserFeature"]
        )
    ]
)

의존성 관리 패턴

패턴 1: 중앙 집중식 DI 모듈

최적: 소규모에서 중간 규모 프로젝트

모든 기능 모듈을 알고 있고 의존성을 구성하는 전용 DI 모듈을 생성합니다.

swift
// DI/Sources/DI.swift
import WeaveDI
import UserFeature
import OrderFeature
import PaymentFeature
import Networking
import Database

public final class AppDI {
    public static func bootstrap() async {
        await WeaveDI.Container.bootstrap { container in
            // 인프라스트럭처
            container.register(APIClient.self) {
                URLSessionAPIClient(baseURL: Configuration.apiBaseURL)
            }

            container.register(Database.self) {
                RealmDatabase()
            }

            // 기능 의존성
            UserFeatureModule.register(in: container)
            OrderFeatureModule.register(in: container)
            PaymentFeatureModule.register(in: container)
        }
    }
}
swift
// UserFeature/Sources/UserFeatureModule.swift
import WeaveDI

public struct UserFeatureModule {
    public static func register(in container: WeaveDI.Container) {
        container.register(UserService.self) {
            UserServiceImpl()
        }

        container.register(UserRepository.self) {
            UserRepositoryImpl()
        }
    }
}

패턴 2: 분산형 모듈 등록

최적: 독립적인 팀이 있는 대규모 프로젝트

각 기능 모듈이 자체 등록 방법을 노출합니다.

swift
// UserFeature/Sources/UserFeatureDI.swift
import WeaveDI

public protocol UserFeatureDependencies {
    var apiClient: APIClient { get }
    var database: Database { get }
}

public struct UserFeatureModule {
    public static func bootstrap(
        dependencies: UserFeatureDependencies
    ) async {
        // 기능별 의존성 등록
        await WeaveDI.Container.bootstrap { container in
            // 제공된 의존성 사용
            container.register(APIClient.self) {
                dependencies.apiClient
            }

            // 기능 서비스 등록
            container.register(UserService.self) {
                UserServiceImpl()
            }
        }
    }
}
swift
// App/Sources/AppDI.swift
import UserFeature
import OrderFeature

struct AppDependencies: UserFeatureDependencies, OrderFeatureDependencies {
    let apiClient: APIClient
    let database: Database

    init() {
        self.apiClient = URLSessionAPIClient(baseURL: Config.apiURL)
        self.database = RealmDatabase()
    }
}

@main
struct MyApp: App {
    init() {
        let deps = AppDependencies()

        Task {
            await UserFeatureModule.bootstrap(dependencies: deps)
            await OrderFeatureModule.bootstrap(dependencies: deps)
        }
    }
}

패턴 3: 프로토콜 기반 모듈 경계

최적: 최대 유연성과 테스트 가능성

모듈 경계에서 프로토콜 인터페이스를 정의합니다.

swift
// UserFeature/Sources/UserFeatureInterface.swift
public protocol UserServiceProtocol {
    func fetchUser(id: String) async throws -> User
}

public protocol UserFeatureInterface {
    var userService: UserServiceProtocol { get }
}
swift
// UserFeature/Sources/UserFeatureImplementation.swift
import WeaveDI

public struct UserFeatureImpl: UserFeatureInterface {
    @Injected(\.userService) public var userService

    public init() {}
}

// 구현 등록
extension InjectedValues {
    public var userFeature: UserFeatureInterface {
        get { self[UserFeatureKey.self] }
        set { self[UserFeatureKey.self] = newValue }
    }
}

struct UserFeatureKey: InjectedKey {
    static var liveValue: UserFeatureInterface = UserFeatureImpl()
    static var testValue: UserFeatureInterface = MockUserFeature()
}

모듈 간 의존성 주입

모듈 간 의존성 공유

swift
// Networking/Sources/APIClient.swift
import WeaveDI

public protocol APIClient {
    func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T
}

public struct NetworkingModule {
    public static func register() async {
        await WeaveDI.Container.bootstrap { container in
            container.register(APIClient.self) {
                URLSessionAPIClient(session: .shared)
            }
        }
    }
}

// APIClient를 주입 가능하게 만들기
extension InjectedValues {
    public var apiClient: APIClient {
        get { self[APIClientKey.self] }
        set { self[APIClientKey.self] = newValue }
    }
}

public struct APIClientKey: InjectedKey {
    public static var liveValue: APIClient = URLSessionAPIClient(session: .shared)
    public static var testValue: APIClient = MockAPIClient()
}
swift
// UserFeature/Sources/UserService.swift
import WeaveDI
import Networking

public final class UserService {
    @Injected(\.apiClient) var apiClient

    public func fetchUser(id: String) async throws -> User {
        try await apiClient.request(.user(id: id))
    }
}

기능 모듈 패턴

자체 완결형 기능 모듈

swift
// UserFeature/Sources/UserFeature.swift
import SwiftUI
import WeaveDI

public struct UserFeature {
    public init() {}

    // 공개 API
    public func makeUserProfileView() -> some View {
        UserProfileView()
    }

    public func makeUserListView() -> some View {
        UserListView()
    }
}

// 내부 의존성
extension InjectedValues {
    var userService: UserService {
        get { self[UserServiceKey.self] }
        set { self[UserServiceKey.self] = newValue }
    }

    var userRepository: UserRepository {
        get { self[UserRepositoryKey.self] }
        set { self[UserRepositoryKey.self] = newValue }
    }
}

struct UserServiceKey: InjectedKey {
    static var liveValue: UserService = UserServiceImpl()
    static var testValue: UserService = MockUserService()
}

struct UserRepositoryKey: InjectedKey {
    static var liveValue: UserRepository = UserRepositoryImpl()
    static var testValue: UserRepository = MockUserRepository()
}

기능 코디네이터 패턴

swift
// UserFeature/Sources/UserCoordinator.swift
import WeaveDI

public protocol UserCoordinator {
    func showUserProfile(userId: String)
    func showUserList()
}

public final class UserCoordinatorImpl: UserCoordinator {
    @Injected(\.navigationService) var navigation

    public init() {}

    public func showUserProfile(userId: String) {
        let view = UserProfileView(userId: userId)
        navigation.push(view)
    }

    public func showUserList() {
        let view = UserListView()
        navigation.push(view)
    }
}

// 코디네이터 등록
extension InjectedValues {
    public var userCoordinator: UserCoordinator {
        get { self[UserCoordinatorKey.self] }
        set { self[UserCoordinatorKey.self] = newValue }
    }
}

struct UserCoordinatorKey: InjectedKey {
    static var liveValue: UserCoordinator = UserCoordinatorImpl()
    static var testValue: UserCoordinator = MockUserCoordinator()
}

모듈 간 통신

이벤트 기반 통신

swift
// Core/Sources/EventBus.swift
import WeaveDI

public protocol Event {}

public protocol EventBus {
    func publish(_ event: Event)
    func subscribe<T: Event>(_ type: T.Type, handler: @escaping (T) -> Void)
}

public final class EventBusImpl: EventBus {
    private var handlers: [String: [(Event) -> Void]] = [:]

    public init() {}

    public func publish(_ event: Event) {
        let key = String(describing: type(of: event))
        handlers[key]?.forEach { $0(event) }
    }

    public func subscribe<T: Event>(_ type: T.Type, handler: @escaping (T) -> Void) {
        let key = String(describing: type)
        let wrapper: (Event) -> Void = { event in
            if let typedEvent = event as? T {
                handler(typedEvent)
            }
        }
        handlers[key, default: []].append(wrapper)
    }
}

// EventBus 등록
extension InjectedValues {
    public var eventBus: EventBus {
        get { self[EventBusKey.self] }
        set { self[EventBusKey.self] = newValue }
    }
}

struct EventBusKey: InjectedKey {
    static var liveValue: EventBus = EventBusImpl()
    static var testValue: EventBus = MockEventBus()
}
swift
// UserFeature가 이벤트 게시
public struct UserLoggedInEvent: Event {
    public let userId: String
}

public final class UserService {
    @Injected(\.eventBus) var eventBus

    public func login(credentials: Credentials) async throws {
        // 로그인 로직...
        eventBus.publish(UserLoggedInEvent(userId: user.id))
    }
}
swift
// OrderFeature가 이벤트 구독
public final class OrderService {
    @Injected(\.eventBus) var eventBus

    public init() {
        eventBus.subscribe(UserLoggedInEvent.self) { [weak self] event in
            self?.handleUserLogin(userId: event.userId)
        }
    }

    private func handleUserLogin(userId: String) {
        // 사용자의 주문 로드
    }
}

멀티 모듈 의존성 테스트

모듈 수준 테스트

swift
// UserFeatureTests/UserServiceTests.swift
import XCTest
@testable import UserFeature
import WeaveDI

final class UserServiceTests: XCTestCase {
    override func setUp() async throws {
        // 각 테스트마다 DI 리셋
        await WeaveDI.Container.reset()
    }

    func testFetchUser() async throws {
        await withInjectedValues { values in
            values.apiClient = MockAPIClient(
                responses: [.user(id: "123"): User.testUser]
            )
        } operation: {
            let service = UserService()
            let user = try await service.fetchUser(id: "123")

            XCTAssertEqual(user.id, "123")
            XCTAssertEqual(user.name, "Test User")
        }
    }
}

모듈 간 통합 테스트

swift
// IntegrationTests/UserOrderIntegrationTests.swift
import XCTest
@testable import UserFeature
@testable import OrderFeature
import WeaveDI

final class UserOrderIntegrationTests: XCTestCase {
    func testUserLoginTriggersOrderLoad() async throws {
        var orderLoadCalled = false

        await withInjectedValues { values in
            values.apiClient = MockAPIClient()
            values.eventBus = MockEventBus { event in
                if event is UserLoggedInEvent {
                    orderLoadCalled = true
                }
            }
        } operation: {
            let userService = UserService()
            let orderService = OrderService()

            try await userService.login(credentials: .test)

            XCTAssertTrue(orderLoadCalled)
        }
    }
}

모범 사례

✅ 할 것들

swift
// ✅ 명확한 모듈 경계 정의
public protocol UserFeatureInterface {
    var userService: UserServiceProtocol { get }
}

// ✅ 모듈 간 의존성에 프로토콜 사용
public protocol APIClient {
    func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T
}

// ✅ 모듈 의존성을 최소화
// UserFeature는 Networking과 SharedModels에만 의존

// ✅ 모듈 수준에서 의존성 등록
public struct UserFeatureModule {
    public static func register(in container: WeaveDI.Container) {
        // 모든 기능 의존성 등록
    }
}

// ✅ 느슨한 결합을 위한 이벤트 버스 사용
eventBus.publish(UserLoggedInEvent(userId: user.id))

❌ 하지 말 것들

swift
// ❌ 순환 모듈 의존성 생성 금지
// UserFeature → OrderFeature → UserFeature (나쁨!)

// ❌ 내부 구현 세부사항 노출 금지
public class UserServiceImpl { }  // internal이어야 함

// ❌ 여러 곳에서 의존성 등록 금지
// 모듈당 하나의 중앙 위치에서 등록

// ❌ 모듈 간 구체 타입 사용 금지
func process(service: UserServiceImpl)  // 대신 프로토콜 사용

// ❌ 모듈 경계 우회 금지
import UserFeature
let service = UserServiceImpl()  // 대신 DI 사용

마이그레이션 전략

모놀리스에서 멀티 모듈로

1단계: 모듈 식별

현재: 폴더가 있는 단일 타겟
목표: 별도의 SPM 패키지

App/
├── User/       → UserFeature 모듈
├── Order/      → OrderFeature 모듈
├── Payment/    → PaymentFeature 모듈
├── Network/    → Networking 모듈
└── Database/   → Database 모듈

2단계: 핵심 인프라 추출

swift
// 먼저 Networking 모듈 생성
// 모든 네트워킹 코드를 Networking 모듈로 이동
// import 업데이트: import Networking

3단계: 기능 모듈 추출

swift
// UserFeature 모듈 생성
// 사용자 관련 코드 이동
// 공개 인터페이스 정의
// 의존성 등록

4단계: DI 연결

swift
// DI 모듈 생성
// 모든 모듈 의존성 구성
// App 타겟에서 부트스트랩

성능 고려 사항

지연 모듈 로딩

swift
// 기능 모듈을 지연 로드
public final class FeatureLoader {
    private var loadedFeatures: Set<String> = []

    public func load(_ feature: Feature) async {
        guard !loadedFeatures.contains(feature.name) else { return }

        switch feature {
        case .user:
            await UserFeatureModule.bootstrap()
        case .order:
            await OrderFeatureModule.bootstrap()
        case .payment:
            await PaymentFeatureModule.bootstrap()
        }

        loadedFeatures.insert(feature.name)
    }
}

모듈 사전 로딩

swift
// 앱 시작 시 중요한 모듈 사전 로드
@main
struct MyApp: App {
    init() {
        Task {
            // 핵심 모듈 사전 로드
            await NetworkingModule.bootstrap()
            await DatabaseModule.bootstrap()

            // 기능 모듈 지연 로드
            await FeatureLoader.shared.load(.user)
        }
    }
}

다음 단계

MIT 라이선스 하에 릴리스됨.