관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] clean architecture - DI (필요한 곳에서 protocol에 선언하는 방법) 본문

Architecture (swift)/MVVM (맛보기)

[iOS - swift] clean architecture - DI (필요한 곳에서 protocol에 선언하는 방법)

jake-kim 2021. 7. 13. 01:01

1. DI패턴 (필요한 곳에서 protocol에 선언하는 방법)

2. 테스트 구조를 고려한 DI패턴

DI 패턴

  • ADIContainer와 AViewModel이 있고 DIContainer에서 AViewModel를 만들 때, AViewModel에 필요한 값을 정의하는 방법
  • DIP와 테스트에 용이하기 위하여 protocol을 통해 설계는 2. 테스트 구조를 고려한 DI패턴 참고
  • 1. DI패턴 (필요한 곳에서 protocol에 선언하는 방법)의 목적
    • DIContainer자체가 구현체가 되는 패턴 파악
    • DIContainer가 구현체가 되는 '패턴'에 대해서 보며, 이 방법은 의미없다는것을 알고 Usecase위치는 ViewModel에 있어야한다는 것을 깨닫는 목적
    • 테스트시에 DIContainer 구현체를 변경하는 일은 적으므로 ViewModel에서 Usecase를 init값에 받는것이 이상적

일차원적인 DI 방법

  • A -> B화면전환 시, B의 Dependencies에 protocol이 아닌 class를 바로 참조하는 것
  • 구성
    • BViewController
    • BViewModel
    • BDIContainer
// BViewController.swift

class BViewController: UIViewController {

    private var viewModel: BViewModel!

    static func create(with viewModel: BViewModel) -> BViewController {
        let view = BViewController()
        view.viewModel = viewModel
        return view
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
    }
}
  • ViewModel에 Dependencies가 있고 BDIContainer에서 해당 코드를 바로 참조하는 형태
// BViewModel.swift

protocol BInput {
}

protocol BOutput {
}

protocol BViewModel: BInput, BOutput {}

class DefaultBViewModel: BViewModel {
    let dependencies: Dependencies

    struct Dependencies {
        let countNumber: Int
        let someUseCase: SomeUseCase
    }

    init(dependencies: Dependencies) {
        self.dependencies = dependencies
    }
}
  • BDIContainer: BViewModel의 Dependencies를 참조
// BDIContainer.swift

class BDIContainer {
    func makeBViewController(countNumber: Int) -> BViewController {
        let vc = BViewController.create(with: makeBViewModel(countNumber: countNumber))
        return vc
    }

    func makeBViewModel(countNumber: Int) -> BViewModel {
        return DefaultBViewModel(dependencies: .init(countNumber: countNumber,
                                                     someUseCase: SomeUseCase()))
    }
}

* source code: 일차원적인 DI방법

필요한 곳에서 protocol에 선언하고 주입하는 쪽에서 구현하여 사용 방법

  • BViewModel에 Protocol로 Dependencies 정의
// BViewModel.swift

protocol BInput {
}

protocol BOutput {
}

protocol BViewModel: BInput, BOutput {}

protocol BDependencies { // 추가
    func getSomeUseCase() -> SomeUseCase
}

class DefaultBViewModel: BViewModel {
    let dependencies: BDependencies // 추가
    let countNumber: Int

    struct Dependencies {
        let someUseCase: SomeUseCase
    }

    init(countNumber: Int, dependencies: BDependencies) {
        self.countNumber = countNumber
        self.dependencies = dependencies
    }

    func executeSomeUsecase() { // usecase는 주입받은 usecase이용
        let usecase = dependencies.getSomeUseCase()
        print("execute usecase")
    }
}
  • BDIContainer에서 protocol을 구현하여 self로 주입
    • 구현체가 BDIContainer가 되는 개념
// BDIContainer.swift

class BDIContainer {
    func makeBViewController(countNumber: Int) -> BViewController {
        let vc = BViewController.create(with: makeBViewModel(countNumber: countNumber))
        return vc
    }

    func makeBViewModel(countNumber: Int) -> BViewModel {
        return DefaultBViewModel(countNumber: countNumber, dependencies: self) // 추가: self로 주입
    }
}

extension BDIContainer: BDependencies { // 추가
    func getSomeUseCase() -> SomeUseCase {
        return SomeUseCase()
    }
}

* source code: 필요한 곳에서 protocol에 선언하고 주입하는 쪽에서 구현하여 사용 방법

테스트 관점에서 의미있는 코드인지 판단

  • Test 시에 BDIContainer를 사용 > UseCase를 갈아 치울 수 없고 이미 extension으로 정의되어 있으므로 usecase를 갈아치우며 테스트하는 로직에 어려움이 존재
  • UseCase는 ViewModel의 init에서 받는 구조가 되어야, 테스트 로직 시 usecase만 mock으로 만들고 viewMoel에 주입하여 테스트 하는 코드가 이상적
Comments