DIP 와 DI 에 대해 설명하고, 두 차이점을 정리한다.
DIP (Dependency Inversion Principle)
DDD start!
라는 책에서 DIP 에 대해 설명하는 부분이 있다. DIP 는 특정 구현체를 직접적으로 참조하지 않고 interface
를 통해 강결합을 약한 결합으로 풀어주어 유지보수(변경)와 테스트하기 쉽게 만드는 것이 목적이라 한다. DDD start
에서 DIP 를 설명할때, 고수준 모듈과 저수준 모듈이라는 용어를 사용한다. 고수준 모듈에서 저수준 모듈을 직접 참조하여 사용하면, 의존성이 강해진다. 구현체(저수준 모듈)를 바로 사용하지 않고, 기능을 정의한 interface(고수준 모듈)를 두어 다른 고수준 모듈에서는 같은 고수준 모듈인 interface 를 사용하는 것이 좋다.
그렇게 한다면, 시스템을 더 유연하고 테스트하기 쉽게 만들어 준다.
강결합의 예
public class Barista {
public Coffee getCoffee() {
CoffeeMachine coffeeMachine = new CoffeeMachine();
Coffee coffee = coffeeMachine.extractCoffee();
coffee.setBaristaNickname("choebk");
return coffee;
}
}
public class CoffeeMachine {
public Coffee extractCoffee() {
Coffee coffee = new Coffee();
// ...
return coffee;
}
}
- CoffeeMachine 클래스의 extractCoffee() 메소드 내구 구현이 변경되면, 경우에 따라 Barista 클래스의 getCoffee() 메소드까지 변경해야한다.
- 내부 구현 변경이 아닌, CoffeeMachine 의 스펙이 확장되어 커피머신기가 여러 종류로 증가되면, Barista 클래스를 크게 변경해야 한다.
- if 문을 통한 처리를 했다면, 커피머신기가 계속 추가될 때 마다 코드 추가가 필요하다. 인터페이스를 통해 결합도를 낮추고 Barista 클래스를 생성하는 시점에 어떤 XXXCoffeeMachineImpl 을 사용할지 구현체를 넘겨주는 방식으로 개선해야 한다.
결합도 낮추기
public class Barista {
private String baristaName;
private CoffeeMachine coffeeMachine;
public Barista(String baristaName, CoffeeMachine coffeeMachine) {
this.baristaName = baristaName;
this.coffeeMachine = coffeeMachine;
}
public Coffee getCoffee() {
Coffee coffee = coffeeMachine.extractCoffee();
coffee.setBaristaName(baristaName);
return coffee;
}
}
public interface CoffeeMachine {
Coffee extractCoffee();
}
public class 에스프레소머신 implement CoffeeMachine {
@Override
public Coffee extractCoffee() {
Coffee coffee = new Coffee();
// ...
return coffee;
}
}
- Barista 객체를 생성할 때 에스프레소머신 객체를 넣던 드롱기커피머신 객체를 넣던 Barista 를 사용하는 측에서 결정하도록 위임하면 Barista 클래스와 CoffeeMachine 클래스의 결합도를 낮출 수 있다.
DI (Dependency Injection)
- 스프링 프레임워크를 사용하면서 대부분 코드들이 위 DIP 처럼 구현된 코드가 많아 오해했었다. 하지만 DI 는 꼭 interface 나 상위클래스를 통해 고수준 모듈과 저수준 모듈을 구분하는 것이 필수는 아니다. DI 는 단지 어떤 객체를 사용해야하는지 외부에서 주입해주는 것이다. 객체를 사용하는 측에서 직접 new 연산자를 통해 생성하는 것이 아니라, 외부에서 생성된 객체를 받는 것을 말하고, 이것을 객체를 주입받는다고 한다. 의존관계가 실행 시점에 결합되어 결합도를 낮출 수 있고, 객체 생성의 책임이 내부에 없기 때문에 응집도를 높일 수 있다.
- 낮은 결합도로 테스트 코드 작성이 쉬워지며, 모듈 간 영향관계가 적어지므로 한 기능을 수정하기 쉬워지고, 모듈 간 재사용이 더욱 쉬워지는 장점이 있다.
- 스프링 xml config 에서는 bean 사이의 의존 관계에 대한 설정 정보를 바탕으로 객체를 주입하고, 스프링 부트에서는 그 설정 정보 조차 자동으로 매핑해준다.
- 아래의 Barista 클래스를 예로들면 주입받는
coffeeMachine
이 interface 이던 구현 class 이던 상관없이 바리스타 클래스에 커피머신이라는 객체를 주입하는 것이다. CoffeeMachine 이 구현 클래스여도 객체를 외부에서 주입했기 때문에 DI 가 아니라고 할 수 없다.
public class Barista {
private final CoffeeMachine coffeeMachine;
public Barista(CoffeeMachine coffeeMachine) {
this.coffeeMachine = coffeeMachine;
}
public Coffee getCoffee() {
Coffee coffee = coffeeMachine.extractCoffee();
coffee.setBaristaNickname("choebk");
return coffee;
}
}
public class CoffeeMachine {
public Coffee extractCoffee() {
Coffee coffee = new Coffee();
// ...
return coffee;
}
}