* 진행 코드는 https://github.com/jeongju9216/Jetflix에서 볼 수 있고, PR에서 에피소드 단위로 코드를 확인할 수 있습니다.
서론
이번에는 프로젝트에 의존성 주입을 개선했습니다.
기존에는 객체가 필요한 곳에서 바로 객체를 생성하여 사용했습니다.
그래서 ViewController에서 ViewModel을 생성할 때면 UseCase와 Repository를 매번 생성해야 했습니다.
이를 DI Container를 이용해 외부에서 주입해서 중복되는 코드를 줄이고,
의존성 관심사를 DI Container로 몰아 넣어서 프로젝트 관리 용이, 객체 재사용성을 높이도록 개선했습니다.
let viewModel = HomeViewModel(getContentUseCase: .init(repository: ContentRepository()),
saveContentUseCase: .init(repository: ContentRepository()))
@Dependency var viewModel: HomeViewModel
전후 코드 차이가 크다는 것을 확인할 수 있습니다.
개선 내용은 아래와 같습니다.
- Dictionary를 이용한 DI Container 구현
- ViewModel, UseCase, Repository DI Container를 이용해 의존성 주입
- ContentUseCase 상속을 이용해 효율적인 Repository 객체 관리
1.
DI와 DI Container 관련 내용은 과거에 다룬 적이 있습니다.
- 의존성 주입 DI(Dependency Injection)와 IoC(Inversion Of Control)
- Dependency Container(feat. Property Wrapper)
이론과 간단한 예제로만 다뤘던 개념을 실제 프로젝트에 적용해보았던 경험이었습니다.
코드는 생각보다 간단합니다.
final class DIContainer {
static var shared = DIContainer()
private init() { }
private var dependencies: [String: Any] = [:]
func register<T>(type: T.Type, service: Any) {
dependencies["\(type)"] = service
}
func resolve<T>(type: T.Type) -> T {
let dependency = dependencies["\(type)"] as? T
return dependency!
}
}
register 메서드를 통해 딕셔너리에 타입을 넣고, resolve를 이용해 객체를 생성하는 원리입니다.
type에 프로토콜 타입을, service에 구현 타입을 넣으면 상황에 맞는 객체를 주입할 수도 있습니다.
예를 들어, 테스트 상황에서 테스트 객체를 사용하고 싶을 때 손쉽게 교체할 수 있을 것입니다.
@propertyWrapper
class Dependency<T> {
let wrappedValue: T
init() {
self.wrappedValue = DIContainer.shared.resolve(type: T.self)
}
}
PropertyWrapper를 이용하면 좀 더 편하게 resolve 할 수 있습니다.
객체가 init할 때 DIContainer에서 resolve 되도록 PropertyWrapper를 구현합니다.
var viewModel = DIContainer.shared.resolve(type: HomeViewModel.self)
덕분에 위처럼 매번 resolve를 해야하는 코드와는 다르게,
@Dependency private var viewModel: HomeViewModel
타입만 명시해주면 PropertyWrapper로 깔끔하게 생성할 수 있습니다.
2.
DI Container를 바탕으로 의존성 주입을 진행합니다.
Jetflix 프로젝트의 ContentUseCase 들은 ContentUseCaseProtocol을 채택해야 합니다.
ContentUseCaseProtocol은 ContentRepositoryProtocol 프로퍼티를 반드시 가져야 합니다.
이 ContentRepositoryProtocol에는 ContentRepository 객체를 주입해서 사용할 수 있습니다.
기존에는 모든 ContentUseCase 생성 코드에서 ContentRepository를 생성했습니다.
하지만 DIContainer를 이용하면 단 한 줄로 Repository를 주입할 수 있습니다.
DIContainer.shared.register(type: ContentRepositoryProtocol.self, service: ContentRepository())
ContentRepositoryProtocol 타입에 ContentRepository 객체를 register 합니다.
ContentRepositoryProtocol 타입에 @Dependency를 기재하면 ContentRepository 객체가 resolve 됩니다.
(이에 대해서는 바로 아래 3번 문단에서 다루겠습니다.)
이외에도 UseCase, ViewModel를 직접 생성하던 코드에서 DIContainer를 이용해 주입하는 방식으로 변경했습니다.
DIContainer.shared.register(type: GetContentUseCase.self, service: GetContentUseCase())
DIContainer.shared.register(type: SaveContentUseCase.self, service: SaveContentUseCase())
final class HomeViewModel {
@Dependency private var getContentUseCase: GetContentUseCase
@Dependency private var saveContentUseCase: SaveContentUseCase
...
}
DIContainer.shared.register(type: HomeViewModel.self, service: HomeViewModel())
class HomeViewController: UIViewController {
...
@Dependency private var viewModel: HomeViewModel
...
}
하지만 이 부분은 조금 더 고민해볼만한 여지가 있습니다.
의존성 주입은 유동적으로 주입하는 객체를 변경할 수 있다는 장점이 있습니다.
하지만 "앱"에서는 하나의 화면에 하나의 ViewModel이 종속적인 경우가 많습니다.
Home 화면에는 HomeViewModel, Upcoming 화면에는 UpcomingViewModel 이렇게 1:1로 매칭되는게 대부분이죠.
그렇기 때문에 유동적으로 주입한다는 장점이 크게 의미가 없습니다.
굳이 다른 객체가 주입될 수 있다는 위험성이 생기는 것이죠.
따라서 그냥 ViewModel에서 사용하는 객체를 바로 생성하는 것도 나쁘지 않는 선택일 것입니다.
저는 DI Container를 처음 적용해보는 초보자라 일단 적용은 했지만, 고민은 해야하는 사항 같습니다.
(빨리 취업하고 싶네요.)
3.
2번에서 잠시 다뤘던 ContentUseCaseProtocol 내용입니다.
기존에는 ContentUseCaseProtocol만을 이용해 세부 UseCase를 구현했습니다.
이번 작업에서는 상속을 사용해보았습니다.
class ContentUseCase: ContentUseCaseProtocol {
@Dependency var repository: ContentRepositoryProtocol
}
final class GetContentUseCase: ContentUseCase, GetContentUseCaseProtocol {
...
}
GetContentUseCase, SaveContentUseCase 등에서 모두 repository를 생성하는게 아니라,
부모 클래스인 ContentUseCase에서 repository를 생성하고,
자식 클래스인 세부적인 ContentUseCase는 부모의 repository를 사용하는 것입니다.
DI를 효율적으로 하면서, 공통된 Repository 타입을 갖는다는 것을 코드로 표현할 수 있었습니다.
마무리
Jetflix 클론 코딩은 어느 정도 마무리가 되어가고 있습니다.
CollectionView 무한 스크롤정도만 추가하고 다른 클론 코딩으로 넘어갈 듯 합니다.
다른 클론 코딩을 하면서도 배우는 점이 있을테니 서로 상호 보완하여 성장해보려고 합니다.
빨리 취업하고 싶네요.
감사합니다.
아직은 초보 개발자입니다.
더 효율적인 코드 훈수 환영합니다!
공감과 댓글 부탁드립니다.