[Spring] 의존성 주입의 3가지 방식
얼마전까지 의존성 주입에 @Autowired방식을 맹목적으로 사용해왔다. 다른 방식이 있는 줄은 모르고 어노테이션을 사용하면 의존성 주입이 간편해서 이런 방식으로 해왔다. 그러다 우연히 의존성 주입은 생성자를 사용한 방식이 좋다는 글을 몇차례 접한뒤 좋은 이유에 대해서 알아보고 실제로 적용해보기로 했다.
의존성 주입 3가지의 방식
첫번째, Field Injection
Field Injection은 의존성을 주입하고 싶은 필드에 @Autowired 어노테이션을 붙여주면 의존성이 주입된다.
@RestController
public class PostController {
@Autowired
private PostService postService;
}
두번째, Setter based Injection
setter메서드에 @Autowired 어노테이션을 붙여 의존성을 주입하는 방식이다.
@RestController
public class PostController {
private PostService postService;
@Autowired
public void setPostService(PostService postService){
this.postService = postService;
}
}
세번쨰, Constructor based Injection
생성자를 사용하여 의존성을 주입하는 방식이다.
@RestController
public class PostController {
private final PostService postService;
public PostController(PostService postService){
this.postService = postService;
}
}
뭐가 다를까?
생성자 주입 방법은 필드 주입이나 수정자 주입과는 빈을 주입하는 순서가 다르다.
Setter based Injection은
1. 주입받으려는 빈의 생성자를 호출하여 빈을 찾거나 빈 팩토리에 등록
2. 생성자 인자에 사용하는 빈을 찾거나 만듦
3. 주입하려는 빈 객체의 수정자를 호출하여 주입
Field Injection은
1. 주입받으려는 빈의 생성자를 호출하여 빈을 찾거나 빈 팩토리에 등록
2. 생성자 인자에 사용하는 빈을 찾거나 만듦
3. 필드에 주입
두 방식은 런타임에서 의존성을 주입하기 때문에 의존성을 주입하지 않아도 객체가 생성될 수 있다.
Constructor based Injection은
1. 생성자의 인자에 사용되는 빈을 찾거나 빈 팩토리에서 만든다.
2. 찾은 인자 빈으로 주입하려는 생성자를 호출한다.
객체가 생성되는 시점에 빈을 주입한다. 의존성이 주입되지 않아 발생할 수 있는 NullPointerException을 방지한다.
순환 참조
아래는 @Autowired를 사용한 의존성 주입 방식이다.
@Service
public class AService {
// 순환 참조
@Autowired
private BService bService;
public void sayA() {
bService.sayB();
}
}
@Service
public class BService {
// 순환 참조
@Autowired
private AService aService;
public void sayB() {
aService.sayA();
}
}
이 코드는 서로간 참조가 발생하게된다.
Field Injection, Setter based Injection은 빈이 생성된 후에 참조를 하기 때문에 애플리케이션이 아무런 오류나 경고없이 구동된다. 실제 코드가 호출될 때까지 문제를 알 수 없는 것이다.
반면, 생성자를 통해 주입하고 실행해보면 BeanCurrentlyInCreationException이 발생한다. 순환 참조 뿐만아니라 나아가서 의존 관계에 내용을 외부로 노출 시킴으로서 애플리케이션을 실행하는 시점에서 오류를 체크할 수 있다.
순환 참조는 설계방식에 문제가 있는 것이기 때문에 오류를 사전에 파악하는 것 이 더 좋다.
불변성
생성자로 의존성을 주입할 때 field를 final로 선언할 수 있다. 이로인해 런타임에서 의존성을 주입받는 객체가 변할 일이 없어진다.
@RestController
public class PostController {
private final PostService postService;
public PostController(PostService postService){
this.postService = postService;
}
}
또한 필드 주입 방식은 null이 만들어질 가능성이 있는데, final로 선언한 생성자 주입 방식은 null이 불가능한다.
단일 책임의 원칙 위반
클래스는 단일 책임을 지녀야한다. @Autowired를 하나의 클래스에 남용하게되면 여러개의 책임을 떠맡게될 수 있다. 생성자를 통한 의존성 주입은 파라미터 수가 늘어가면 하나의 클래스가 여러 책임을 지닌다는 걸 깨달을 수 있고 리팩토링의 신호가 된다. 개인적으로 이부분에 대해서 와닿지는 않는다. @Autowired를 선언한 필드를 보는 것과 생성자에 파라미터 수를 보는게 무슨 차이가 있는지 모르겠다.
테스트 작성의 용이함
테스트하려는 클래스가 필드 주입을 받을 때 외부에서 빈을 주입해 줄 수 없다. 그렇기에 해당 필드는 null 값이다. 따라서 스프링 빈 및 모든 설정을 가져오고 실행해야 테스트가 가능하다. 생성자 주입일때에는 테스트 코드 자체에서 필요한 의존관계를 만들어서 테스트가 가능하다.
lombok을 활용한 생성자 주입
매번 생성자를 만들어 의존성을 주입하는게 귀찮게 느껴질 수 있다. 그때 lombok을 활용하면 쉽게 적용이 가능하다.
@RequiredArgsConstructor는 final로 선언된 필드를 가지고 생성자를 만들어준다. 개발도중 필드가 계속 바뀌어도 lombok의 관리하에 생성자 코드의 파라미터를 고칠 필요가 없다.
@RequiredArgsConstructor
@RestController
public class PostController {
private final PostService postService;
}
생성자 주입 방식은 이러한 장점들이 있다. 코드를 작성할 때 생성자 주입방식을 사용하여 개발을 진행할 것이다.