Skip to content

의존성 주입

Swinject 기반 DI 시스템

truloop iOS는 Swinject 라이브러리를 사용하여 의존성 주입을 관리합니다. 각 모듈별 Assembly 패턴을 통해 의존성을 등록하고, CompositionRoot에서 전체 Container를 구성합니다.

전체 DI 구조

CompositionRoot

앱 시작 시 모든 Assembly를 Container에 등록하고, 최상위 의존성을 해결합니다. 한 번 생성된 AppDependencysharedDependency에 캐싱되어 재사용됩니다.

swift
enum CompositionRoot {
    static let container = Container(defaultObjectScope: .container)
    static private var sharedDependency: AppDependency?

    static var shared: AppDependency {
        resolve()
    }

    static func resolve() -> AppDependency {
        if let dependency = sharedDependency {
            return dependency
        }

        let assemblies: [Assembly] = [
            AppAssembly(),
            NetworkingAssembly(),
            DomainAssembly(),
            OnboardAssembly(),
            HomeAssembly(),
            RepositoryAssembly(),
            ExtensionsAssembly(),
        ]

        assemblies.forEach { $0.assemble(container: self.container) }
        let resolver = self.container

        let dependency = AppDependency(
            splashBuilder: resolver.resolve(),
            introBuilder: resolver.resolve(),
            mainTabBarBuilder: resolver.resolve(),
            rootSceneRouteUseCase: resolver.resolve(),
            chatConfigUseCase: resolver.resolve(),
            purchaseRepository: resolver.resolve(),
            branchHandler: resolver.resolve(),
            appSchemeManager: resolver.resolve(),
            uploader: resolver.resolve(),
            googlePlaceConfigUseCase: resolver.resolve(),
            fcmManager: resolver.resolve(),
            liveActivityTokenManager: resolver.resolve(),
            loopShareUseCase: resolver.resolve(),
            accountUseCase: resolver.resolve(),
            notificationRepository: resolver.resolve()
        )
        sharedDependency = dependency

        return dependency
    }
}

주의

Container의 기본 objectScope.container로 설정되어 있으므로, 등록된 모든 의존성은 기본적으로 싱글톤처럼 동작합니다. 필요한 경우 개별 등록에서 scope를 변경할 수 있습니다.

Assembly 패턴

각 모듈은 자신의 Assembly 클래스를 통해 의존성을 등록합니다.

NetworkingAssembly

네트워크 인프라를 등록합니다:

swift
public final class NetworkingAssembly: Assembly {
    public func assemble(container: Container) {
        container.register(Networking.self) { resolver in
            NetworkingImpl(
                authInterceptor: resolver.resolve(),
                networkingErrorRouter: resolver.resolve()
            )
        }
    }
}

DomainAssembly

비즈니스 로직(UseCase)을 등록합니다:

swift
public final class DomainAssembly: Assembly {
    public func assemble(container: Container) {
        container.register(RootSceneRouteUseCase.self) { _ in
            RootSceneRouteUseCase()
        }

        container.register(AccountUseCase.self) { resolver in
            AccountUseCaseImpl(
                authRepository: resolver.resolve(),
                homeRepository: resolver.resolve(),
                purchaseRepository: resolver.resolve(),
                fcmManager: resolver.resolve(),
                keychainStore: resolver.resolve(),
                chatConfigUseCase: resolver.resolve()
            )
        }

        container.register(ContactManager.self) { resolver in
            ContactManagerImpl(contactRepository: resolver.resolve())
        }

        container.register(MissionManager.self) { resolver in
            MissionManagerImpl(homeRepository: resolver.resolve())
        }
    }
}

RepositoryAssembly

Repository 프로토콜의 구현체와 인프라 관련 의존성을 등록합니다:

swift
public final class RepositoryAssembly: Assembly {
    public func assemble(container: Container) {
        container.register(ChatConfigUseCase.self) { resolver in
            SendbirdConfigurator(
                deviceRepository: resolver.resolve(),
                channelListRepository: resolver.resolve()
            )
        }

        container.register(GooglePlaceConfigUseCase.self) { _ in
            GooglePlaceConfigurator()
        }

        container.register(AuthRepository.self) { resolver in
            AuthDataSourceImpl(networking: resolver.resolve())
        }

        container.register(DeviceRepository.self) { resolver in
            DeviceDataSourceImpl(networking: resolver.resolve())
        }

        container.register(LiveActivityTokenRepository.self) { resolver in
            LiveActivityTokenDataSourceImpl(networking: resolver.resolve())
        }

        container.register(HomeRepository.self) { resolver in
            HomeDataSourceImpl(networking: resolver.resolve())
        }

        container.register(ContactRepository.self) { resolver in
            ContactDataSourceImpl(networking: resolver.resolve())
        }

        container.register(FriendRepository.self) { resolver in
            FriendDataSourceImpl(networking: resolver.resolve())
        }

        container.register(NotificationRepository.self) { resolver in
            NotificationDataSourceImpl(networking: resolver.resolve())
        }

        container.register(ChannelListRepository.self) { _ in
            ChannelListDataSource()
        }

        container.register(ChannelRepoFactory.self) { resolver in
            ChannelRepositoryFactory(networking: resolver.resolve())
        }

        container.register(PurchaseRepository.self) { _ in
            PurchaseDataSource()
        }

        container.register(FCMManager.self) { resolver in
            FCMManagerImpl(
                deviceRepository: resolver.resolve(),
                keychainStore: resolver.resolve(),
                device: UIDevice.current
            )
        }

        container.register(LiveActivityTokenManager.self) { resolver in
            LiveActivityTokenManagerImpl(
                liveActivityTokenRepository: resolver.resolve(),
                keychainStore: resolver.resolve()
            )
        }

        container.register(AuthInterceptor.self) { resolver in
            AuthInterceptorImpl(
                keychainStore: resolver.resolve(),
                rootSceneRouteUseCase: resolver.resolve()
            )
        }
    }
}

HomeAssembly (Feature-level DI)

Feature 모듈의 Assembly는 해당 Feature의 모든 Builder를 등록합니다. 각 화면은 Buildable 프로토콜과 Builder 구현체, Dependency 구조체의 조합으로 구성됩니다:

swift
public final class HomeAssembly: Assembly {
    public func assemble(container: Container) {
        // 홈 화면 Builder
        container.register(HomeBuildable.self) { resolver in
            HomeBuilder(dependency: HomeDependency(
                loopMainBuilder: resolver.resolve(),
                homeRepository: resolver.resolve(),
                purchaseRepository: resolver.resolve(),
                uploader: resolver.resolve(),
                // ...
            ))
        }

        // 룹 메인 Builder
        container.register(LoopMainBuildable.self) { resolver in
            LoopMainBuilder(dependency: LoopMainDependency(
                loopDetailBuilder: resolver.resolve(),
                homeRepository: resolver.resolve(),
                // ...
            ))
        }

        // 프로필 Builder
        container.register(ProfileBuildable.self) { resolver in
            ProfileBuilder(dependency: ProfileDependency(
                accountUseCase: resolver.resolve(),
                rootSceneRouteUseCase: resolver.resolve(),
                // ...
            ))
        }

        // ... 30+ Builder 등록
    }
}

Resolver Extension

Swinject의 Resolver를 확장하여 타입만으로 resolve할 수 있는 편의 메서드를 제공합니다. 반환 타입이 Service! (Implicitly Unwrapped Optional)이므로, 등록되지 않은 타입을 resolve하면 런타임에 crash가 발생합니다:

swift
// Utils/Extensions/Sources/Resolver+ext.swift
public extension Resolver {
    func resolve<Service>() -> Service! {
        return self.resolve(Service.self)
    }
}

이를 통해 다음과 같이 간결한 코드 작성이 가능합니다:

swift
// Before
let repo = resolver.resolve(HomeRepository.self)!

// After
let repo: HomeRepository = resolver.resolve()

의존성 등록 전체 맵

Assembly등록 항목
AppAssemblyMainTabBarBuildable, BranchHandler, AppSchemeManager, AppSchemeHandler, LoopImageUploader, LoopImageSimpleUploader, ToastManager, NetworkingErrorRouter, LoopShareUseCase
NetworkingAssemblyNetworking 구현체
DomainAssemblyRootSceneRouteUseCase, AccountUseCase, ContactManager, MissionManager
RepositoryAssemblyChatConfigUseCase, GooglePlaceConfigUseCase, AuthRepository, DeviceRepository, LiveActivityTokenRepository, HomeRepository, ContactRepository, FriendRepository, NotificationRepository, ChannelListRepository, ChannelRepoFactory, PurchaseRepository, FCMManager, LiveActivityTokenManager, AuthInterceptor
HomeAssembly30+ Builder (Home, LoopMain, LoopDetail, Profile, Channel, ChannelList, CreateLoop, CreateStory, StoryDetail, Paywall, InAppPurchase, Reels, FriendSearch, PosterSelect, AddPhotosForLoop, CreateAppointment, ArrangementList, Notification 등)
OnboardAssemblySplash, Intro, PhoneNumber, VerificationCode, UserName, UserID, ProfileImage, Contact, CreateNewLoop, ImagePermission, PushPermission Builder
ExtensionsAssemblyKeychainStore

변경 이력

날짜내용
2026-03-10소스 코드 기반 검증: CompositionRoot 캐싱 로직 추가, AppDependency 필드 전체 반영, RepositoryAssembly 전체 등록 항목 반영, Resolver extension 시그니처 수정 (Service!), ExtensionsAssembly 등록 항목 수정 (KeychainStore만), AppAssembly 등록 항목 상세화, HomeAssembly builder 수 30+로 수정