DI – Dependency Injection
의존하는 클래스에 대한 인스턴스를 직접 생성하지 않고, 컨테이너로부터 생성된 빈을 setter나 생성자를 통해 외부로부터 주입받는 것을 의미한다.
DI는 왜 사용될까? 가장 큰 이유는 의존성을 외부에서 주입하기 위해 사용한다. 아래 코드를 보자.
class Program { private IMacBook mac; public Program() { this.mac = new MacBook(); } public StartMacBook() { this.mac.boot(); } }
위 코드에서 boot 메서드를 호출하기 위해서는 MacBook 클래스가 필요로 한다. 여기서 Program 클래스는 MacBook 클래스의 의존성을 가진다고 말할 수 있다. 하지만 위 코드는 의존성을 Program 클래스 내에서 생성한다.
위와 같이 코드를 작성하면, 코드의 재활용성이 떨어지고 MacBook 클래스를 수정하면 Program 클래스도 수정해야하는 문제가 발생한다. 지금은 하나의 클래스지만 수많은 클래스에서 MacBook를 사용하면 하나씩 수정해야 하는 이슈가 있다. 결국, Coupling(결합도)이 높아진다. 그리고 Unit Test를 하기 어려워진다.
위 코드에 DI를 적용하면 아래 코드가 된다.
class Program { private IMacBook mac; @Autowired public Program(IMacBook mac) { this.mac = mac; } public StartMacBook() { this.mac.boot(); } }
Program에 IMacBook 이라는 인터페이스를 주입했다. 인터페이스를 전달했으며, 객체가 변화되더라도 Program Class는 변경할 필요가 없어진다. IMacBook에 어떤 클래스를 전달할 지만 정하면 된다. 또한, 연결된 Class를 바꿔야 한다면 해당 Class 이름만 바꾸면 된다.
사용 방법
IMacBook은 컨테이너에서 관리되며, Spring은 세 가지 방법을 통해 DI 컨테이너를 관리한다.
1. Contructor Injection – 생성자를 통해 전달
public class ClassName { private final IMacBook mac; @Autowired public ClassName(IMacBook mac) { this.mac = mac; } }
2. Method(Setter) Injection – setter()을 통한 전달
public class ClassName { private IMacBook mac; @Autowired public void setMacBook(IMacBook mac) { this.mac = mac; } }
3. Field Injection – 일반적인 변수 선언만으로 전달
public class ClassName { @Autowired private IMacBook mac; }
위 세 가지 중에서 Intellij에서는 첫 번째 방법을 사용하라고 권장한다. 이유는 아래와 같다.
- NullPointerException를 방지할 수 있다.
- 주입받을 필드를 final로 선언 가능하다.
- 스프링에서는 테스트 코드 작성이 쉬워지며, 순환 참조 방지가 된다.
사용하는 이유는?
DI를 사용하면 결합도가 낮아진다. 그로 인해 코드 수정이 쉬워지며, 테스트 코드 작성도 쉬워진다.
IoC를 실현하는데 DI가 필수이며 IoC를 사용하면, 귀찮은 객체 생명 주기 같은 프로그램 제어권을 프레임워크가 관리하도록 넘길 수 있다.
스프링 프레임워크 버전 4.3 이후에는 @Autowired도 생략되면서 생성자에 사용한 Bean만 선언하면 된다. 코드가 간결해졌다.
DI를 사용하면 순환 참조 에러를 방지 할 수 있다. 순환 참조 에러란 A 클래스와 B 클래스가 서로를 참조하면서 문제가 발생하는 것을 의미한다.
객체의 불변성을 확보하면서 객체 안정성을 높일 수 있다. 생성자 주입이기 때문에 변경의 가능성을 배제할 수 있다.