메인 모듈과 하위 모듈 간의 의존성을 느슨하게 만들 수 있으며, 모듈을 쉽게 교체 가능한 구조로 만들 수 있다.
→ 소프트웨어의 유지보수성, 테스트 용이성, 확장성을 향상
예시 I: Dependency Injector 없이, 상위 모듈이 하위 모듈에 의존하고 있는 코드
import java.util.*;
class BackendDeveloper {
public void writeJava() {
System.out.println("자바");
}
}
class FrontEndDeveloper {
public void writeJavascript() {
System.out.println("자바스크립트");
}
}
public 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.writeJavascript();
}
public static void main(String[] args) {
BackendDeveloper backendDev = new BackendDeveloper();
FrontEndDeveloper frontEndDev = new FrontEndDeveloper();
Project project = new Project(backendDev, frontEndDev);
project.implement();
}
}
→ 여기서 만약 BackendDeveloper class의 method 이름이 변경된다면, Project의 implement method의 코드도 변경 되어야한다.
= Project가 BackendDeveloper or FrontendDeveloper에 의존하고 있다.
import java.util.List;
import java.util.ArrayList;
interface Developer {
void develop();
}
class BackendDeveloper implements Developer {
@Override
public void develop() {
writeJava();
}
public void writeJava() {
System.out.println("자바가 좋아~ 새삥새삥");
}
}
class FrontendDeveloper implements Developer {
@Override
public void develop() {
writeJavascript();
}
public void writeJavascript() {
System.out.println("자바스크립트가 좋아~ 새삥새삥");
}
}
public class Project {
private final List<Developer> developers;
public Project(List<Developer> developers) {
this.developers = developers;
}
public void implement() {
developers.forEach(Developer::develop);
}
public static void main(String[] args) {
List<Developer> dev = new ArrayList<>();
dev.add(new BackendDeveloper());
dev.add(new FrontendDeveloper());
Project project = new Project(dev);
project.implement();
}
}
→ BackendDeveloper 클래스의 메서드 이름이 writeJava에서 printJava로 변경되어도, Project 클래스의 implement 메서드의 코드는 변경할 필요가 없다.
이는 Project 클래스가 Developer 인터페이스에 의존하고 있으며, 인터페이스의 시그니처는 변경되지 않았기 때문이다.
→ Developer 인터페이스의 시그니처가 변경되면, BackendDeveloper, FrontendDeveloper, 그리고 Project 클래스에서 해당 변경된 시그니처를 따르도록 메서드를 수정해야 한다.
이는 Project, BackendDeveloper, FrontendDeveloper가 Developer 인터페이스에 의존하고 있기 때문이다.
의존관계의 역전 (DIP, Dependency Inversion Principle)
- 상위 모듈은 하위 모듈에 의존해서는 안되며, 모두 추상화에 의존해야 한다.
→ Project는 Developer Interface를 통해 하위 모듈에 의존하며, 하위 모듈 또한 BackendDeveloper, FrontendDeveloper 모두 추상화에 의존하고 있다.- 추상화는 세부사항에 의존해서는 안되며, 세부 사항은 추상화에 따라 달라져야 한다.
→ Developer 인터페이스는 BackendDeveloper, FrontendDeveloper 구현 클래스에 변경이 발생하더라도 수정이 이루어지지 않는다.
만약 Developer 인터페이스에 수정 사항이 발생하면, 해당 변경된 시그니처 부분을 구현한 BackendDeveloper 및 FrontendDeveloper 클래스에서 수정해야 한다.
Dependency Injection의 장점
- 모듈들을 쉽게 교체할 수 있는 구조가 된다.
→ Project 클래스가 구체적인 모듈(개발자)에 직접 의존하지 않고 인터페이스에 의존하기 때문에, 새로운 모듈을 도입하거나 기존 모듈을 교체하기가 용이하다.
예를 들어, 새로운 개발자 모듈을 추가하려면 해당 모듈을 Developer 인터페이스를 구현하도록 만들고, 코드 변경 없이 주입할 수 있다.- 애플리케이션 의존성 방향이 일관되어 코드를 추론하기가 쉬워진다.
- 단위 테스팅과 마이그레이션이 쉬워진다.
Dependency Injection의 단점
- 모듈이 더 생기게 되므로 (Dependency Injector) 복잡도가 증가한다.
- 종속성 주입이 파일을 할 때가 아닌 런타임 때 일어나기 때문에 컴파일 시점에서 주입에 관한 에러를 잡기가 어려워질 수 있다.