[팝콘]개발기록 - DIContainer를 사용해보자

sunghun kim·2025년 3월 17일

[팝콘-프로젝트]

목록 보기
6/6

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가지 입니다!

  1. 뷰컨트롤러를 생성하는 로직이 간단하고, 가시성도 좋기
  2. 관계없는 인스턴스는 생성하지 않기
  3. 테스트 환경에서 Mock 인스턴스로 바꿀 수 있기

사용될 개념

딕셔너리 개념과 Register & Resolve 방식을 사용합니다!

  1. Register(등록)
    • 특정 타입의 객체를 DIContainer에 등록하는 과정입니다.
    • 자동차를 만들기 위해 어떤 엔진을 사용할지 등록하는 것이라고 볼 수 있습니다.
    • TokenRepositoryProtocol 타입의 TokenRepository 객체를 DIContainer에 등록한다!
  2. Resolve(해결)
    • 필요할 때 등록된 객체를 가져오는 과정(해결)입니다.
    • 자동차가 필요할 때 등록되어있는 엔진을 가져와 사용하는 것이라고 볼 수 있습니다.
    • 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] = [:]
    • dependencies
      • 의존성을 저장하는 딕셔너리
      • Key: String (의존성의 타입 이름을 문자열로 저장)
      • Value: Any (어떤 타입이든 저장 가능하도록 Any 사용)
    • KeyString인 이유
      • Swift에서는 Type을 Key로 사용할 수 없음
  • 코드 설명: Register 함수
    func register<T>(_ type: T.Type, instance: T) {
        let key = String(describing: type)
        dependencies[key] = instance
    }
    • 제네릭(Generic) 사용
      • func register<T>:
        • 제네릭 함수로 모든 타입(T)을 등록할 수 있음
        • <T>는 어떤 타입이든 사용할 수 있도록 해주는 Swift의 기능
      • _ type: T.Type:
        • 타입의 메타타입(Metatype)을 전달받음
          • 메타타입은 타입의 타입임

          • 예를들어 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
    • Key 생성 및 의존성 저장
      • let key = String(describing: type)
        • 타입의 이름을 문자열로 변환하여 Key로 사용
        • 예: NetworkManagerProtocol타입 → “NetworkManagerProtocol”
      • dependencies[key] = instance
        • 딕셔너리 dependenciesKeyInstance를 저장함
    • Register 함수 사용 예시
      let container = DIContainer.shared
      container.register(NetworkManagerProtocol.self, instance: NetworkManger())
      container.register(TokenRepositoryProtocol.self, instance: TokenRepository())
      저장된 데이터 구조:
      dependencies = [
          "NetworkMangerProtocol": NetworkManager()
          "TokenRepositoryProtocol": TokenRepository()
      ]
      나는 NetworkManagerProtocol을 사용할 때 NetworkManager를 쓰겠다 라고 등록!
  • 코드 설명: 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
    }
    • Key 생성
      • let key = String(describing: type):
        • Register와 동일하게 타입의 이름을 문자열로 변환하여 Key를 사용함
    • Instance 가져오기
      • guard let instance = dependencies[key] as? T else:
        • dependencies 딕셔너리에서 Key에 해당하는 객체를 가져옴.
        • 가져온 객체를 원하는 타입(T)로 캐스팅함
    • 에러처리
      • fatalError():
        • Key에 해당하는 객체가 없거나, 캐스팅 실패 시 앱을 강제로 종료함
    • 사용 예시
      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 인스턴스를 사용한다면!
    // 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하는 코드를 몰라도 되게 하였다.
  • 어찌 됐든 등록(Register)→해결(Resolve) 순으로 해야하니깐 다음 순서대로 하였다.
    • 등록(Register)
      • AppDelegate에서 등록 (뷰모델, 유즈케이스, 레포지토리, 네트워크매니저, 키체인매니저)
      • DIContainer에서 등록하는 메서드 (make뷰컨)
    • 해결(Resolve)
      • AppDelegate에서 등록할 때 Resolve하여 사용 (뷰모델, 유즈케이스, 레포지토리, 네트워크매니저, 키체인매니저)
      • DIContainer에서 등록할 때 Resolve하여 사용 (make뷰컨)
  • 그리고 뷰컨을 생성하는 코드를 다음과 같이 작성하면 완료이다. (뷰컨을 생성이니깐 뷰컨에서만 일어남)
    @objc private func signUpButtonTapped() {
        let signUpFirstViewController = DIContainer.shared.makeSignUpFirstViewController()
        self.navigationController?.pushViewController(signUpFirstViewController, animated: true)
    }
  • AppDelegateDIContainer이외에 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)
            }
        }
    }
    ```
profile
기죽지않기

0개의 댓글