Spring DI
1. DI란
DI(Dependency Injection)란 스프링이 다른 프레임워크와 차별화되어 제공하는 의존 관계 주입 기능으로, 객체를 직접 생성하는 것이 아니라 외부에서 생성한 후 주입 시켜주는 방식이다.
DI(의존성 주입)를 통해서 모듈 간의 결합도가 낮아지고 유연성이 높아진다.
DI는 Spring에서만 사용되는 용어가 아니라 객체지향 프로그래밍에서는 어디에서나 통용되는 개념이다.
1) 강한 결합
객체 내부에서 다른 객체를 생성하는 것은 강한 결합도를 가지는 구조이다.
A 클래스 내부에서 B 라는 객체를 직접 생성하고 있다면, B 객체를 C 객체로 바꾸고 싶은 경우에 A 클래스도 수정해야 하는 방식이기 때문에 강한 결합이다.
2) 느슨한 결합
객체를 주입 받는다는 것은 외부에서 생성된 객체를 인터페이스를 통해서 넘겨받는 것이다. 이렇게 하면 결합도를 낮출 수 있고, 런타임시에 의존관계가 결정되기 때문에 유연한 구조를 가진다.
2. IoC와 DI의 관계
DI란 IoC(Inversion of Control)를 구현하는 방법으로 DL, DI 2가지 방밥이 있다.
1) DL(Dependency Lookup): 의존성 검색
컨테이너에서 제공하는 API를 이용해 사용하고자 하는 빈(Bean)을 저장소에서 Lookup 하는 것을 말한다.
2) DI(Dependency Injection): 의존성 주입
각 객체 간의 의존성을 컨테이너가 자동으로 연결해주는 것으로 개발자가 빈(Bean) 설정 파일에 의존관계가 필요한 정보를 추가해주면 컨테이너가 자동으로 연결해준다.
3. 의존성 주입 종류
Field Injection, Setter Injection, Constructor Injection 방법이 있다.
1) Field Injection
Field Injection(필드 주입)은 변수 선언부에 @Autowired Annotation을 붙인다.
1
2
3
4
5
6
@Component
public class SampleController {
@Autowired
private SampleService sampleService;
}
많이 사용됨에도 불구하고 Field Injection을 통한 의존성 주입은 권장되지 않는다.
Single Responsibility Principle Violation: 너무나 쉬운 의존성의 주입은 하나의 클래스가 지나치게 많은 기능을 하게 됨으로써 초기 설계의 목적성이자 ‘객체는 그에 맞는 동작만을 한다.’는 원칙에 위배되기 쉽다. 위배된 경우 리팩토링의 비용은 크다.
Dependency Hiding: 추상화된 의존관계는 의존성을 검증하기 힘들게 만든다.
DI Container Coupling: Field Injection을 사용하면 해당 클래스를 곧바로 Instant 화시킬 수 없다. 이 부분 때문에 Constructor Injection 이 권장되는 이유이기도 하다. 가령 Container 밖의 환경에서 해당 클래스의 객체를 참조할 때, Dependency를 정의해두는 Reflection을 사용하는 방법 외에는 참조할 방법이 없다. DI Framework는 Field Injection 된 클래스의 Instance 화에 대해서 Null Pointer Exception을 만들어낼 것이다.
Immutability: Field Injection 된 객체는 final을 선언할 수 없으므로 가변적(Mutable)이다. 객체는 변경될 수 있으며 이에 대한 대응에는 큰 비용이 든다.
2) Setter Injection
Setter Injection(수정자를 통한 주입)은 선택적인 의존성을 사용할 때 유용하다. 상황에 따라 의존성 주입이 가능하다. 스프링 3.x Documents에서는 Setter Injection을 추천했었다.
Setter Injection은 set Method를 정의해서 사용한다.
1
2
3
4
5
6
7
8
9
10
@Component
public class SampleController {
private SampleService sampleService;
@Autowired
public void setSampleService(SampleService sampleService) {
this.sampleService = sampleService;
}
}
Setter Injection으로 의존관계 주입은 런타임시 할 수 있도록 낮은 결합도를 가지게 구현되었다. 그러나 Setter Injection을 통해서 Service의 구현체를 주입해주지 않아도 Controller 객체는 생성이 가능하다.
Controller 객체가 생성 가능하다는 것은 내부에 있는 Service의 method 호출이 가능하다는 것인데, set을 통해 Service의 구현체를 주입해주지 않았으므로, NullPointerException이 발생한다.
주입이 필요한 객체가 주입되지 않아도 얼마든지 객체를 생성할 수 있다는 것이 문제다. 이 문제를 해결 할 수 있는 방법이 Constructor Injection이다.
3) Constructor Injection
Constructor based Injection (생성자를 통한 주입)은 Constructor에 @Autowired Annotation을 붙여 의존성을 주입받을 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
public class SampleService {
private SampleDAO sampleDAO;
@Autowired
public SampleService(SampleDAO sampleDAO) {
this.sampleDAO = sampleDAO;
}
}
@Component
public class SampleController {
private final SampleService sampleService = new SampleService(new SampleDAO());
...
}
4. Construtor Injection을 사용해야 하는 이유
Spring Framework Reference에서 권장하는 방법은 생성자를 통한 주입이다. 생성자를 사용하는 방법이 좋은 이유는 필수적으로 사용해야 하는 의존성 없이는 Instance를 만들지 못하도록 강제할 수 있기 때문이다.
Spring 4.3버전부터는 Class를 완벽하게 DI Framework로부터 분리할 수 있다. 단일 생성자에 한해 @Autowired를 붙이지 않아도 된다.
Spring 4.3부터는 클래스의 생성자가 하나이고 그 생성자로 주입받을 객체가 Bean으로 등록되어 있다면 @Autowired를 생략할 수 있다. 또한 Field Injection의 단점들을 장점으로 가져갈 수 있다.
NullPointerException을 방지할 수 있다.
객체에 final을 사용할 수 있다.
순환 의존성을 알 수 있다.
생성자의 인자가 많아지면 코드가 길어지며 리펙토링을 하게 된다.