티스토리 뷰

728x90
반응형

곰튀김님의 Inversion 세션 (let us go summer 2020 => 2:18:19 쯤 나와요! 👍) 을 보다가

Dependency Container를 공부해보고자합니다.

 

Dependency Injection의 개념 & SOLID의 D인 의존관계 역전 원칙(DIP)을 어떻게 따르게 해줄 수 있을지는

위의 세션 또는 이 글 을 참고해주세요 

 

이 포스팅은

Dependency Container 또는

Dependecny Inject Container 또는

IOC Container (Inversion Of Control Container) 에 대해서만

살펴보겠습니다. 

 

[1] Dependency Container란 무엇인가?! 

 

의존성 주입을 해줄때는 밖에서 인스턴스를 만들어서 주입해줍니다. (참고:  의존성 주입(Dependency Injection) 을 해주는 세가지 방법 )

밖에서 인스턴스를 만들어서 주입해주는 곳은 앱에서 여러군데 입니다. 즉 인스턴스를 만드는 위치가 분산되어있습니다. 

 

근데 Container라는 것이 있고 이 친구가 모든 인스턴스를 다 가지고 있고 다 관리를 한다고 생각해봅시다.

 

Container에 앞으로 내가 사용할 모든 인스턴스를 다 만들어서 등록해두고 => register 라는 용어를 씀

필요한 시점에 Container에게 특정타입의 인스턴스를 달라고 하면  Container가 꺼내주는 => resolve 라는 용어를 씀 

것입니다. 

 

 

예를들어 아래 코드 같은 모양이라고 할 수 있겠습니다.

 

 

/// Copyright (c) 2018 Razeware LLC
/// 출처: https://www.raywenderlich.com/17-swinject-tutorial-for-ios-getting-started
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")
}
}

 

 

 

이니셜라이저를 통해서 인스턴스를 초기화하는 코드가 

Container에 register해줄 때 한번만 쓰이게 되는 것을 볼 수 있습니다.

 

이제 사용하는 쪽에서는 이니셜라이저가 아니라 resolve()를 통해 인스턴스를 만들게 되는 것이죠

 

 

https://www.youtube.com/watch?v=h2FBZcLBeq0

 


[2] Dependency Container는 왜 필요한가?!

 

주입해줘야하는 게 많아서 이니셜라이저의 파라미터가 엄청 많은데

아래와 같이 중복코드가 있을때 유용합니다. 

 

 

https://medium.com/flawless-app-stories/ios-dependency-injection-using-swinject-9c4ceff99e41

 

 

 

저 코드는 Container를 쓰면 이렇게 간단히 변경가능하기 때문입니다. 

class ClientA {
    func someMethod() {
        let dataFetcher = Container.shared.resolve(DataFetcher.self)
    }
}

class ClientB {
    func someMethod() {
        let dataFetcher = Container.shared.resolve(DataFetcher.self)
    }
}


위의 예제는 Swinject(Dependency Container 관련 라이브러리)를 소개하는 블로그에서 가져왔습니다. 

resolve(DataFetcher.self) 를 할때마다 새로운 DataFetcher 인스턴스를 리턴한다고 합니다. 찍어보면 주소값이 다른 것을 확인할 수 있어요

 

 

 

https://medium.com/flawless-app-stories/ios-dependency-injection-using-swinject-9c4ceff99e41

 

 

만약 DataFetcher를 하나의 인스턴스로 쓰고 싶다면 Object Scope로 설정해줄 수있다고 하네요

 

(다른 라이브러리들은 어떤 지 확인해봐야겠네요-!)

 

[3] Dependency Container의 내부구현은 대충 어떻게 될까?!

 

많은 것을 고려안하고 완전완전 슈퍼심플한 Container를 만들어본다면 이렇게 되겠네요

 

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)
}
}

 

 

그리고 Swift 5.1에서 추가된 Property Wrapper를 만들어주면

 

@propertyWrapper
class Dependency<T> {
let wrappedValue: T
init() {
self.wrappedValue = DIContainer.shared.resolve()
}
}

 

더 간결해집니다 

 

class ViewController: UIViewController {
@Dependency var foodTruck: FoodTruck
override func viewDidLoad() {
super.viewDidLoad()
print(foodTruck)
}
}

 

 

 

 

 

 

Reference

www.youtube.com/watch?v=h2FBZcLBeq0

 

medium.com/flawless-app-stories/ios-dependency-injection-using-swinject-9c4ceff99e41

 

iOS Dependency Injection Using Swinject

In this article, you’ll find everything you should know about iOS Dependency Injection with Swinject.

medium.com

 

https://www.raywenderlich.com/17-swinject-tutorial-for-ios-getting-started

 

Swinject Tutorial for iOS: Getting Started

In this tutorial, you will explore Dependency Injection (DI) through Swinject, a Dependency Injection framework written in Swift. Dependency Injection is an approach to organizing code so that its dependencies are provided by a different object, instead of

www.raywenderlich.com

 

반응형
댓글

eungding님의
글이 좋았다면 응원을 보내주세요!