Dependency와 injection & SOLID의 의존관계 역전 원칙(DIP)의 사전지식이 필요하다.
https://clamp-coding.tistory.com/447
Dependency Container란 무엇인가.
의존성 주입을 해줄 때는 외부에서 인스턴스를 만들어서 주입해준다. 외부에서 인스턴스를 만들어서 주입해주는 곳은 여러곳일 수 있다.
즉 인스턴스를 만드는 위치가 분산되어있다. 근데 Container라는 것이 있고 Container가 모든 인스턴스를 다 가지고 있고, 관리한다고 생각해보자..
Container에 앞으로 내가 사용할 모든 인스턴스를 다 만들어서 등록해두고 => register
필요한 시점에 Container에게 특정 타입의 인스턴스를 달라고하면 Container가 꺼내주는 => resolve
것이다!
아래같은 코드의 모양이라고 할 수 있다.
import XCTest
import Swinject
@testable import Bitcoin_Adventurer
class BasicTests: XCTestCase {
private let container = Container()
// MARK: - Boilerplate methods
override func setUp() {
super.setUp()
// 1
container.register(Currency.self) { _ in .USD }
container.register(CryptoCurrency.self) { _ in .BTC }
// 2
container.register(Price.self) { resolver in
let crypto = resolver.resolve(CryptoCurrency.self)!
let currency = resolver.resolve(Currency.self)!
return Price(base: crypto, amount: "999456", currency: currency)
}
// 3
container.register(PriceResponse.self) { resolver in
let price = resolver.resolve(Price.self)!
return PriceResponse(data: price, warnings: nil)
}
}
override func tearDown() {
super.tearDown()
container.removeAll()
}
// MARK: - Tests
func testPriceResponseData() {
let response = container.resolve(PriceResponse.self)!
XCTAssertEqual(response.data.amount, "999456")
}
}
언제나 느끼는 거지만 처음 보는 코드는 막막합니다..
쨋든! 필요한 이유를 알아보자면
Dependency Container가 필요한 이유
DIContainer를 사용할 경우를 이미지로 그려보면 아래와 같을 수 있다.
주입해줘야하는게 많아서 이니셜라이저의 파라미터가 엄청 많은데 아래와같은 중복 코드가 있을 때 유용하다.
이처럼 주입해줘야 하는 코드가 많고, 중복코드가 많을 때 유용하다.
class ClientA {
func someMethod() {
let dataFetcher = Container.shared.resolve(DataFetcher.self)
}
}
class ClientB {
func someMethod() {
let dataFetcher = Container.shared.resolve(DataFetcher.self)
}
}
resolve(DataFetcher.self) 할 때 마다 새로운 DataFetcher 인스턴스를 리턴하고있다.
리턴하는 인스턴스 객체들의 주소는 다르다.
만약 리턴하는 DataFetcher를 하나의 인스턴스(객체)로 하고싶다면 Object Scope로 설정할 수 있다. 추후에 다루기로 한다.
Dependency Container의 내부 구현은 ??
정말 간단한 DIContainer를 만들어본다면 이렇게 될 것이다.
class DIContainer {
static let shared = DIContainer()
private var dependencies = [String: Any]()
private init() {}
func register<T>(_ dependency: T) {
let key = String(describing: type(of: T.self))
dependencies[key] = dependency
}
func resolve<T>() -> T {
let key = String(describing: type(of: T.self))
let dependency = dependencies[key]
precondition(dependency != nil, "\(key)는 register되지 않았어어요. resolve 부르기전에 register 해주세요")
return dependency as! T
}
}
만약 모델이 이렇게 구성되어 있다면?
protocol Eatable {
var calorie: Int { get }
}
protocol CityPresentable {
var code: String { get }
var name: String { get }
}
struct Pizza: Eatable {
var calorie: Int {
return 300
}
}
struct Seoul: CityPresentable {
var code: String {
return "02"
}
var name: String {
return "서울"
}
}
struct FoodTruck {
let food: Eatable
let city: CityPresentable
init(food: Eatable, city: CityPresentable) {
self.food = food
self.city = city
}
}
아래와같이 register, resolve 할 수 있을 것이다.
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
registerDependencies()
return true
}
private func registerDependencies() {
DIContainer.shared.register(Pizza())
DIContainer.shared.register(Seoul())
let pizza: Pizza = DIContainer.shared.resolve()
let seoul: Seoul = DIContainer.shared.resolve()
DIContainer.shared.register(FoodTruck(food: pizza,
city: seoul))
}
}
class ViewController: UIViewController {
let foodTruck: FoodTruck = DIContainer.shared.resolve()
override func viewDidLoad() {
super.viewDidLoad()
print(foodTruck)
}
}
다음에는 Swinject를 활용한 DIContainer를 공부해보려한다.
DIContainer의 모든 기능을 직접 구현하기엔 어렵고? Swift에서는 주로 Swinject를 사용한다고 알고있다.
Swinject는 DIContainer를 구현하고 사용할 수 있도록 도와주는 프레임워크라고 한다. Swinject는 의존성 주입을 위한 컨테이너를 제공하며, 클래스나 객체의 인스턴스를 생성하고 의존성을 주입하는 기능을 제공한다.
쉽게말하면 DIContainer는 기법, Swinject는 DIContainer를 활용하기 위한 라이브러리 정도로 이해가 될 것 같다.