boraBong

[iOS] Clean Architecture - 1. DI, 의존성 주입이란? 본문

iOS/Clean Architecture

[iOS] Clean Architecture - 1. DI, 의존성 주입이란?

보라봉_ 2022. 9. 5. 00:50
728x90

 

 

DI.. 의존성 주입.. 중요하다는 얘기를 많이 들어서인지, 글을 읽고 공부를 해봐도

어려운 말로 이해되고, 의존성 주입이라는 말 자체가 어렵게 느껴지기도 한다.

그러다 블로그들에서 소개되는 의존성 주입 관련 영상을 보게 되었는데,

Dependency injection is a 25-dollar term for 5-cent concept
의존성 주입은 5센트 개념에 대한 25달러짜리 용어이다.

=> 별거 아닌 개념인데 다들 너무 어렵게 받아들인다는 ,, 용기를 주는 문구

라고 의존성 주입을 소개한 점이 굉장히 인상적이었고 나에게도 이해할 수 있다는 용기를 주었다.

 

💡 DI?

DI = Dependency Injection (의존관계 주입)

DI는 의존성을 클래스에 주입시키는 것이고, 동시에 “의존성 분리"의 조건을 만족해야 한다.

DI를 공부하기 위해 아래의 개념들을 차례대로 정리해보자

  • 의존성
  • 주입
  • 의존성 주입
  • DIP 의존성 분리
  • DI의 장점

 

💡 Dependency, 의존성이란 무엇인가요?

“B가 A를 의존한다.” ❓ ⇒ 의존 대상 A가 변하면 그것이 B에 영향을 미친다.

class A {
    var name = "new jeans"
}

// A와 의존관계인 클래스 B
class B {
		// 내부 변수로 A 클래스의 인스턴스를 직접 생성하여 사용 -> 의존관계 성립
    var aClass = A()
}

let b = B()
print(b.aClass.name)

/// ✅ [출력] new jeans

즉, A의 기능이 추가되거나 변경되거나 형식이 바뀌게 된다면 그 영향이 B에게까지 미친다는 말이다.

→ 위의 예시에서 A의 name값이 변경된다면, b 객체의 출력값이 변경되기 때문에 A의 변경이나 수정이 B에게도 미친다고 볼 수 있다.

+) 의존성이 생기는 대표적인 3가지 경우

https://woozzang.tistory.com/137

의존성이 생기는 경우를 너무 잘 정리해두신 블로그가 있어 링크를 남겨 둡니다!

💡 Injection이란?

주입. 주입이란 “내부가 아니라 외부에서 객체를 생성하여 넣어주는 것”을 일컫는다.

class Barista {
    private let recipeNote: Recipe

    // 생성자로 Injection
    init(recipeNote: Recipe) {
        self.recipeNote = recipeNote
    }
}

// 외부에서 객체를 생성하여 **주입**
let borabong = Barista(recipeNote: Recipe())

그렇다면, 다시 한번

💡 의존성 주입이란?

위에서 살펴본 의존성과 주입을 합쳐 생각해본다면

의존성 + 주입 = 의존성 주입 = “의존 관계를 내부가 아니라 외부에서 성립시켜주는 것” 이라는 말이다.

즉, 인스턴스 생성의 책임을 객체가 스스로 하도록 하지 않고 initializer, property, method parameter의 형태로 넘겨주는 것이다.

 

 

✅ [ 의존성 주입의 3가지 방법 ]

1. Initializer Injection

  • Initializer 즉 생성자를 통해 외부에서 생성한 인스턴스를 주입시키는 방법이다.
class Barista {
    private let recipeNote: Recipe

    // 생성자로 Injection
    init(recipeNote: Recipe) {
        self.recipeNote = recipeNote
    }
}

이 방법을 사용하면 객체 생성 시 Initializer를 통해 의존관계(dependency)를 파악할 수 있고, 주입받는 인스턴스를 **불변(immutable)**하게 유지할 수 있다.

또한 인스턴스를 생성할 때 Initializer를 통해 인스턴스의 올바른 구성을 보장가능하다는 장점도 있다.

2. Property Injection

  • 프로퍼티에 직접 접근하여 인스턴스를 주입시키는 방법이다.
class Barista {
    var recipeNote: Recipe?
}

// 외부 코드
let borabong = Barista()

// 외부에서 객체를 생성하고, 객체의 프로퍼티에 직접 접근하여 인스턴스를 대입
borabong.recipeNote = Recipe()

굉장히 편리한 방법이고, 자주 사용하는 방법이지만 Initializer Injection과 비교했을 때 주입받는 인스턴스를 불변(immutable)하게 유지할 수 없다는 단점이 있다. 중간에 대체될 수 있음에 주의해야 한다!

3. Method Injection

  • 메서드의 매개변수로 인스턴스를 주입시키는 방법이다.
class Barista {
    func printRecipe(recipeNote: Recipe) {
        recipeNote.printRecipe()
    }
}
  • 이 경우 Barista 자체가 recipeNote에 대한 control을 갖고 있지 않다. 이 방법은 여러 use case에 따라 매개변수를 달리해서 유연성에 집중할 수 있다는 장점이 있다.

그런데.. 이렇게 의존성 주입을 했다고 해서 DI라고 부를 수는 없다.

DI는 의존성 분리의 조건을 만족해야 하기 때문이다. 그리고 의존성 분리는 💡의존성 역전 원칙💡을 기반으로 이루어진다.

 

💡 DIP(Dependency Inversion Principle), 의존성 역전 원칙

DIP는 객체지향 프로그래밍 설계의 다섯가지 기본 원칙(SOLID) 중 하나에 해당한다.

 

✅ [DIP 원칙이란]

  • 추상성이 높고 안정적인 고수준의 클래스는 구체적이고 불안정한 저수준의 클래스에 의존해서는 안된다는 원칙
    • 의존 관계가 존재하되, 다만 구체적인 클래스끼리 의존하지 말고 최대한 추상화된 클래스에 의존하라
    • => 일반적으로 객체지향의 인터페이스(interface)를 통해서 이 원칙을 준수할 수 있게 된다. (Swift에서는 Protocol)
  • (상대적으로 고수준인) 클라이언트는 저수준의 클래스에서 추상화한 인터페이스만을 바라보기 때문에, 이 인터페이스를 구현한 클래스는 클라이언트에 어떤 변경도 없이 얼마든지 나중에 교체될 수 있다.

⇒ 따라서 우리는 Swift의 추상화된 객체인 Protocol을 사용하여 의존성 분리를 시킬 수 있다.

  • 주입받는 속성의 타입을 프로토콜로 만들어주면 테스트가 용이해지고, 의도가 명확한 코드를 작성하는데 도움이 된다.

 

위의 Barista와 Recipe 예시를 Protocol을 활용하여 바꿔보면

protocol RecipeProtocol {
    var coffeeBean: String { get set }
    var coffeeType: [String] { get set }
}

final class CoffeeRecipe: RecipeProtocol {
    var coffeeBean: String = "아라비카"
    var coffeeType: [String] = ["아메리카노", "라떼", "카라멜 마끼야또", "바닐라빈 라떼"]
}

final class Barista {
    var recipe: RecipeProtocol
    
    init(recipe: RecipeProtocol) {
        self.recipe = recipe
    }
}

let baristaBorabong = Barista(recipe: CoffeeRecipe())
print(baristaBorabong.recipe.coffeeType) 
// ["아메리카노", "라떼", "카라멜 마끼야또", "바닐라빈 라떼"]

이렇게 바꿀 수 있다.

이렇게 프로토콜을 사용하여 의존성을 잘 분리해주면, 클래스의 제어 주체가 프로토콜에게 있게 된다.

 

만약 프로토콜의 coffeeBean 프로퍼티의 타입을 String에서 Int로 변경한다면

첨부한 사진과 같이 RecipeProtocol을 채택한 클래스들이 에러를 가지게 된다.

 

 

이렇게 프로토콜을 사용하여 의존성 분리를 해주게 되면 클래스들의 제어 주체가 프로토콜에게 있게 되고, 이 말인즉슨 해당 프로토콜을 준수하는 모든 클래스를 제어하고 분석하기 쉬워지게 된다는 것이다. 이렇게 의존성 분리를 잘 해주면 클래스에서 프로토콜로 의존의 방향이 역전되기 때문에 IoC(Inversion of Control), 제어 반전도 함께 이루어진다.

 

또한 이렇게 의존성을 잘 분리하면 얻을 수 있는 장점으로는 다음과 같다.

💡 DI의 장점

  1. 모듈간의 결합도를 낮춘다
  2. 객체간의 의존성을 줄이거나 없앨 수 있다
  3. 코드의 재활용성이 높아진다
  4. 클래스나 구조체의 책임이 더욱 명확해진다.
    • 어떤 객체 간에 의존성이 있는지 더욱 분명히 인지할 수 있기 때문
  5. 유지보수가 용이하다
  6. 테스트가 용이하다

 

https://www.youtube.com/watch?v=-n8allUvhw8

https://velog.io/@heyksw/Swift-DI-와-Swinject

https://sihyungyou.github.io/iOS-dependency-injection/

https://medium.com/@jang.wangsu/di-dependency-injection-이란-1b12fdefec4f

반응형
Comments