https://eunjin3786.tistory.com/233
https://bibi6666667.tistory.com/459
https://velog.io/@kimscastle/iOS-DI-Container%EB%A5%BC-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EC%9E%90feat.-Swinject
뷰 컨트롤러를 생성하려고 할 때마다 레포지토리, 유즈케이스와 뷰모델을 생성하고 주입해야하는
보기도 안좋고, 코드의 중복도 많은 상황이었습니다.
이를 해결해보고자 레포지토리, 유즈케이스와 뷰모델을 생성하는 인스턴스를 만들고 이 인스턴스의 메서드로 뷰컨트롤러를 만드는 다음 코드를 작성했습니다.
//
// DIContainer.swift
// Popcorn-iOS
//
// Created by 김성훈 on 2/16/25.
//
import Foundation
final class DIContainer {
// MARK: - Network, Keychain
private let networkManager: NetworkManagerProtocol
private let keychainManager: KeychainManagerProtocol
// MARK: - Token
private let tokenRepository: TokenRepositoryProtocol
let tokenUseCase: TokenUseCaseProtocol
// MARK: - Login
private let loginRepository: LoginRepositoryProtocol
private let loginUseCase: LoginUseCaseProtocol
// MARK: - Social Login
private let socialLoginRepository: SocialLoginRepositoryProtocol
private let socialLoginUseCase: SocialLoginUseCaseProtocol
// MARK: - SignUp
private let signUpRepository: SignUpRepositoryProtocol
private let signUpUseCase: SignUpUseCaseProtocol
// MARK: - Initializer
init() {
self.networkManager = NetworkManager()
self.keychainManager = KeychainManager()
self.tokenRepository = TokenRepository(
networkManager: networkManager,
keychainManager: keychainManager
)
self.loginRepository = LoginRepository(
networkManager: networkManager
)
self.socialLoginRepository = SocialLoginRepository(
networkManager: networkManager,
keychainManager: keychainManager
)
self.signUpRepository = SignUpRepository(
networkManager: networkManager,
keychainManager: keychainManager
)
self.tokenUseCase = TokenUseCase(
tokenRepository: tokenRepository
)
self.loginUseCase = LoginUseCase(
loginRepository: loginRepository,
tokenRepository: tokenRepository
)
self.socialLoginUseCase = SocialLoginUseCase(
socialLoginRepository: socialLoginRepository,
tokenRepository: tokenRepository
)
self.signUpUseCase = SignUpUseCase(
signUpRepository: signUpRepository
)
}
}
// MARK: - ViewModel, ViewController 생성
extension DIContainer {
// MARK: - Login
func makeLoginViewModel() -> LoginViewModelProtocol {
return LoginViewModel(loginUseCase: loginUseCase)
}
func makeSocialLoginViewModel() -> SocialLoginViewModelProtocol {
return SocialLoginViewModel(socialLoginUseCase: socialLoginUseCase)
}
func makeLoginViewController() -> LoginViewController {
return LoginViewController(
loginViewModel: makeLoginViewModel(),
socialLoginViewModel: makeSocialLoginViewModel()
)
}
// MARK: - SignUp
func makeSignUpFirstViewModel() -> SignUpFirstViewModelProtocol {
return SignUpFirstViewModel(signUpUseCase: signUpUseCase)
}
func makeSignUpSecondViewModel() -> SignUpSecondViewModelProtocol {
return SignUpSecondViewModel(signUpUseCase: signUpUseCase)
}
func makeSignUpFirstViewController() -> SignUpFirstViewController {
return SignUpFirstViewController(signUpFirstViewModel: makeSignUpFirstViewModel())
}
func makeSignUpSecondViewController() -> SignUpSecondViewController {
return SignUpSecondViewController(signUpSecondViewModel: makeSignUpSecondViewModel())
}
}
코드의 중복은 피할 수 있지만, 뷰컨트롤러를 생성할 때 관계없는 옆집 레포지토리도 생성한다는 문제와 테스트 환경에서 Mock 인스턴스를 사용하고 싶어도 바꿀 방법이 없는 문제가 생겨버렸습니다.
목표는 다음 3가지 입니다!
Mock 인스턴스로 바꿀 수 있기딕셔너리 개념과 Register & Resolve 방식을 사용합니다!
TokenRepositoryProtocol 타입의 TokenRepository 객체를 DIContainer에 등록한다!TokenRepositoryProtocol 을 요청하면 TokenRepository 객체를 반환한다!DIContainer// 코드 전체
final class DIContainer {
static let shared = DIContainer() // 싱글톤
private var dependencies: [String: Any] = [:]
private init() {}
// MARK: - Register(등록)
func register<T>(_ type: T.Type, instance: T) {
let key = String(describing: type)
dependencies[key] = instance
}
// MARK: - Resolve(해결)
func resolve<T>(_ type: T.Type) -> T {
let key = String(describing: type)
guard let instance = dependencies[key] as? T else {
fatalError("\(key) 의존성이 등록되지 않았습니다.")
}
return instance
}
}
final class DIContainer {
static let shared = DIContainer()final
DIContainer가 상속되지 않도록 함static let shared = DIContainer()
- 싱글톤 패턴
- shared 라는 정적(static) 속성을 사용해서 앱 전체에서 동일한 인스턴스를 사용할 수 있게 함
- DIContainer.shared 를 사용하여 어디서든 동일한 DIContainer 인스턴스에 접근할 수 있음
private init() {}
init()을 private으로 제한하여 외부에서 인스턴스 생성을 금지 → 싱글톤 패턴을 유지하기 위함
DIContainer.shared로만 접근 가능
private var dependencies: [String: Any] = [:]dependenciesString (의존성의 타입 이름을 문자열로 저장)Any (어떤 타입이든 저장 가능하도록 Any 사용)String인 이유func register<T>(_ type: T.Type, instance: T) {
let key = String(describing: type)
dependencies[key] = instance
}func register<T>:<T>는 어떤 타입이든 사용할 수 있도록 해주는 Swift의 기능_ type: T.Type:메타타입은 타입의 타입임
예를들어 Int는 타입, Int.Type은 메타타입으로 Int타입 자체를 나타내는 타입임
let numberType: Int.Type = Int.self 라고 하면
- Int.self: Int타입의 인스턴스가 아니라 Int타입 자체를 말하는거임
- .self: 타입 이름 뒤에 붙여 해당 타입의 메타타입 인스턴스를 반환
// 1. Int.Type 사용
let numberType: Int.Type = Int.self
print(numberType) // 출력: Int
print(numberType.init(42)) // 출력: 42 (Int 타입의 값을 생성)
// 2. Int 사용
let number: Int = 5
print(number) // 출력: 5
let key = String(describing: type)NetworkManagerProtocol타입 → “NetworkManagerProtocol”dependencies[key] = instancedependencies에 Key와 Instance를 저장함let container = DIContainer.shared
container.register(NetworkManagerProtocol.self, instance: NetworkManger())
container.register(TokenRepositoryProtocol.self, instance: TokenRepository()) 저장된 데이터 구조:dependencies = [
"NetworkMangerProtocol": NetworkManager()
"TokenRepositoryProtocol": TokenRepository()
] 나는 NetworkManagerProtocol을 사용할 때 NetworkManager를 쓰겠다 라고 등록!func resolve<T>(_ type: T.Type) -> T {
let key = String(describing: type)
guard let instance = dependencies[key] as? T else {
fatalError("\(key) 의존성이 등록되지 않았습니다.")
}
return instance
}let key = String(describing: type):guard let instance = dependencies[key] as? T else:dependencies 딕셔너리에서 Key에 해당하는 객체를 가져옴.fatalError():let container = DIContainer.shared
// 의존성 등록
container.register(NetworkMangerProtocol.self, instance: NetworkManager())
container.register(TokenRepositoryProtocol.self, instance: TokenRepository())
// 의존성 해결
let networkManager: NetworkManagerProtocol = container.resolve(NetworkManagerProtocol.self)
let tokenRepository: TokenRepositoryProtocol = container.resolve(TokenRepositoryProtocol.self) 나는 NetworkManagerProtocol을 사용할 건데, 등록된 NetworkManager를 가져와 사용하겠다 라고 해결함!// Mock 객체 등록
container.register(NetworkManagerProtocol.self, instance: MockNetworkManager())
container.register(TokenRepositoryProtocol.self, instance: MockTokenRepository())
// 테스트에서는 Mock 객체가 반환됨
let networkManager: NetworkManagerProtocol = container.resolve(NetworkManagerProtocol.self) // MockNetworkManager 사용됨
DIContainer 코드//
// DIContainer.swift
// Popcorn-iOS
//
// Created by 김성훈 on 2/16/25.
//
import Foundation
final class DIContainer {
static let shared = DIContainer()
private var dependencies: [String: Any] = [:]
private init() {}
// MARK: - Register
func register<T>(_ type: T.Type, instance: T) {
let key = String(describing: type)
dependencies[key] = instance
}
// MARK: - Resolve
func resolve<T>(_ type: T.Type) -> T {
let key = String(describing: type)
guard let instance = dependencies[key] as? T else {
fatalError("\(key) 의존성이 등록되지 않았습니다.")
}
return instance
}
}
// MARK: - Make ViewController
extension DIContainer {
func makeLoginViewController() -> LoginViewController {
return LoginViewController(
loginViewModel: resolve(LoginViewModelProtocol.self),
socialLoginViewModel: resolve(SocialLoginViewModelProtocol.self)
)
}
func makeSignUpFirstViewController() -> SignUpFirstViewController {
return SignUpFirstViewController(
signUpFirstViewModel: resolve(SignUpFirstViewModelProtocol.self)
)
}
func makeSignUpSecondViewController() -> SignUpSecondViewController {
return SignUpSecondViewController(
signUpSecondViewModel: resolve(SignUpSecondViewModelProtocol.self)
)
}
}
AppDelegate 코드func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
registerDependencies()
return true
}
// MARK: - Register Dependencies
private func registerDependencies() {
let diContainer = DIContainer.shared
// MARK: - NetworkManager, KeychainManager
diContainer.register(
NetworkManagerProtocol.self,
instance: NetworkManager()
)
diContainer.register(
KeychainManagerProtocol.self,
instance: KeychainManager()
)
// MARK: - Repositories
diContainer.register(
TokenRepositoryProtocol.self,
instance: TokenRepository(
networkManager: diContainer.resolve(NetworkManagerProtocol.self),
keychainManager: diContainer.resolve(KeychainManagerProtocol.self)
)
)
diContainer.register(
LoginRepositoryProtocol.self,
instance: LoginRepository(
networkManager: diContainer.resolve(NetworkManagerProtocol.self)
)
)
diContainer.register(
SocialLoginRepositoryProtocol.self,
instance: SocialLoginRepository(
networkManager: diContainer.resolve(NetworkManagerProtocol.self),
keychainManager: diContainer.resolve(KeychainManagerProtocol.self)
)
)
diContainer.register(
SignUpRepositoryProtocol.self,
instance: SignUpRepository(
networkManager: diContainer.resolve(NetworkManagerProtocol.self),
keychainManager: diContainer.resolve(KeychainManagerProtocol.self)
)
)
// MARK: - UseCases
diContainer.register(
TokenUseCaseProtocol.self,
instance: TokenUseCase(
tokenRepository: diContainer.resolve(TokenRepositoryProtocol.self)
)
)
diContainer.register(
LoginUseCaseProtocol.self,
instance: LoginUseCase(
loginRepository: diContainer.resolve(LoginRepositoryProtocol.self),
tokenRepository: diContainer.resolve(TokenRepositoryProtocol.self)
)
)
diContainer.register(
SocialLoginUseCaseProtocol.self,
instance: SocialLoginUseCase(
socialLoginRepository: diContainer.resolve(SocialLoginRepositoryProtocol.self),
tokenRepository: diContainer.resolve(TokenRepositoryProtocol.self)
)
)
diContainer.register(
SignUpUseCaseProtocol.self,
instance: SignUpUseCase(
signUpRepository: diContainer.resolve(SignUpRepositoryProtocol.self)
)
)
// MARK: - ViewModels
diContainer.register(
LoginViewModelProtocol.self,
instance: LoginViewModel(
loginUseCase: diContainer.resolve(LoginUseCaseProtocol.self)
)
)
diContainer.register(
SocialLoginViewModelProtocol.self,
instance: SocialLoginViewModel(
socialLoginUseCase: diContainer.resolve(SocialLoginUseCaseProtocol.self)
)
)
diContainer.register(
SignUpFirstViewModelProtocol.self,
instance: SignUpFirstViewModel(
signUpUseCase: diContainer.resolve(SignUpUseCaseProtocol.self)
)
)
diContainer.register(
SignUpSecondViewModelProtocol.self,
instance: SignUpSecondViewModel(
signUpUseCase: diContainer.resolve(SignUpUseCaseProtocol.self)
)
)
}factory 패턴을 살짝 사용하였다.DIContainer.shared.make뷰컨 이런식으로resolve하는 코드를 몰라도 되게 하였다.AppDelegate에서 등록 (뷰모델, 유즈케이스, 레포지토리, 네트워크매니저, 키체인매니저)DIContainer에서 등록하는 메서드 (make뷰컨)AppDelegate에서 등록할 때 Resolve하여 사용 (뷰모델, 유즈케이스, 레포지토리, 네트워크매니저, 키체인매니저)DIContainer에서 등록할 때 Resolve하여 사용 (make뷰컨)@objc private func signUpButtonTapped() {
let signUpFirstViewController = DIContainer.shared.makeSignUpFirstViewController()
self.navigationController?.pushViewController(signUpFirstViewController, animated: true)
}AppDelegate와 DIContainer이외에 resolve키워드를 사용하는 예외가 하나 있었다.```swift
let tokenUseCase = DIContainer.shared.resolve(TokenUseCaseProtocol.self)
let loginViewController = DIContainer.shared.makeLoginViewController()
// MARK: - 토큰 상태에 따른 초기화면 설정
tokenUseCase.handleTokenExpiration { [weak self] isTokenValid in
guard let self = self else { return }
DispatchQueue.main.async {
if isTokenValid {
let mainSceneViewController = MainSceneViewController()
self.window?.rootViewController = UINavigationController(
rootViewController: mainSceneViewController
)
} else {
self.window?.rootViewController = UINavigationController(rootViewController: loginViewController)
}
}
}
```