Dependency Injection
Dependency Injection
구현 목표
- 싱글턴 클래스 → 안티 패턴으로도 불림
(1). 싱글턴 클래스는 전역으로 선언되기 때문
(2). 이니셜라이즈 당시 특정 변수를 커스텀 불가능
(3). 특정 서비스를 쉽게 변경 불가능 → 싱글턴 클래스에 의존하고 있기 때문
- 의존성 주입을 통해 위 문제점 해결 가능
구현 태스크
- 데이터 서비스를 담당하는 싱글턴 클래스 사용
- 싱글턴 클래스를 의존성 주입을 사용하는 코드화
- 실제 서버에 리퀘스트하는 데이터 서비스와 가데이터를 사용하는 데이터 서비스를 모두 사용하도록 프로토콜화
- 가데이터의 디폴트 값 및 이니셜라이저 가능 → 확장성 늘리기
핵심 코드
protocol DataServiceProtocol {
func getData() -> AnyPublisher<[PostModel], Error>?
}
...
init(dataService: DataServiceProtocol) {
self.dataService = dataService
loadPost()
}
...
init(dataService: DataServiceProtocol) {
_viewModel = StateObject(wrappedValue: DependencyInjectionViewModel(dataService: dataService))
}
소스 코드
import SwiftUI
import Combine
struct PostModel: Identifiable, Codable {
let userId: Int
let id: Int
let title: String
let body: String
}
class ProductionDataServiceSingleton {
static let instance = ProductionDataServiceSingleton()
private init() {}
let urlString = "https://jsonplaceholder.typicode.com/posts"
func getData() -> AnyPublisher<[PostModel], Error>? {
guard let url = getUrl() else { return nil }
return URLSession.shared.dataTaskPublisher(for: url)
.map{$0.data}
.decode(type: [PostModel].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
private func getUrl() -> URL? {
guard let url = URL(string: urlString) else { return nil }
return url
}
}
- URL을 통해 받아올 데이터 모델 및 데이터 모델을 비동기적으로 받아오은 서비스 클래스
- 싱글턴 클래스 → 데이터 패치를 위해 해당 클래스에 의존적으로 되는 문제점 발생
- 특정 값을 커스텀 불가능(URL 주소 등)
protocol DataServiceProtocol {
func getData() -> AnyPublisher<[PostModel], Error>?
}
class ProductionDataService: DataServiceProtocol {
let urlString: String
init(urlString: String = "https://jsonplaceholder.typicode.com/posts") {
self.urlString = urlString
}
func getData() -> AnyPublisher<[PostModel], Error>? {
guard let url = getUrl() else { return nil }
return URLSession.shared.dataTaskPublisher(for: url)
.map{$0.data}
.decode(type: [PostModel].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
private func getUrl() -> URL? {
guard let url = URL(string: urlString) else { return nil }
return url
}
}
class MockDataService: DataServiceProtocol {
let testData: [PostModel]
init(data: [PostModel]?) {
self.testData = data ??
[PostModel(userId: 1, id: 1, title: "title1", body: "body1"), PostModel(userId: 2, id: 2, title: "title2", body: "body2")]
}
func getData() -> AnyPublisher<[PostModel], Error>? {
Just(testData)
.tryMap{$0}
.eraseToAnyPublisher()
}
}
ProductionDataService
는 일반적인 클래스와 같이 초기화 가능한 클래스로 선언. url
을 초깃값으로 줄 수 있기 때문에 시점에 따라 서로 다른 조회 가능(확장성 확보)
MockDataService
또한 ProductionDataService
와 마찬가지로 실제 서버 데이터가 아니라 개발 단의 임시 데이터를 테스트해보는 용도로 구현하는 서비스 담당 클래스
MockDataService
의 임시 데이터 초기화 시 널 값을 통해 디폴트 값 사용 또는 의존성 주입 시 사용하고자 하는 데이터 입력 가능
DataServiceProtocol
프로토콜을 기준으로 준수: getData
함수가 필요조건
class DependencyInjectionViewModel: ObservableObject {
@Published var dataArray = [PostModel]()
var cancellables = Set<AnyCancellable>()
let dataService: DataServiceProtocol
init(dataService: DataServiceProtocol) {
self.dataService = dataService
loadPost()
}
private func loadPost() {
guard let data = dataService.getData() else { return }
data
.sink { completion in
switch completion {
case .finished: print("SUCCESS")
case .failure(let error): print(error.localizedDescription)
}
} receiveValue: { [weak self] returnedPosts in
guard let self = self else { return }
self.dataArray = returnedPosts
}
.store(in: &cancellables)
}
}
- 어떤 데이터 서비스를 사용할지 이니셜라이즈 단에서 주입받는
ObservableObject
struct DependencyInjectionBootCamp: View {
@StateObject private var viewModel: DependencyInjectionViewModel
init(dataService: DataServiceProtocol) {
_viewModel = StateObject(wrappedValue: DependencyInjectionViewModel(dataService: dataService))
}
var body: some View {
ScrollView {
VStack {
ForEach(viewModel.dataArray) { data in
Text(data.title)
.font(.headline)
.fontWeight(.semibold)
}
}
.padding()
}
}
}
DependencyIngectionViewModel
이라는 ObservableObject
를 관찰할 대 어떤 데이터 베이스를 사용할 것인지 현재 UI 뷰 또한 '주입'받은 데이터 서비스 클래스를 그대로 전달
import SwiftUI
@main
struct SwiftfulThinkingAdvancedLearningApp: App {
var body: some Scene {
WindowGroup {
DependencyInjectionBootCamp(dataService: ProductionDataService())
}
}
}
- 해당 뷰를 선언하는 가장 초기 단계
- 어떤 데이터 서비스를 사용할지 현재 단에서 결정 → 종류 바로 반영 가능
ProductionDataService
또는 MockDataService
모두 주입 가능하기 때문에 테스트 가능한 매우 확장적인 코드
구현 화면