지금까지 포스팅을 통해서 protocol을 활용해서 Service 객체와 DB 객체를 만들어두었습니다. 이제 해당 객체를 필요한 곳에 주입하는 Dependency Injection을 구현해야 합니다. 어떻게 구현했는지 한번 보겠습니다.
우리가 구현한 Service 객체는 총 3종류입니다. 모든 객체를 하나하나 init에 전달하려면 코드가 너무 길어질 것 같습니다. 따라서 3개의 Service 객체를 묶어주는 Dependency 객체를 만들도록 하겠습니다.
이 객체의 init 안에서 선언된 db (= FirebaseDB 객체)는 한번 init되어서 각각의 Service 객체가 init될 때 참조가 전달되어 사용됩니다. 각각의 Service 클래스가 캡쳐하고 있으므로 앱이 꺼질 때까지 딱 1개의 객체만 메모리에 유지됩니다. 결국 싱글톤과 같은 장점을 가지는 것이죠. 하지만 싱글톤만으로는 불가능한 독립적인 테스트를 가능하도록 해줍니다.
import Foundation
protocol Dependency {
var wordBookService: WordBookService { get }
var wordService: WordService { get }
var sampleService: SampleService { get }
}
class DependencyImpl: Dependency {
let wordBookService: WordBookService
let wordService: WordService
let sampleService: SampleService
init() {
let db = FirestoreDB()
let ic = ImageCompressorImpl()
let iu = FirebaseIU(imageCompressor: ic)
self.wordService = WordServiceImpl(database: db, imageUploader: iu)
self.wordBookService = WordBookServiceImpl(database: db, wordService: wordService)
self.sampleService = SampleServiceImpl(database: db)
}
}
우리 앱의 최상위 객체인 App 객체 내에서 위에 정의한 Dependency 객체를 init하고 하위 객체에 전달해줍니다. 이 객체의 전달은 중간에 끊기지 않고 계속해서 필요한 곳까지 전달, 전달 해주면 됩니다.
물론 EnvironmentObject를 사용하면 최상위에 한번만 코드를 써주면 되는 장점이 있습니다만 EnvironmentObject는 기본적으로 ObservableObject입니다. 즉 State가 변했을 때 View의 변화가 일어날 필요가 있을 때 쓰는 객체입니다. 하지만 우리가 만든 Dependency는 View를 변화시키는 객체가 아니므로 EnvionmentObject로 전달하지 않았습니다.
@main
struct JWordsApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
private let dependency: Dependency = DependencyImpl()
var body: some Scene {
WindowGroup {
NavigationView {
ContentView(dependency)
}
}
}
}
최종적으로 Service를 사용하는 ViewModel 안에 Service 객체의 참조를 가지고 있을 변수를 선언합니다. 물론 init을 구현할 때 해당 참조를 외부에서 주입할 수 있도록 만들어야 합니다.
final class ViewModel: ObservableObject {
@Published var bookName: String = ""
private let wordBookService: WordBookService
init(wordBookService: WordBookService) {
self.wordBookService = wordBookService
}
}
이제 View에서는 상위 View에서 전달 받은 Dependency 객체를 활용해서 ViewModel을 init하면 됩니다
struct MacAddBookView: View {
@ObservedObject private var viewModel: ViewModel
init(_ dependency: Dependency) {
self.viewModel = ViewModel(wordBookService: dependency.wordBookService)
}
var body: some View {
VStack {
TextField("단어장 이름", text: $viewModel.bookName)
.padding()
Button {
viewModel.saveBook()
} label: {
Text("저장")
}
.disabled(viewModel.isSaveButtonUnable)
}
}
}