← Lab
Swift 의존성 조립기(Assembler)
스터디2025.06.01
- Swift
- KeyChain
- Keychain
단일 AppAssembler 구조
import Foundation
// MARK: - Assembler Protocol
protocol Assembler {
func assemble(container: DIContainer)
}
// MARK: - App Assembler (단일 Assembler)
final class AppAssembler: Assembler {
func assemble(container: DIContainer) {
// MARK: - Network
container.register(RequesterProtocol.self, lifetime: .singleton) {
Requester(configuration: .default)
}
// MARK: - Keychain
container.register(KeychainManagerProtocol.self, lifetime: .singleton) {
KeychainManager()
}
// MARK: - Auth
container.register(AuthProviderProtocol.self, lifetime: .singleton) {
AuthProvider(keychainManager: container.resolve(KeychainManagerProtocol.self))
}
// MARK: - Login
container.register(LoginRepositoryProtocol.self, lifetime: .singleton) {
LoginRepository(
requester: container.resolve(RequesterProtocol.self),
keychainManager: container.resolve(KeychainManagerProtocol.self)
)
}
container.register(LoginUseCaseProtocol.self, lifetime: .singleton) {
LoginUseCase(repository: container.resolve(LoginRepositoryProtocol.self))
}
container.register(LoginViewModel.self, lifetime: .transient) {
LoginViewModel(
loginUseCase: container.resolve(LoginUseCaseProtocol.self),
authProvider: container.resolve(AuthProviderProtocol.self)
)
}
// MARK: - IssueList
container.register(IssueListViewModel.self, lifetime: .transient) {
IssueListViewModel(authProvider: container.resolve(AuthProviderProtocol.self))
}
}
}
// MARK: - 적용된 IssueTrackerApp
@main
struct IssueTrackerApp: App {
private let container = DIContainer.shared
@StateObject private var authProvider: AuthProvider
@StateObject private var loginCoordinator = LoginCoordinator()
init() {
// 1. Assembler 생성 및 의존성 조립
let assembler = AppAssembler()
assembler.assemble(container: container)
// 2. Container에서 AuthProvider resolve
let authProvider: AuthProvider = container.resolve(AuthProviderProtocol.self)
self._authProvider = StateObject(wrappedValue: authProvider)
}
var body: some Scene {
WindowGroup {
if authProvider.isLoggedIn {
NavigationStack(path: $loginCoordinator.path) {
loginCoordinator.build(.issueListView)
.navigationDestination(
for: NavigationRoute.self,
destination: loginCoordinator.build(_:))
}
} else {
NavigationStack(path: $loginCoordinator.path) {
loginCoordinator.build(.loginView)
.navigationDestination(
for: NavigationRoute.self,
destination: loginCoordinator.build(_:))
}
}
}
.environmentObject(authProvider)
}
}
// MARK: - 사용 예시 (다른 곳에서 ViewModel resolve)
/*
// LoginView에서
struct LoginView: View {
@StateObject private var viewModel: LoginViewModel
init() {
let vm: LoginViewModel = DIContainer.shared.resolve(LoginViewModel.self)
self._viewModel = StateObject(wrappedValue: vm)
}
var body: some View {
// UI 구현
}
}
*/
- 장점
- 파일 하나로 의존성 관리 가능
- 전체 의존성 흐름 한 눈에 파악 가능
- 간단한 구조로 초기 설정 용이
- 단점
- 프로젝트 규모가 커지면 Assembler 파일이 비대해짐
- 모듈별 의존성 분리가 안되어 특정 기능만 테스트 불가
모듈 단위 Assembler 구조
import Foundation
// MARK: - Assembler Protocol
protocol Assembler {
func assemble(container: DIContainer)
}
// MARK: - Auth Assembler
final class AuthAssembler: Assembler {
func assemble(container: DIContainer) {
// Keychain Manager
container.register(KeychainManagerProtocol.self, lifetime: .singleton) {
KeychainManager()
}
// Auth Provider
container.register(AuthProviderProtocol.self, lifetime: .singleton) {
AuthProvider(keychainManager: container.resolve(KeychainManagerProtocol.self))
}
}
}
// MARK: - Network Assembler
final class NetworkAssembler: Assembler {
func assemble(container: DIContainer) {
// Requester
container.register(RequesterProtocol.self, lifetime: .singleton) {
Requester(configuration: .default)
}
}
}
// MARK: - Login Assembler
final class LoginAssembler: Assembler {
func assemble(container: DIContainer) {
// Repository
container.register(LoginRepositoryProtocol.self, lifetime: .singleton) {
LoginRepository(
requester: container.resolve(RequesterProtocol.self),
keychainManager: container.resolve(KeychainManagerProtocol.self)
)
}
// UseCase
container.register(LoginUseCaseProtocol.self, lifetime: .singleton) {
LoginUseCase(repository: container.resolve(LoginRepositoryProtocol.self))
}
// ViewModel
container.register(LoginViewModel.self, lifetime: .transient) {
LoginViewModel(
loginUseCase: container.resolve(LoginUseCaseProtocol.self),
authProvider: container.resolve(AuthProviderProtocol.self)
)
}
}
}
// MARK: - IssueList Assembler
final class IssueListAssembler: Assembler {
func assemble(container: DIContainer) {
// ViewModel
container.register(IssueListViewModel.self, lifetime: .transient) {
IssueListViewModel(authProvider: container.resolve(AuthProviderProtocol.self))
}
}
}
// MARK: - App Assembler (Root)
final class AppAssembler: Assembler {
private let assemblers: [Assembler]
init(assemblers: [Assembler]) {
self.assemblers = assemblers
}
func assemble(container: DIContainer) {
assemblers.forEach { $0.assemble(container: container) }
}
}
// MARK: - 적용된 IssueTrackerApp
@main
struct IssueTrackerApp: App {
private let container = DIContainer.shared
@StateObject private var authProvider: AuthProvider
@StateObject private var loginCoordinator = LoginCoordinator()
init() {
// 1. Assembler 생성 및 의존성 조립
let assembler = AppAssembler(assemblers: [
NetworkAssembler(),
AuthAssembler(),
LoginAssembler(),
IssueListAssembler()
])
// 2. 모든 의존성 등록
assembler.assemble(container: container)
// 3. Container에서 AuthProvider resolve
let authProvider: AuthProvider = container.resolve(AuthProviderProtocol.self)
self._authProvider = StateObject(wrappedValue: authProvider)
}
var body: some Scene {
WindowGroup {
if authProvider.isLoggedIn {
NavigationStack(path: $loginCoordinator.path) {
loginCoordinator.build(.issueListView)
.navigationDestination(
for: NavigationRoute.self,
destination: loginCoordinator.build(_:))
}
} else {
NavigationStack(path: $loginCoordinator.path) {
loginCoordinator.build(.loginView)
.navigationDestination(
for: NavigationRoute.self,
destination: loginCoordinator.build(_:))
}
}
}
.environmentObject(authProvider)
}
}
// MARK: - 사용 예시 (다른 곳에서 ViewModel resolve)
/*
// LoginView에서
struct LoginView: View {
@StateObject private var viewModel: LoginViewModel
init() {
let vm: LoginViewModel = DIContainer.shared.resolve(LoginViewModel.self)
self._viewModel = StateObject(wrappedValue: vm)
}
var body: some View {
// UI 구현
}
}
*/
- Auth, Network, Login, IssueList로 분리된 개별 Assembler들
- AppleAssembler를 통해 모든 Assembler를 조합하는 루트 어셈블러