스프링의 다양한 의존성 주입 방식
스프링에는 다양한 DI(Dependency Injection) 의존성 주입 방식이 있습니다. 종류는 다음과 같습니다.
- 수정자 주입(Setter 주입)
- 필드 주입
- 생성자 주입
여러 방법 중 스프링에서는 생성자 주입 방식을 권장합니다. 왜 DI를 할 때 생성자 주입을 해야 될까요?
각각의 의존관계 주입 방식의 특징과 차이에 대해서 자세히 살펴보도록 하겠습니다.
수정자 주입(Setter 주입 방식)
@Getter
@Controller
public class MyController {
private ServiceA service;
@Autowired
public void setService(ServiceA serviceA) {
this.service = serviceA;
}
}
수정자 주입은 다음과 같이 setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해 의존관계를 주입하는 방식을 뜻합니다. set 메서드 위에 @Autowired를 붙이면 스프링 컨테이너에서 관리하는 빈을 주입해주게 됩니다.
최종적으로 MyController는 ServiceA빈을 주입받게 되는 것이죠. 수정자 주입 방식의 가장 큰 문제는 변경 가능한 특징에
있습니다.
의존 관계 주입은 한번 일어나면, 어플리케이션 종료 시점까지 의존관계를 변경할 일이 없습니다. 대부분의 의존관계는
어플리케이션 종료 전까지 불변해야 됩니다. 수정자 주입 방식을 사용하려면 set 메서드를 public으로 열어둬야 하는데
이렇게 되면 누군가의 실수로 인해 해당 필드를 변경할 수 있습니다.
@SpringBootTest
public class DependencyInjectionTest {
@Autowired
MyController myController;
@Test
void test1() {
System.out.println(myController.getService());
myController.setService(new ServiceB());
System.out.println(myController.getService());
}
}
위의 코드는 스프링에서 생성이 완료된 빈 MyController을 불러와 DI된 service의 클래스명을 한번 출력하고
set 메서드를 통해 또 다른 ServiceB로 필드를 바꿔준 다시 클래스 명을 출력하는 예시 코드입니다.
결과는 다음과 같습니다. 보시는 바와 같이 불변해야 할 의존관계에 변하는 것을 알 수 있습니다.
이러한 특징 때문에 수정자 주입 방식은 되도로 지양하는게 좋습니다.
필드 주입
필드 주입은 말 그대로 필드에 바로 주입하는 방식을 뜻합니다.
@Getter
@Controller
public class MyController {
@Autowired
private ServiceA serviceA;
}
위와 같이 serviceA 필드 위에 @Autowired 어노테이션을 붙여주면 스프링에서 빈 생성 후 의존관계를 설정할 때 필드에 바로 주입하게 됩니다.
상대적으로 다른 의존관계 주입 방식에 비하여 코드가 간결해지는 특징이 있지만, 스프링 프레임워크에 종속적이고
외부에서 변경이 불가능하여 테스트 하기 힘든 치명적인 단점을 지니고 있습니다.
예를 들어 컨트롤러와 서비스가 같이 잘 작동하는지 통합 테스트를 하는 경우를 예로 들어보겠습니다.
@Getter
@Controller
public class MyController {
@Autowired
private ServiceA serviceA;
public void printControllerAndService() {
System.out.println("Controller =" + this + " Service =" + serviceA);
}
}
위와 같은 경우 생성된 Controller 빈과 주입된 필드 ServiceA의 빈 명을 출력하는 printControllerAndService() 라는
메서드가 있습니다. 만약 MyController가 ServiceA가 아닌 ServiceB와 통합 테스트를 하거나 Service 필드 자체를
Mock으로 생성하여 단위테스트를 하고 싶다면 어떻게 할까요?
필드 주입 방식으로는 이를 해결할 수 있는 방법이 없습니다. 프레임워크에 종속적이기 때문에 자바 코드 상으로 이를
바꿔줄 수 없기 때문입니다.
따라서 필드 주입또한 의존성 주입 방식으로 지양해야 될 방법입니다.
생성자 주입
생성자 주입은 호출 시점에 1번만 호출되는 것이 보장됩니다. 즉 의존성 주입 받은 필드 자체가 불변의 특징을 지닙니다.
@Getter
@Controller
public class MyController {
private final ServiceA serviceA;
@Autowired
public MyController(ServiceA serviceA) {
this.serviceA = serviceA;
}
}
생성자 주입은 위의 코드와 같이 생성자를 통해 ServiceA 빈을 주입 받게 됩니다. 또한 생성자가 1개일 경우
@Autowired의 생략이 가능합니다.
또한 다른 주입 방식과 다르게 필드에 final 키워드의 사용이 가능해집니다. 빈의 생성시에 의존관계 설정까지 완료되기
때문에 생성 뒤에 의존간계 주입이 일어나는 다른 방법과 다르게 final 키워드를 사용할 수 있습니다.
또한 생성자 주입 방식은 순수 자바 코드이기 때문에 테스트에 용이합니다.
정리하자면, 수정자 주입이 지니는 단점인 의존 관계의 가변성과 필드 주입이 지니는 테스트의 불편함을 해결 할 수 있는
방식이 생성자 주입 방식입니다.
필드 주입에서 하지 못한 다른 Service와의 테스트를 다음과 같이 할 수 있습니다.
@SpringBootTest
public class DependencyInjectionTest {
@Autowired ServiceA serviceA;
@Autowired ServiceB serviceB;
MyController myController;
@Test
void testServiceA() {
myController = new MyController(serviceA);
myController.printControllerAndService();
}
@Test
void testServiceB() {
myController = new MyController(serviceB);
myController.printControllerAndService();
}
}
ServiceA와 ServiceB는 빈으로 스프링에서 받고 각각의 테스트 케이스에서 MyController 객체를 새로 생성하여
직접 필드 값을 설정해주었습니다.
그 결과 MyController + ServiceA / MyController + ServiceB 와 같이 통합테스트를 각각의 Service에 맞게 나누어
진행할 수 있게 되었습니다. (참고로 ServiceB는 ServiceA를 상속받고 있습니다.)
위와 같이 생성자 주입은 의존간계의 불변성을 보장해주고 테스트의 편의를 보장해주기 때문에 의존관계 설정시에
생성자 주입 방식을 사용하는것이 좋습니다.
이상으로 의존관계 설정시에 왜 생성자 주입 방식을 써야 되는지에 대하여 살펴보았습니다.
글 읽어주셔서 감사합니다.
'Spring' 카테고리의 다른 글
AOP에 대하여 (2) | 2023.04.19 |
---|---|
Bean과 Component 뜯어보기 (0) | 2023.03.22 |
BeanFactory와 ApplicationContext의 차이점 (0) | 2023.02.21 |
스프링 빈의 쓰레드 안정성에 대하여 (0) | 2023.02.21 |
스프링 시큐리티 아키텍처에 대하여 (0) | 2023.01.29 |