DI(Dependency Injection, 의존성 주입)은 설명 그대로 의존성을 주입하는 방식을 뜻한다.
우선 의존성을 주입한다는 뜻부터 알아보자. A가 B에 의존한다는 말은 B가 변하면 A도 변해야 함을 뜻하고, A -> B와 같이 나타낼 수 있다. 예를 들어 다음과 같은 코드가 있다고 할 때
class JdbcRepository {
public void save() {
// code
}
}
class User {
public void saveUser(User user){
new JdbcRepository().save(user);
}
}
만약 DB 접근 방식이 Jdbc에서 Jpa로 바뀐다면 아래와 같이 코드가 변해야 한다.
class JpaRepository {
public void save() {
// code
}
}
class User {
public void saveUser(User user){
new JpaRepository().save(user); // 코드 변경
}
}
이렇게 User 클래스는 JdbcRepository, JpaRepository에 의존하고 있고
User -> JdbcRepository
User -> JpaRepository
그림으로는
와 같이 나타낼 수 있다.
그렇다면 위 코드는 올바르게 DI가 적용되었다고 말할 수 있을까? 당연히 아니다. 왜냐하면 User가 구체적인 클래스(JdbcRepository, JpaRepository)에 의존하고 있기 때문이다. 즉 Repository 관련 코드가 변할 때 마다 User 클래스의 코드도 변해야 하기 때문이다.
DIP
User 클래스가 추상화에 의존하도록 설계하려면 다음과 같이 수정할 수 있다.
interface Repository {
void save();
}
class JdbcRepository implements Repository {
@Override
public void save() {
// code
}
}
class JpaRepository implements Repository {
@Override
public void save() {
// code
}
}
class User {
private final Repository repository;
public User(Repository repository) {
this.repository = repository;
}
public void saveUser(User user){
repository.save(user);
}
}
이렇게 하면 User 클래스와 Repository 관련 클래스(Jdbc, Jpa) 모두 인터페이스인 Repoistory 클래스에 의존하게 된다.
그림에서 볼 수 있듯이 화살표가 Repository를 향하고, 즉 화살표의 역전이 일어나고 있다. 이를 DIP(Dependency Inversion Principle, 의존관계역전원칙)이라 한다.
그리고 이제는 Repository 관련 클래스가 변해도 User 클래스는 변화가 없음을 알 수 있다. 왜냐하면 User 클래스가 Repository, 즉 추상화에 의존하고 있기 때문이다.
DIP는 2가지의 규칙을 지켜야 한다.
1. 상위 모듈은 하위 모듈에 의존해서는 안된다. 둘 다 추상화에 의존해야 한다. 2. 추상화는 세부사항에 의존해서는 안되고, 세부사항은 추상화에 따라 달라져야 한다.
DI의 장점과 단점
DI를 사용하면 모듈들을 쉽게 교체할 수 있는 구조로 설계가능하다. 또한 단위 테스팅과 마이그레이션이 쉬워지며, 어플리케이션의 의존성 방향이 일관되어 코드를 추론하기 쉬워진다는 장점이 있다. 하지만 결국 모듈이 더 생기게 되므로 복잡도가 증가하고 종속성 주입이 런타임 때 일어나기 때문에 컴파일을 할 때 종속성 주입에 관한 에러를 잡기 어려워질 수 있다.