DEV-STUDY/Spring

[Spring] 필드 인젝션을 피해야 하는 이유

HwangJerry 2023. 9. 30. 15:25

프로젝트를 진행하면서 각 도메인의 작업물을 stage 브랜치로 merge하고 나니 팀원 중 한 명이 필드 인젝션을 사용한 것을 확인할 수 있었다. 본능적으로 "어라? 왜 생성자 인젝션을 사용하지 않았지?"라는 생각이 들었는데, 스스로 필드 인젝션의 위험성에 대한 이유를 명확히 댈 수 없음을 깨달았다. 따라서 이번 기회에 정리하고자 한다.

 

의존성 주입 방법 3가지

스프링부트를 활용하면서 의존성을 주입하는 방법은 대표적으로는 3가지가 있다.

  • Setter 주입
  • 생성자 주입
  • 필드 주입

 

이는 @Autowired 어노테이션을 Setter, 생성자, 필드에 선언해주는 것으로 구현한다. 따라서 이 방법들을 알아보기 전에 @Autowired 에 대해 간단하게 짚고 넘어가고자 한다.

 

@Autowired란?

docs.spring.io에 따르면 다음과 같이 정의되어 있다.

Marks a constructor, field, setter method, or config method as to be autowired by Spring's dependency injection facilities. This is an alternative to the JSR-330 Inject annotation, adding required-vs-optional semantics.

출처: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/annotation/Autowired.html

중요한 포인트는 생성자, 필드, setter 메서드에 선언함으로써 스프링 컨텍스트가 해당 의존성을 인지한 뒤 Dependency Injection을 수행할 때, @Autowired로 선언된 방식을 수행할 수 있게 묶어준다는 것이다. 그러면 이제 각 방식에 대해 한번 알아보자.

 

Setter 주입

메서드에 @Autowired를 연결해두면, 파라미터에 있는 객체가 Spring Container의 일치하는 Bean으로 자동으로 연결되어 주입된다. (method는 public 일 필요는 없다.)

@Controller
public class ExampleController {
    private ExampleService service;
    
    @Autowired
    public void setService(ExampleService service) {
    	this.service = service;
    }
}

 

 

필드 주입

필드에 @Autowired를 선언해두면, Bean들이 생성된 직후, config 메서드들이 호출되기 전에 Spring Container에서 의존성과 일치하는 Bean이 주입된다. (필드 또한 public 일 필요는 없다.)

 

즉, 이는 객체를 기본생성자로 생성한 이후에 그 객체의 필드에 직접적으로 주입하는 방식이다. 하지만 이는 Spring Container에 강하게 결속되어 Spring container가 없으면 의존성이 주입되지 않는다. 따라서 Test case를 작성하는 경우, 불가피하게 무거운 스프링 컨테이너를 띄워야 하는 경우가 발생한다.

@Controller
public class ExampleController {
    @Autowired
    private ExampleService service;
    
    ...
    
}

필드 주입은 의존성을 클래스의 필드로 선언해두고, 이 필드에 해당하는 의존성을 스프링 컨텍스트에게 알아서 주입해달라고 명시하는 방식이다.  사실 어찌보면 가장 명료해보이는 이 방식은 많은 자바 개발자들에게 "아름다운 코드"라 여겨지며 한동안 지배적으로 사용되었다고 한다. 하지만 오늘날, 많은 개발자들이 필드 인젝션의 불안정성을 인지하고 이를 다시 생성자 인젝션으로 리팩하였다고 한다. (필드 인젝션이 갖는 리스크가 무엇인지는 생성자 주입에 대한 설명을 확인하면서 이해할 수 있다.)

 

생성자 주입

생성자에 @Autowired를 선언해두면, 해당 객체가 Bean으로 사용되어야 할 때 @Autowired가 선언된 생성자를 이용하여 객체를 생성하고 사용된다. 이 때, 메서드와 마찬가지로 파라미터에 선언된 의존 객체들은 마찬가지로 Container에 있는 일치하는 Bean들로 주입된다. 따라서 @Autowired가 선언된 생성자는 하나여야 하며, 만약 여러 생성자에 @Autowired를 선언해두고자 한다면 @Autowired(required = false)로 먹여두면 생성자 주입 방식에서 사용되는 후보 생성자로써 등록되게 된다. required attribute는 true가 default이므로, 별도로 설정하지 않으면 true로 설정된다. 만약 모든 생성자가 의존성 주입 방식에서 활용될 수 없는 상태라면 기본 생성자가 의존성 주입에 사용되게 된다. (생성자 또한 public일 필요는 없다.)

@Controller
public class ExampleController {
    private ExampleService service;
    
    @Autowired
    public ExampleController(ExampleService service) {
    	this.service = service;
    }
}

만약 생성자가 하나만 선언되었다면 그 생성자에 @Autowired가 기본적으로 선언되므로, 보통 final 을 이용하여 의존성들을 필드로 정의한 이후에 @RequiredArgsConstructor를 이용하여 생성자 인젝션을 선언한다.

 

생성자 주입을 사용해야 하는 이유

  1. setter 주입과 필드 주입은 둘 다 객체 생성 이후에 의존성을 주입하는 매커니즘이다. 하지만 생성자는 객체를 생성하는 시점에 의존성을 모두 주입하고 객체를 생성하게 된다. 따라서 컴파일 단계에서 의존성에 문제가 있음을 파악할 수 있고, 런타임에서 의존성으로 인해 NPE가 발생할 수 있는 위험을 최소화할 수 있다. (필드 주입과 setter 주입은 의존성이 런타임에 다이나믹하게 주입된다. 따라서 컴파일 타임에 문제를 파악할 수 없다는 특징이 있다.)
  2. NPE 문제와 같이 심각하게 다뤄지는 문제로 순환참조 문제가 있다. 객체를 bean으로 등록하는 시점에 의존성을 주입해두는 생성자 주입과 달리, setter 주입과 필드 주입은 Bean 등록 이후에 사용될 때 의존성을 주입하는 방식이므로 런타임에서 순환 참조 문제가 발생하기도 한다. 하지만 생성자 주입을 사용하면 이러한 문제가 발생할지를 컴파일 타임에서 확인할 수 있으므로 개발자가 원활히 이를 체크할 수 있다.
  3. 필드 인젝션과 setter 인젝션은 객체 생성 이후에 의존성을 주입받아야 하므로 해당 의존성들에 대하여 final로 선언할 수 없고, 이는 결국 의존 객체를 immutable 객체로 선언할 수 없다는 의미이다. 따라서 개발자는 필드 인젝션과 setter 인젝션으로 의존성 주입시에, 로직에 따라서 계속 다른 객체가 주입되어도 모른다는 의미이다. 이는 의도한 것이 아닌 이상, 안정성 측면에서 절대 권장되지 않는 흐름일 것이다.

 

최소한 유저가 아닌 엔지니어라면 적어도 사용하고 있는 기술에 대한 원리를 이해하고 사용해야 한다고 생각한다. 그렇지 않으면 자신이 사용하는 기술임에도 문제가 발생하였을 때 원인을 파악하는 것에 어려움을 겪을 것이고, 이는 결국 이슈 해결을 원활히 하지 못하는 결과로 이어지기 때문이다. 자신이 디자인한 프로덕트의 문제를 해결할 수 없는 엔지니어를 과연 엔지니어라고 불러줘도 되는 것일까? 나는 지금 엔지니어라고 불릴 수 있는가? 다시금 공부에 대한 열의를 다지게 된다.

 

참고:

https://wildeveloperetrain.tistory.com/139

https://shanepark.tistory.com/368

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/annotation/Autowired.html

https://alisyabob.tistory.com/327