[SwiftUI] Dependency Injection

Junyoung Park·2022년 8월 22일
0

SwiftUI

목록 보기
44/136
post-thumbnail
post-custom-banner

Dependency Injection

Dependency Injection

구현 목표

  • 싱글턴 클래스 → 안티 패턴으로도 불림
    (1). 싱글턴 클래스는 전역으로 선언되기 때문
    (2). 이니셜라이즈 당시 특정 변수를 커스텀 불가능
    (3). 특정 서비스를 쉽게 변경 불가능 → 싱글턴 클래스에 의존하고 있기 때문
  • 의존성 주입을 통해 위 문제점 해결 가능

구현 태스크

  1. 데이터 서비스를 담당하는 싱글턴 클래스 사용
  2. 싱글턴 클래스를 의존성 주입을 사용하는 코드화
  3. 실제 서버에 리퀘스트하는 데이터 서비스와 가데이터를 사용하는 데이터 서비스를 모두 사용하도록 프로토콜화
  4. 가데이터의 디폴트 값 및 이니셜라이저 가능 → 확장성 늘리기

핵심 코드

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 모두 주입 가능하기 때문에 테스트 가능한 매우 확장적인 코드

구현 화면

profile
JUST DO IT
post-custom-banner

0개의 댓글