DI가 되지 않았을 때 문제점
전공자들은 객체지향프로그래밍을 한번쯤 수업에서 접할 기회가 있었을 것이다.
혹시 잘 모르겠다면 정리해둔 OOP에 대해서를 참고해보는 것도 좋을 것 같다.
SOLID에서 D를 담당하는 의존성 역전 원칙은 결국 내부에 구현체를 직접 생성하는 것은 좋지 않다는 것을 의미하는데,
이를 해결하기 위해 밖에서 생성된 것을 인자로 받아서 사용함으로써 구현에 의존하지 않도록 한다.
즉 객체 생성의 책임을 다른 곳으로 돌리는 것이다.
이를테면 Computer가 있고 여기에 CPU와 GPU가 있을 때 자신의 부품을 설명하는 기능이 있다고 생각해보자.
class Computer(){
val cpu = INTEL()
val gpu = RADEON()
fun describe() {
println("본 컴퓨터의 제품 사양을 설명해드리겠습니다!")
cpu.describe()
gpu.describe()
}
}
본 코드처럼 Computer 안에 cpu와 gpu의 객체를 직접 생성했을 때 만약 컴퓨터의 사양이 바뀌어서 cpu와 gpu의 회사가 달라졌다면 어떻게 될까? 혹은 기존의 구현체에서 생성자가 수정되었다면 어떻게 될까?
아래와 같이 직접 Computer 클래스의 내부 구현을 바뀐 사양으로 바꿔야 할 것이다.
class Computer(){
// 생성자를 바꿔주었다.
val cpu = INTEL("인텔입니다")
val gpu = RADEON("라데온입니다")
// cpu와 gpu 부품을 바뀐 회사의 것으로 교체해주었다.
val cpu = AMD()
val gpu = NVIDIA()
fun describe() {
println("본 컴퓨터의 제품 사양을 설명해드리겠습니다!")
cpu.describe()
gpu.describe()
}
}
지금은 간단한 예시이니만큼 간단하게 고친 것이지만 실무에서 이렇게 코드를 작성했다면 특정 객체가 바뀔 때마다 모든 파일을 돌아보면서 영향을 받는 파일이 있는지를 확인하고 직접 고쳐가야 할 것이다.
파일이 1억개, 각 파일당 1만줄이 적혀있는 엄청난 규모라면 객체 한번 바꾸려다가 1년이 걸려도 못할 지도 모른다.
이러한 사태의 원인은 바로 클래스 내부가 다른 것의 구현에 영향을 직접적으로 받기 때문이다.
본 코드에서는 Computer클래스의 cpu와 gpu가 직접적인 구현체에 영향을 받고 있다.
이로 인해 CPU와 GPU가 바뀐 것이지만 나비효과로 Computer 클래스까지 영향을 미치는 것이다.
그렇다면 이를 인자로 받아서 사용한다면 어떨까
abstract class CPU(){
abstract fun describe()
}
abstract class GPU(){
abstract fun describe()
}
class AMD : CPU(){
override fun describe() { println("본 CPU의 제조회사는 AMD 입니다!") }
}
class NVIDIA : GPU(){
override fun describe() { println("본 GPU의 제조회사는 NVIDIA 입니다!") }
}
class Computer(val cpu : CPU, val gpu : GPU){
fun describe() {
println("본 컴퓨터의 제품 사양을 설멍해드리겠습니다!")
cpu.describe()
gpu.describe()
}
}
본 코드를 보면 Computer클래스는 더 이상 내부에서 직접 변수를 생성하지 않고 인자로 받는다. 더 나아가 받는 타입을 기존의 INTEL과 같은 구체적인 브랜드가 아니라 CPU, GPU와 같은 타입으로 받음으로써 CPU 혹은 GPU의 브랜드가 변경되더라도 Computer클래스에는 영향이 가지 않는다.
이렇게 구체적인 브랜드가 아니라 CPU, GPU로 바꿔서 유연하게 대응하는 것은 객체지향에서 추상화를 살려 의존성 역전, DIP를 지킨 것이다. 또한 Computer클래스 입장에서만 보자면 구현체를 직접 만들지 않고 인자로 받음으로써 기존의 자신이 사용하기 위해 구현체를 만들어야 하던 책임을 외부로 넘겼다.
결론적으로 이 객체 생성의 책임을 개발자가 아니라 외부에서 관리하는 것을 IoC, Inversion of Control, 제어 역전이라고 한다.
그리고 외부에서 구현체를 주입 받는 것을 의존성 주입, Dependency Injection이라고 한다.
용어 정리
처음부터 많은 얘기와 낯선 용어로 조금 혼란이 왔을 것 같다. 기존의 설명 뿐 아니라 앞으로 설명할 것들을 위해 잠시 용어를 정리하고자 한다.
의존성, Dependency
의존성이란 어떤 대상이 참조하는 객체 또는 함수를 의미한다.
본 예시에서는 컴퓨터는
CPU와GPU에 의존한다라고 하며CPU와GPU각각을 의존성이라고 한다.의존성 주입, Dependency Injection
의존성을 외부에서 주입받는 방식을 의미한다.
Injector
의존성을 제공해주는 역할을 하는 것을 의미한다.
컴퓨터에서
CPU와GPU를 인자로 외부에서 받게 되는데, 이를 제공해주는 역할을 하는 것을 Injector라고 한다.혹은
Container,Assembler,Provider,Factory라고 불리기도 한다.
정리
기존에 설명한 내용을 조금 정리해보면
- 구현체에 의존하는 방식은 코드 수정 등에 좋지 않다.
- 의존성을 직접 구현하지 않고, Injector를 통해 주입받아서 사용하자
- 이걸
Dependency Injection이라고 한다.
이게 핵심이다.
Injector
본 설명만 들었으면 그래서 Injector가 뭔지는 이해했지만 그래서 Injector라는 것이 어디 있는지가 궁금할 것이다.
우선 아무것도 없이 직접 만들 수도 있다.
// File: "Injector.kt"
abstract class CPU(){
abstract fun describe()
}
abstract class GPU(){
abstract fun describe()
}
class AMD : CPU(){
override fun describe() { println("본 CPU의 제조회사는 AMD 입니다!") }
}
class NVIDIA : GPU(){
override fun describe() { println("본 GPU의 제조회사는 NVIDIA 입니다!") }
}
class CPUInjector{
fun getAMD() = AMD()
}
class GPUInjector{
fun getNVIDIA() = NVIDIA()
}
class Computer(val cpu : CPU, val gpu : GPU){
fun describe() {
println("본 컴퓨터의 제품 사양을 설멍해드리겠습니다!")
cpu.describe()
gpu.describe()
}
}
fun main() {
val cpuInjector = CPUInjector();
val gpuInjector = GPUInjector();
// CPU Injector와 GPU Injector를 통해 외부에서 객체를 생성하여 넘겨주는 코드
val computer = Computer(cpuInjector.getAMD(), gpuInjector.getNVIDIA())
computer.describe()
}
본 코드를 보면 CPU와 GPU 의존성을 제공해주기 위해 CPUInjector와 GPUInjector클래스를 생성했다. 각 Injector내에는 컴퓨터의 의존성을 만들어내는 함수들이 존재하고 컴퓨터 객체 생성 시에 Injector로 의존성을 넣어주었다.
이를 통해 Computer는 각 의존성을 생성, 관리할 책임에서 벗어나고, 각각의 Injector가 관리하게 된다.
곧 Android에서 사용하는 DI를 위해 사용하는 Hilt에 대해서도 올려볼 수 있도록 하겠다.