DI란 무엇이고 사용하면 뭐가 좋을까?
What is DI?
DI는 Depedency Injection의 줄임말로 의존관계 주입이라고 한다.
의존관계(Dependency)
우선 DI를 설명하기 전에, 의존관계(Dependency)가 뭘까? 다음과 같은 코드를 A가 B에 대한 의존관계를 가지고 있다고 할 수 있다.
class A {
private final B b;
public A() {
b = new b();
}
}
이렇게 의존관계를 가지고 있을 시, B가 변하게 되면 그 영향이 A에 미칠 수 있다.
즉, B에 대한 결합도가 높아지게 되는데 결합도가 높아지면 어떻게 될까? 결합도가 높을수록 하나의 모듈이 다른 모듈에 종속적이게 되어 유지 보수와 변경이 어려워진다. 또한, 확장성과 재사용성이 감소된다.
추상화를 이용하여 관계를 조금 느슨하게 하여 결합도를 낮출 수 있다.
class A {
private final BInterface bInterface;
public A() {
bInteface = new BInterfaceImpl1();
//bInteface = new BInterfaceImpl2();
//bInteface = new BInterfaceImpl3();
}
public void print() {
System.out.println(bInterface.print());
}
}
interface BInterface {
print();
}
class BInterfaceImpl1 implements BInterface {
@Override
public String print() {
return "B1";
}
}
...
하지만 아직까지도 추상화된 Interface인 BInterface뿐 아니라 구체 클래스인 BInterfaceImpl1(BInterface2, BIntreface3)도 의존하고 있다.
Dependency Injection(DI)
토비의 스프링에서는 다음의 세가지 조건을 충족하는 작업을 의존관계 주입이라고 한다
- 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스만 의존하고 있어야 한다.
- 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정한다.
- 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입)해줌으로써 만들어진다.
이처럼 그 의존관계를 외부에서 결정하고 주입하는 것을 의존관계 주입이라고 한다. 즉, A 안의 BInterface를 내부적으로 어떤 값을 가질지 정하는 것이 아니라 런타임 시점의 의존관계를 외부에서 결정하고 주입해주는 것이다.
class A {
private final BInterface bInterface;
public A(BInterface bInterface) {
this.bInterface = bInterface;
}
public void print() {
System.out.println(bInterface.print());
}
}
A a = new A(new BInterfaceImpl1());
a.print(); //B1
위처럼 외부에서 DI를 주입함으로 써 DIP까지 만족하게 되었다. 하지만 추상화와 DI를 이용하여 결합도를 낮추고 DIP까지 만족하게 되었지만 아직 OCP를 만족하지 못하였다.
DIP: 프로그래머는 추상화(인터페이스)에 의존해야지 구현체(클래스)에 의존하면 안된다.
OCP: 기존의 코드를 변경하지 않으면서 기능을 확장할 수 있어야 함.
//A a1 = new A(new BInterfaceImpl1());
A a2 = new A(new BInterfaceImpl2()); //BInterfaecImpl2로 변경
위처럼 직접 DI를 주입했을 때는 기능을 확장하려면 기존의 코드를 수정해야 한다. 하지만 스프링을 이용하여 DI를 주입하게 되면 이러한 문제점을 해결할 수 있다.
애플리케이션의 전체 동작 방식을 구성(config) 하기 위해 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스 생성(혹은 각 객체에@Component 사용할 수도 있음)
@Configuration
public class AppConfig{
@Bean
public BInterface bInterface() {
return new BInterfaceImpl1());
}
}
위와 같은 설정 클래스에 @Configuration을 붙이고 주입하려고 하는 의존관계를 @Bean을 붙여 빈 등록을 한다.
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
A a = ac.getBean(A.class); //b = BInterfaceImpl1
a.print() //B1
이렇게 하면 BInterfaceImpl1이 주입된 A 객체를 사용할 수 있다. BInterfaceImpl1이 아닌 BInterfaceImpl2로 변경하고 싶다면 AppConfig의 다음 부분만 수정하면 된다.
@Configuration
public class AppConfig{
@Bean
public BInterface bInterface() {
return new BInterfaceImpl2()); //이 부분만 수정
}
}
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
A a = ac.getBean(A.class); //b = BInterfaceImpl2
a.print(); //B2
기존의 코드는 변경되지 않았지만, 내부의 의존성은 BInterfaceImpl2로 변경된 걸 확인할 수 있다. 이처럼 스프링을 사용하여 DI를 주입하면 기존의 코드를 변경하지 않고도 기능 확장이 가능하다.
DI의 장점
- 결합도의 감소: DI를 사용하면 객체 간의 결합도가 낮아지므로 개발, 유지보수성이 높아진다.
- 테스트 용이성: DI를 사용하면 의존성을 주입하기 때문에 객체의 동작을 검증하기 위해 테스트 객체를 주입하여 테스트 용이성을 높일 수 있다.
- 재사용성: 객체 간의 결합도를 낮추기 때문에 객체의 재사용성을 높인다.
- 가독성: DI는 객체 간 의존성을 명시하고 객체 생성 및 의존성 주입을 한 곳에서 관리하기 때문에 코드의 가독성을 높일 수 있다.
아래는 예전에 스프링의 강력한 무기 DI에 대해 작성한 글이므로 참고
참고: 의존관계 주입(Dependency Injection) 쉽게 이해하기
*틀린 부분이 있으면 언제든지 말씀해 주시면 공부해서 수정하겠습니다.