Swinject
https://github.com/Swinject/Swinject
DI(Dependency Injection)는 종속성을 해결하기 위해 IoC(Inversion of Control)을 구현하는 소프트웨어 디자인 패턴이다.
Swinject는 Swift용 DIContainer라이브러리로, DIContainer를 구현하고 사용할 수 있도록 도와주는 친구이다.
Swinject는 의존성 주입을 위한 DIContainer를 제공하며 클래스나 객체의 인스턴스를 생성하고 의존성을 주입하는 기능을 제공한다.
Swinject의 핵심 개념은 "컨테이너(Container)"이다. 컨테이너는 객체의 생성과 의존성을 해결관리하는 역할을 한다.
Swinject에서는 컨테이너를 생성하고 등록된 객체를 요청할 때 의존성을 자동으로 주입하는 역할을 한다.
의존성 주입을 쉽게 구현할수 있도록 도와주기 때문에 모듈화와 테스트 용이성을 향상시키는데 도움이 된다.
말로는 어렵고 튜토리얼을 보는 게 좋겠다.
Tutorial
여기서 사용한 모델들은 다음과 같다.
protocol Animal {
var name: String? { get }
}
class Cat: Animal {
let name: String?
init(name: String?) {
self.name = name
}
}
protocol Person {
func play()
}
class PetOwner: Person {
let pet: Animal
init(pet: Animal) {
self.pet = pet
}
func play() {
let name = pet.name ?? "someone"
print("I'm playing with \(name).")
}
}
제일 먼저 의존성 주입은 Class 외부에서 해야한다. class단의 외부는 App단이 될 것이다. 바로 App Delegate가 될 수 있다.
그 위치는 앱의 설정을 관리해주는 AppDelegate 에서 의존성 주입을 시켜주면 된다.
// AppDelegate.swift
import UIKit
import Swinject
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
let container = Container() // 컨테이너
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
이렇게 생성한 컨테이너가 의존성을 주입해주는 틀이 된다.
이걸 살짝 바꿔서 아래처럼 컨테이너를 초기화할 때, 등록도 할 수 있을 것이다.
let container: Container = {
let container = Container()
container.register(Animal.self) { _ in
Cat(name: "Mimi")
}
container.register(Person.self) { r in
PetOwner(pet: r.resolve(Animal.self)!)
return container
}()
- 의존성을 주입해주는 큰 틀인 컨테이너를 생성헀다. 그리고, 해당 컨테이너에 주입시켜줄 의존성의 서비스를 등록했다.
let person = container.resolve(Person.self)
person?.play()
- resolve를 이용해 사용할 서비스 인스턴스를 가져올 수 있다.
register 등록 resolve 사용!
만약 DI없이 사용한다면?
이렇게 Animal프로토콜을 사용하지 않고 Cat클래스를 직접 초기화 해서 가져온뒤 그대로 사용할 것이다. 하지만 Swinject를 사용하게 되면 해당 클래스의 함수가 바뀌지 않는 이상 바뀔 내용이 없어지게 된다.
또한 초기화도 ViewController 외부에서 하게 된다.
DI를 사용한다면?
이제 컨트롤러에서 가져다 써야한다.
컨테이너를 초기화할 때 의존성을 주입해주기 위해 ViewController도 추가해준다.
여기선 등록만 할 것이 아니다.
resolver를 통해 ViewController에서 사용하려는 person에 의존성을 주입해준다.
이제 window에 register한 뷰 컨트롤러를 등록해줘야 하는데, 두 가지의 경우로 나뉜다.
iOS13을 기점으로 SceneDelegate가 생기면서 방법이 나뉘었다. SceneDelegate의 유무에 따라 거의 흡사하지만 방법이 조금 다르다.
먼저 AppDelegate에 등록하는 방법이다.
iOS 13버전 전의 형태(SceneDelegate없이 개발할때)
SceneDelegate에 등록하는 방법(SceneDelegate를 사용할 때)
잘 되었는지 테스트해본다.
person을 잘 주입했다면
I'm playing with Mimi. 가 찍힐것이다.
여기서 아마 스토리보드를 사용중이였다면 화면이 표시되지 않을 것이다. 이런 경우엔 SwinjectStoryboard가 따로있고 이를 이용해야한다.
github.com/Swinject/SwinjectStoryboard
알아보다보니 궁금한 점이 생겼다.
+추가. 동일한 프로토콜을 여러곳에서 사용한다면(여러 구현체가 있다면) 어떻게 해야할까?
내부적으로 생성되는 Key인 registeration Key를 통해서 구분할 수 있다. 라이브러리에 구현된 코드는 이것이다.
@discardableResult
public func register<Service>(
_ serviceType: Service.Type,
name: String? = nil,
factory: @escaping (Resolver) -> Service
) -> ServiceEntry<Service> {
return _register(serviceType, factory: factory, name: name)
}
container에 등록할 때 내부적으로는 Registration Key가 생성되어 서로를 구분하게 된다. 이 때 키는 위에서 register할때의 인자 3가지를 통해서 구분하게된다.
type of the service(service Type)
name of registration
number and type of the arguments
이 덕분에 동일한 타입을 등록하더라도 다른 register함수와의 차이를 판단한고 서로 다른 구현체를 resolve할 수 있다. 2번에 대한 설명을 문서에 있는 코드와 함께 설명해보자면 문서의 코드에서 name: "cat" 이라는 인자를 전달했다.
container.register(Animal.self, name: "cat") { _ in Cat(name: "Mimi") }
이런 경우 registration key를 구분하는 3가지의 인자중 하나라도 다른 경우 nil을 리턴하게 되니 이를 조심해야 한다.
아래의 내용은 프로젝트를 진행하고 필요하다고 느껴질 경우 추가하겠습니다.
Object Scope
resolve를 통해 생성된 인스턴스의 lifecycle과 관련된 내용
Assembly
container 자체로 활용하는게 아닌 도메인별로 나뉘어 그룹화해 관리하는 내용
Circle Dependencies
순환참조를 해결하기 위한 내용
Swinject 프로젝트 예제
예제 1)
let container = Container()
container.register(FirstViewModel.self) { r in FirstViewModel() }
container.register(SecondViewModel.self) { r in SecondViewModel() }
container.register(FirstViewController.self) { r in
let controller = FirstViewController()
controller.viewModel = r.resolve(FirstViewModel.self)
return controller
}
container.register(SecondViewController.self) { r in
let controller = SecondViewController()
controller.viewModel = r.resolve(SecondViewModel.self)
return controller
}
self.window?.rootViewController = container.resolve(FirstViewController.self)
앱 델리게이트 파일 하나에서 하나의 컨테이너를 만들고 여러 의존성을 주입시키고 사용함으로 앱 전체의 의존성을 편하게 관리할 수 있다.
예제 2)
final class DIContainer {
static let shared = DIContainer()
let container = Container()
private init() {}
func inject() {
registerDataSources()
registerRepositories()
registerUseCases()
registerViewModels()
}
private func registerDataSources() {
container.register(AuthServiceProtocol.self) { _ in FBAuthService() }
container.register(RemoteUserDataSourceProtocol.self) { _ in RemoteUserDataSource() }
}
private func registerRepositories() {
container.register(TokenRepositoryProtocol.self) { resolver in
var repository = TokenRepository()
repository.keychainManager = resolver.resolve(KeychainManagerProtocol.self)
return repository
}
container.register(AuthRepositoryProtocol.self) { resolver in
var repository = AuthRepository()
repository.authService = resolver.resolve(AuthServiceProtocol.self)
repository.remoteUserDataSource = resolver.resolve(RemoteUserDataSourceProtocol.self)
repository.chatRoomDataSource = resolver.resolve(ChatRoomDataSourceProtocol.self)
repository.pushNotificationService = resolver.resolve(PushNotificationServiceProtocol.self)
return repository
}
container.register(ChatRepositoryProtocol.self) { resolver in
var repository = ChatRepository()
repository.chatDataSource = resolver.resolve(ChatDataSourceProtocol.self)
repository.reportDataSource = resolver.resolve(ReportDataSourceProtocol.self)
repository.pushNotificationService = resolver.resolve(PushNotificationServiceProtocol.self)
return repository
}
}
private func registerUseCases() {
container.register(AutoLoginUseCaseProtocol.self) { resolver in
var useCase = AutoLoginUseCase()
useCase.tokenRepository = resolver.resolve(TokenRepositoryProtocol.self)
return useCase
}
container.register(ChatUseCaseProtocol.self) { resolver in
var useCase = ChatUseCase()
useCase.userRepository = resolver.resolve(UserRepositoryProtocol.self)
useCase.chatRepository = resolver.resolve(ChatRepositoryProtocol.self)
return useCase
}
container.register(ChatRoomListUseCaseProtocol.self) { resolver in
var useCase = ChatRoomListUseCase()
useCase.userRepository = resolver.resolve(UserRepositoryProtocol.self)
useCase.chatRoomRepository = resolver.resolve(ChatRoomRepositoryProtocol.self)
return useCase
}
}
private func registerViewModels() {
container.register(ChatViewModel.self) { resolver in
let viewModel = ChatViewModel()
viewModel.chatUseCase = resolver.resolve(ChatUseCaseProtocol.self)
viewModel.leaveStudyUseCase = resolver.resolve(LeaveStudyUseCaseProtocol.self)
viewModel.subscribePushNotificationUseCase = resolver.resolve(SubscribePushNotificationUseCaseProtocol.self)
viewModel.unsubscribePushNotificationUseCase = resolver.resolve(UnsubscribePushNotificationUseCaseProtocol.self)
return viewModel
}
container.register(ChatListViewModel.self) { resolver in
let viewModel = ChatListViewModel()
viewModel.chatRoomListUseCase = resolver.resolve(ChatRoomListUseCaseProtocol.self)
return viewModel
}
container.register(SetEmailViewModel.self) { _ in
let viewModel = SetEmailViewModel()
return viewModel
}
}
}