다크 모드
의존성 주입
Swinject 기반 DI 시스템
truloop iOS는 Swinject 라이브러리를 사용하여 의존성 주입을 관리합니다. 각 모듈별 Assembly 패턴을 통해 의존성을 등록하고, CompositionRoot에서 전체 Container를 구성합니다.
전체 DI 구조
CompositionRoot
앱 시작 시 모든 Assembly를 Container에 등록하고, 최상위 의존성을 해결합니다. 한 번 생성된 AppDependency는 sharedDependency에 캐싱되어 재사용됩니다.
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 | 등록 항목 |
|---|---|
| AppAssembly | MainTabBarBuildable, BranchHandler, AppSchemeManager, AppSchemeHandler, LoopImageUploader, LoopImageSimpleUploader, ToastManager, NetworkingErrorRouter, LoopShareUseCase |
| NetworkingAssembly | Networking 구현체 |
| DomainAssembly | RootSceneRouteUseCase, AccountUseCase, ContactManager, MissionManager |
| RepositoryAssembly | ChatConfigUseCase, GooglePlaceConfigUseCase, AuthRepository, DeviceRepository, LiveActivityTokenRepository, HomeRepository, ContactRepository, FriendRepository, NotificationRepository, ChannelListRepository, ChannelRepoFactory, PurchaseRepository, FCMManager, LiveActivityTokenManager, AuthInterceptor |
| HomeAssembly | 30+ Builder (Home, LoopMain, LoopDetail, Profile, Channel, ChannelList, CreateLoop, CreateStory, StoryDetail, Paywall, InAppPurchase, Reels, FriendSearch, PosterSelect, AddPhotosForLoop, CreateAppointment, ArrangementList, Notification 등) |
| OnboardAssembly | Splash, Intro, PhoneNumber, VerificationCode, UserName, UserID, ProfileImage, Contact, CreateNewLoop, ImagePermission, PushPermission Builder |
| ExtensionsAssembly | KeychainStore |
변경 이력
| 날짜 | 내용 |
|---|---|
| 2026-03-10 | 소스 코드 기반 검증: CompositionRoot 캐싱 로직 추가, AppDependency 필드 전체 반영, RepositoryAssembly 전체 등록 항목 반영, Resolver extension 시그니처 수정 (Service!), ExtensionsAssembly 등록 항목 수정 (KeychainStore만), AppAssembly 등록 항목 상세화, HomeAssembly builder 수 30+로 수정 |