IT/기초 지식

의존성 주입 (DI: Dependency Injection)

개발자 두더지 2026. 3. 9. 21:34
728x90

일본의 한 블로그 글을 번역한 포스트입니다. 오역 및 의역, 직역이 있을 수 있으며 틀린 내용은 지적해주시면 감사하겠습니다.

 

 

DI의 탄생 경위


 참고로 코드는 Kotlin으로 작성하도록 하겠다.

 제일 먼저 DI를 하지 않고 어느 오브젝트 내에서 외부 오브젝트를 인스턴스화하는 경우를 살펴보도록 하겠다.

class Apple(val hasPoison: Boolean = false)

// 식품 클래스. 추상적인 개념이어야하지만 사과(Apple)에 의존해버리고 만다.
class Food {
    fun eat() {
        val apple = Apple(hasPoison = false)
        if (apple.hasPoison) {
            println("게임 오버")
        } else {
            println("사과 맛있다!")
        }
    }
}

class Human {
    // 이 메소드에서 eat()메소드를 호출. 테스크 코드에 해당하는 부분이기도 하다.
    fun doSomething() {
        val food = Food()
        food.eat()
    }
}

doSomething() 실행 결과는 항상 동일하다. 위 코드는 Food클래스의 eat()메소드로, 외부의 Apple클래스를 인스턴스화하고 있다. 즉,  Food클래스의 eat()메소드는 Apple클래스에 의존하고 있다. 하지만 이것은 문제이다. 왜냐하면 eat()메소드의 처리 내용을 직접 편집하거나 Apple클래스를 편집하지 않는 한 eat() 메소드의 처리 결과는 변화하지 않기 때문이다. 

식품(Food)은 사과 이외에 바나나나 치즈도 있을 것이며, 한층 더 사과 안에도 독들이와 독 없음 2종류 있을 것인데, 식품(Food)를 먹을 때의 결과는 항상 같고, 유닛 테스트의 작성도 어렵다. 예를 들어, eat() 메소드내에서 if문에 문제가 없는지 등을 확인하기 위해서는 , Apple 클래스를 다시 작성할 수 밖에 없다. 그로인해 테스트 코드(doSomething() 메소드)만으로 테스트하는 것은 불가능하게 되어 버린다. 

 이러한 문제를 해결하기 위해  eat() 메소드의 Apple 클래스에 대한 의존성 을 일단 약하게 하고 나중에 주입하자라는 생각으로 이어지게 되었고, 이러한 경위로 의존성 주입이 탄생하게 된다.

 즉 하나의 객체 A가 객체 B에 의존하는 경우, 즉 객체 A가 다른 객체 B를 가지는 것은 사용할 때의 유연성이나 테스트의 어려움의 관점에서 볼 때 좋지 않으므로, 먼저 오브젝트 A가 오브젝트 B를 가지지 않도록 하고 외부에서 오브젝트 A에 오브젝트 B를 주입해 주는 것이 DI의 아이디어이다.

 DI의 간단한 예에 대해서 살펴보자.

// Food클래스・eat()메소드의 Apple에 대한 의존성을 없애고, Food클래스・eat()메소드를 추상화
interface Food {
    fun eat()
}

// Food 인터페이스를 비준하여, eat()메소드를 Apple클래스용으로 구현한다.
class Apple(val hasPoison: Boolean) : Food {
    override fun eat() {
        if (hasPoison) {
            println("게임 오버")
        } else {
            println("사과 맛있다!")
        }
    }
}

class Human {
    // 이 메소드로 부터 eat()메소드를 호출. 테스트 코드에 해당하는 부분이기도 하다.
    fun doSomething() {
        val apple = Apple(hasPoison = false)
        val poisonApple = Apple(hasPoison = true)
        apple.eat()
        poisonApple.eat()
    }
}

doSomething() 실행 결과는 다음과 같이 된다.

사과 맛있다! // apple.eat()의 실행결과
게임오버       // poisonApple.eat()의 실행결과

덧붙여서, 식품(Food) 인터페이스는 사과(Apple)에 의존하지 않는 추상적인 개념이므로, 다음과 같이 바나나(Banana)도 만들 수 있다.

class Banana : Food {
    override fun eat() {
        println("바나나를 먹으면 힘이나")
    }
}

Banana().eat() 실행 결과는 다음과 같다.

바나나를 먹으면 힘이나

 

 

DI의 종류


  • Interface Injection : 바로 앞에서 살펴본 예가 여기에 해당한다.
    이 방법에서는 인터페이스, 즉 기초가 되는 메소드군을 정의해, 작성하는 클래스에 그 인터페이스를 비준시키는 것으로 DI를 구현한다.
    Java / Kotlin에서는 인터페이스, Objective-C / Swift는 프로토콜이 인터페이스에 해당한다.
  • Constructor Injection
    어떤 클래스 A의 생성자로서 그 변수 hoge 를 인스턴스화하는 것으로, 클래스 A에 Hoge 클래스의 「의존성」을 「주입」한다
  • Setter Injection
    한 클래스 내의 프로퍼티에 다른 클래스의 인스턴스가 되는 것을 선언만 해 두어(var hoge: Hoge?)
    외부로부터 액세스 가능한 setter 메소드 경유로 hoge 를 인스턴스화하는 방법으로, Hoge 클래스의 「의존성」을 「주입」한다

 

 

"의존성"의 의미


 의존성이라는 단어로 인해 이해하기 어려운 느낌이므로 다음과 같이 생각해보았다.

 의존성 주입에서의 "의존성 = 객체 ". 영어판 Wikipedia에는 의존성을 객체라고 정의하고 있다. 즉, "의존성=인스턴스",  "의존성=Java / Kotlin 등이라면 interface에서 정의 된 메소드의 처리 내용을 실제로 기술하고있는 메소드", "의존성 =Objective-C/Swift에서 말하는 것을 protocol비준하고 있는 클래스내에서 처리 내용을 실제로 기술하고 있는 델리게이트 메소드"가 된다.

 다음엔 주입이라는 단어를 덧붙어 의존성 주입이라는 단어를 바꿔보도록하겠다.

 

 

"의존성 주입"이란


(오브젝트 A의 외부로부터) 「오브젝트 A에서 필요한, 오브젝트 B의 객체(인스턴스나 구체적인 메소드)」를 「주입/대입/설정/제공」하는 것이다

라고 말을 더해 설명할 수 있겠다. 덧붙여 여기서 객체는 클래스나 메소드에 해당한다. 하지만 이것만으로는, 아직 뭐야라고 생각할지도 모른다. 앞에서 설명한 3가지 DI 방법을 의존성 정의에 따라 다음과 같이 따라 바꿔 보았다.

  • Interface Injection
    메소드 A가 정의된 인터페이스를 비준한 클래스 B로, 메소드 A의 구체적인 처리를 구현하는 것이다
  • Constructor Injection
    클래스 A의 생성자로 클래스 B를 설정하고 클래스 A 생성시 클래스 B를 구현하는 것이다.
  • Setter Injection
    먼저 클래스 A의 속성으로 클래스 B를 설정하고 클래스 B의 setter 메서드도 정의한다.
    그리고 클래스 A 생성 후 클래스 B의 setter 메서드를 호출하여 클래스 B를 구현하는 것이다.
interface Food {
    fun eat()  // <- 메소드A
}

// Apple <- 클래스B
class Apple(val hasPoison: Boolean) : Food {
    // 여기서「메소드A의 구체적인 처리를 구현」
    override fun eat() {
        if (hasPoison) {
            println("게임 오버")
        } else {
            println("사과 맛있다!")
        }
    }
}


class Human {
    // 이 메소드에서 eat()메소드를 호출. 테스트 코드에 해당하는 부분도 있다.
    fun doSomething() {
        val apple = Apple(hasPoison = false)
        val poisonApple = Apple(hasPoison = true)
        apple.eat()
        poisonApple.eat()
    }
}

 

 

DI의 장점


 마지막으로 지금까지 중간 중간에 설명했던 DI의 장점을 모두 정리하도록 하겠다.

  • 결합 정도의 저하에 의한 컴포넌트화 촉진
  • 단위 테스트 효율성
  • 특정 프레임워크에 대한 의존도 감소

참고자료

https://qiita.com/iTakahiro/items/353a11f6c9d2a927158d

728x90