Dependency Injection 구현하기

SteadySlower·2022년 9월 18일
0

지금까지 포스팅을 통해서 protocol을 활용해서 Service 객체와 DB 객체를 만들어두었습니다. 이제 해당 객체를 필요한 곳에 주입하는 Dependency Injection을 구현해야 합니다. 어떻게 구현했는지 한번 보겠습니다.

Dependency 객체 만들기

우리가 구현한 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)
    }
    
}

최상위에서 init해서 하위로 전달하기

우리 앱의 최상위 객체인 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)
            }
        }
    }
}

ViewModel안에 Service property 만들기

최종적으로 Service를 사용하는 ViewModel 안에 Service 객체의 참조를 가지고 있을 변수를 선언합니다. 물론 init을 구현할 때 해당 참조를 외부에서 주입할 수 있도록 만들어야 합니다.

final class ViewModel: ObservableObject {
    @Published var bookName: String = ""
    private let wordBookService: WordBookService
    
    init(wordBookService: WordBookService) {
        self.wordBookService = wordBookService
    }
}

View에서 ViewModel init할 때 DI 구현하기

이제 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)
        }
    }
}
profile
백과사전 보다 항해일지(혹은 표류일지)를 지향합니다.

0개의 댓글