Dependency Injection 이란 메인 모듈이 직접 다른 하위 모듈에 대한 의존성을 가지고 있기 보다는, 중간에 의존성 주입자 (dependency injector) 가 이 부분을 가로채 메인 모듈이 간접적으로 의존성을 주입하는 방식을 말한다.
이를 통해서 메인 모듈과 하위 모듈간의 의존성을 조금 더 느슨하게 만들 수 있으며 모듈을 쉽게 교체 가능한 구조로 만든다.
의존한다는 의미
A가 B에 의존한다 = B가 변하면, A도 변한다.
아래 코드의 경우, Hi는 Hello 클래스에 의존하고 있는데, 만약 Hello 클래스의 greet 에 매개변수나 다른 요소들이 추가되면, Hi 클래스도 그에 맞춰 같이 바뀌어져야한다.
package me.whiteship.designpatterns;
public class DependencyInjectionApp {
public static void main(String[] args) {
new Hi().greet();
}
}
class Hello {
public void greet() {
System.out.println("greetings from b");
}
}
class Hi {
public void greet() {
new Hello().greet();
}
}
Dependency Injection 예시
DI 적용하지 않고 바로 구현했을 때
public class DINotApplied {
public static void main(String[] args) {
Project project = new Project(new BackendDeveloper(), new FrontendDeveloper());
project.implement();
}
private static class BackendDeveloper {
public void writeJava() {
System.out.println("coding with java");
}
}
private static class FrontendDeveloper {
public void writeJs() {
System.out.println("coding with javascript");
}
}
private static class Project {
private final BackendDeveloper backendDeveloper;
private final FrontendDeveloper frontendDeveloper;
public Project(BackendDeveloper backendDeveloper,
FrontendDeveloper frontendDeveloper) {
this.backendDeveloper = backendDeveloper;
this.frontendDeveloper = frontendDeveloper;
}
public void implement() {
backendDeveloper.writeJava();
frontendDeveloper.writeJs();
}
}
}
DI 적용하여 구현했을 때
package me.whiteship.designpatterns._04_di;
import java.util.ArrayList;
import java.util.List;
public class DIApplied {
public static void main(String[] args) {
List<Developer> developers = new ArrayList<>();
developers.add(new BackendDeveloper());
developers.add(new FrontendDeveloper());
Project project = new Project(developers);
project.implement();
}
interface Developer {
void develop();
}
static class BackendDeveloper implements Developer {
@Override
public void develop() {
writeJava();
}
void writeJava() {
System.out.println("coding with java");
}
}
static class FrontendDeveloper implements Developer {
@Override
public void develop() {
writeJs();
}
private void writeJs() {
System.out.println("coding with javascript");
}
}
static class Project {
private final List<Developer> developers;
public Project(List<Developer> developers) {
this.developers = developers;
}
public void implement() {
developers.forEach(Developer::develop);
}
}
}
자세히보기
위와 같이 구현하면 새로운 개발자 유형 (안드로이드, IOS 등)이 추가되어도 크게 수정하지 않고 바로 적용이 가능하다.
의존 관계 역전 원칙(Inversion of Dependency Injection)
의존성 주입시에는 의존관계역전원칙이 적용된다. 이는 아래의 2가지가 충족되는 상황을 말한다.
상위 모듈은 하위 모듈에 의존해서는 안된다. 둘다 추상화에 의존해야한다.
추상화는 세부사항에 의존해서는 안된다. 세부사항은 추상화에 따라 달라져야한다.
의존성 주입의 장점
외부에서 모듈을 생성해서 집어넣는 구조가 되기 때문에 모듈을 쉽게 교체할 수 있게 된다.
위의 예에서 BackendDeveloper -> FrontDeveloper -> AppDeveloper 등 손쉽게 교체가 가능하다.
단위 테스트와 마이그레이션이 쉬워진다.
어플리케이션 의존성 방향이 좀 더 일관되어 코드를 추론하기가 쉬워진다.
의존성 주입의 단점
모듈이 더 생기는 것이므로 코드가 복잡해진다.
종속성 주입이 런타임에 일어나기 때문에 컴파일 할 때에는 종속성 주입에 관한 에러를 사전에 잡기가 어려워진다.