Swift/개념 & 응용

[Swift] Dependency Container(feat. Property Wrapper)

유정주 2023. 4. 24. 08:48
반응형

* 틀린 내용이 있을 수 있습니다. 댓글로 알려주시면 매우 감사하겠습니다.

 

Dependency Container

지난 포스팅에서 DI에 대해 배웠습니다.

DI를 하나의 Container로 관리하는 방법이 Dependency Container(DI Container, IoC Container)입니다.

기존 DI는 인스턴스 생성 위치가 분산되었지만, Container를 사용하면서 한 곳에 모아진다는 장점이 있습니다.

동일한 생성자 코드 중복을 줄일 수 있다는 장점도 있습니다.

 

이번 포스팅에서 (매우) 기본적인 Container를 직접 만들어보고,

다른 포스팅에서 Swinject 라이브러리를 소개하겠습니다.

 

Dependency Container 구현

Dependency Container(이하 Container)는 싱글턴으로 구현해야 합니다.

하나의 객체에서 DI 할 인스턴스를 관리해야 하기 때문입니다.

만약 Container를 여러 개 생성할 수 있다면 DI를 한 곳에서 관리하는 의미가 사라질 것입니다.

그래서 싱글턴을 이용해 하나의 인스턴스만 생성하도록 구현합니다.

 

Container는 register와 resolve 두 가지 동작을 합니다.

register는 Container에 앞으로 사용할 모든 인스턴스를 등록하는 동작이고,

resolve는 Container에게 특정 타입의 인스턴스를 요구하면 Container가 반환하는 동작입니다.

 

final class DependencyContainer {
    private static var shared = DependencyContainer()
    
    private init() { }
    
    private var dependencies: [String: Any] = [:]

    static func register<T>(_ dependency: T) {
        shared.register(dependency)
    }

    static func resolve<T>() -> T {
        shared.resolve()
    }

    private func register<T>(_ dependency: T) {
        let key = String(describing: T.self)
        dependencies[key] = dependency as Any
    }

    private func resolve<T>() -> T {
        let key = String(describing: T.self)
        let dependency = dependencies[key] as? T

        precondition(dependency != nil, "\(key) Dependency가 없음")

        return dependency!
    }
}

Dependency들은 딕셔너리로 관리합니다.

모든 타입을 받아야 하므로 Value 타입을 Any로 설정했습니다.

 

register는 인스턴스를 파라미터로 받고 딕셔너리에 저장합니다.

resolve는 메서드를 호출한 인스턴스 이름을 키로 사용해 register로 저장한 인스턴스를 반환합니다.

 

사용 예시

Dependency Container의 사용법을 알아보겠습니다.

protocol Eattable {
    func eat()
}

struct Chicken: Eattable {
    func eat() {
        print("치킨를 냠냠")
    }
}

protocol Drinkable {
    func drink()
}

struct Beer: Drinkable {
    func drink() {
        print("맥주를 꼴깍꼴깍")
    }
}

struct Restaurant {
    var food: Eattable
    var drink: Drinkable
    
    init(food: Eattable, drink: Drinkable) {
        self.food = food
        self.drink = drink
    }
    
    func sell() {
        food.eat()
        drink.drink()
    }
}

사용할 모델은 위와 같습니다.

Restaurant는 Eattable과 Drinkable 프로퍼티를 가지고 있어서 음식과 음료를 설정할 수 있습니다.

 

let chicken = Chicken()
let beer = Beer()

let chickenRestaurant = Restaurant(food: chicken, drink: beer)
chickenRestaurant.sell()

치킨집을 만든다고 하면 위 코드가 매번 중복됩니다.

대한민국 치킨집 수를 생각하면 엄청난 중복 코드가 생길 것이고 수정사항이 있다면 이 모든 코드를 수정해야 합니다.

 

Dependency Container를 이용해 중복을 줄이고, DI 코드를 한 곳에서 관리하여 개선할 수 있습니다.

DependencyContainer.register(Chicken())
DependencyContainer.register(Beer())

let chicken: Chicken = DependencyContainer.resolve()
let beer: Beer = DependencyContainer.resolve()

DependencyContainer.register(Restaurant(food: chicken, drink: beer))

Chicken과 Beer를 register 메서드로 등록하고, Restaurant를 생성합니다.

그후 Restaurant도 Dependency를 등록합니다.

 

이제 치킨집 생성은 아래 단 한 줄로 가능합니다.

let chickenRestaurant: Restaurant = DependencyContainer.resolve()
chickenRestaurant.sell()
//치킨을 냠냠
//맥주를 꼴깍꼴깍

 

주입된 타입까지 확인한다면 치킨집 뿐만 아니라 여러 식당을 간편하게 생성할 수 있습니다.

 

Property Wrapper로 개선

Property Wrapper를 이용해 위 코드에서 더 줄일 수 있습니다.

(Property Wrapper 포스팅은 여기에서 볼 수 있습니다.)

 

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

Property Wrapper를 등록한 인스턴스에 자동으로 resolve로 대입합니다.

 

class ViewController {
    @Dependency var chickenRestaurant: Restaurant
    
    override func viewDidLoad() {
        chickenRestaurant.sell()
    }
}

//치킨을 냠냠
//맥주를 꼴깍꼴깍

 

개발자가 직접 객체를 생성하지 않아도 Property Wrapper에 의해 자동으로 대입되었습니다.

안전성과 간편성 모두 개선된 모습입니다.

 

감사합니다.

 

참고

https://medium.com/swift2go/dependency-injection-with-property-wrappers-1a8a07a4124c

https://eunjin3786.tistory.com/233?category=837198

https://siempay.medium.com/dependency-injection-in-ios-like-a-pro-5e6f923389d7


아직은 초보 개발자입니다.

더 효율적인 코드 훈수 환영합니다!

공감 댓글 부탁드립니다.

 

 

반응형