Context
DI에 대한 이야기를 하기 전, 의존성 그 자체에 대한 이야기를 먼저 해보자.
의존성은 무엇이고, 언제 발생하는가?
- 바리스타가 커피를 만들기 위해선 커피 머신의 협력이 필요하다. 즉 바리스타의 '커피 만들기' 라는 행위는 커피 머신에 의존적이다.
- 어떠한 애플리케이션은 작업을 처리하기 앞서 설정 값을 참조한다. 이 때 애플리케이션은 설정 값에 의존적이다.
두 번째 예시를 코드로 옮겨보면 다음과 같다. Calculator는 CalculatorConfig의 값에 따라 동작 여부를 결정한다.
data class CalculatorConfig(
val name: String,
val status: Boolean = true
)
class Calculator {
fun isAvailable(): Boolean {
val config = CalculatorConfig("First Calculator", true)
return config.status
}
}
이 코드는 동작하는데 전혀 문제가 없다. 하지만 테스트 코드를 작성하거나 실제로 비즈니스 로직을 개발하다보면 문제가 있음을 쉽게 알 수 있다.
강결합으로 인해 발생하는 문제
위 예시에서 CalculatorConfig는 isAvailable() 함수의 로컬 변수로 선언되어 참조되고 있다. Config의 값이 isAvailable() 함수에 강하게 결합되어 있는 것이다.
특정 함수에 강결합이다 보니 값을 변경하기 위해선 함수 자체를 수정해야 하며, 같은 Calculator 클래스 내의 다른 함수에서도 사용할 수 없게 된다.
그리고 이 상황에서는 테스트 코드를 작성하기도 어렵다. 아래는 Config의 status가 true이면 true를 반환하고, false이면 false를 반환하는지 확인하는 테스트코드이다. 하지만 지금은 이 중 하나는 무조건 실패하게 되어있다.
class DiExampleTest {
private val calculator = Calculator()
@Test
fun `config 의 status 가 false 이면 false 를 리턴한다`() {
// when
val actual = calculator.isAvailable()
// then
expectThat(actual) isEqualTo false
}
@Test
fun `config 의 status 가 true 이면 true 를 리턴한다`() {
// when
val actual = calculator.isAvailable()
// then
expectThat(actual) isEqualTo true
}
}
이 테스트를 통과하려면 Config 인스턴스를 isAvailable() 함수 안에서 만드는 것이 아닌, 외부에서 만들어 전달해주게끔 바꿔야 한다. 외부에서 isAvailable() 함수로 Config 인스턴스를 전달해주는 방법으로는 두 가지가 있다.
- Calculator 클래스의 생성자로 전달
- isAvailable() 함수의 인자로 전달
이 중 두 번째 방법으로 리팩토링을 해보겠다.
data class CalculatorConfig(
val name: String,
val status: Boolean = true
)
class Calculator {
fun isAvailable(config: CalculatorConfig): Boolean {
return config.status
}
}
// ....
class DiExampleTest {
private val calculator = Calculator()
@Test
fun `config 의 status 가 false 이면 false 를 리턴한다`() {
// given
val config = CalculatorConfig("First Calculator", false)
// when
val actual = calculator.isAvailable(config)
// then
expectThat(actual) isEqualTo false
}
@Test
fun `config 의 status 가 true 이면 true 를 리턴한다`() {
// given
val config = CalculatorConfig("First Calculator", true)
// when
val actual = calculator.isAvailable(config)
// then
expectThat(actual) isEqualTo true
}
}
외부에서 Config 인스턴스를 생성해 isAvailable() 함수로 전달해줌으로써 두 테스트 모두 통과할 수 있게 되었다. Config 인스턴스를 생성자로 전달 할지, 함수의 인자로 전달 할지는 의존 관계에 따라 달라지니 코드의 흐름에 따라 적절히 선택하면 된다.
DI의 필요성
이처럼 클래스나 함수에서 사용할 인스턴스의 결정권을 자신이 아닌, 외부에 위임함으로써 조금 더 유연한 구조를 갖게하는 방식을 DI라고 말할 수 있다.
유연한 구조를 가진 애플리케이션은 테스트가 용이하며, 최소한의 수정으로 확장할 수 있음을 의미하기도 한다.
하지만 현재의 방식은 외부에서 인스턴스를 직접 생성하여 생성자나 함수의 인자로 전달해줘야 하는 불편함이 존재한다.
이를 극복하고자 스프링에는 IoC(Inversion Of Control) 개념이 등장했으며, 다음에는 IoC의 개념과 Spring Framework의 IoC 동작 방식에 대한 이야기를 써보려 한다.
☕️ Networking
기술 직군의 기술적인 교류, 커리어 이야기, 직군 무관 네트워킹 모두 환영합니다!
위클리 아카데미 오픈 채팅방(비밀번호: 9323)
kakaotalk: https://open.kakao.com/o/gyvuT5Yd