좀 해묵은 주제일 수 있는데, 기존 코드의 리팩토링 준비 중에 문득 생각나서 끄적여 봅니다.

Spring 의존성 주입은 무조건 생성자 주입방식으로 작성해야 한다?

스프링을 사용하면서 가장 많이 접하는 코드는 의존성 주입에 관련된 @Autowired 어노테이션일 겁니다.

스프링 컨네이너에서 컴포넌트 스캔을 통해 빈으로 등록된 객체의 인스턴스를 @Autowired 어노테이션을 통해서 사용하고자 하는 객체에 주입시켜 줍니다.

 

객체의 생성과 객체간의 의존관계를 스프링 컨테이너가 해주는 거죠.

서로 의존관계에 있는 객체가 상대객체의 생성에 직접 관여하지 않고 컨테이너가 대신 함으로써
객체간의 결합도를 최대한 떨어뜨리는 게 스프링 IoC(Inversion of Control)의 핵심 개념입니다.

 

이전까지는 객체의 생성과 소멸 등의 생명주기를 그 객체를 사용하는 클래스에서 관리를 했다면, 이제는 스프링 컨테이너가 모두 일임하게 되었습니다.

 

우리는 컨테이너가 그 객체들을 스캔할 수 있도록 @Autowired만 붙여주면 됩니다. 물론 그 객체는 스캔이 가능한 컴포넌트로 등록되어 있는 빈 객채여야 합니다. 스프링을 사용하는 가장 큰 이유(이점) 중에 하나입니다.

 

Spring에서 지원하는 의존성 주입방식은 3가지입니다.

  1. 필드 주입 -  field based injection
  2. 수정자 주입 -  setter based injection
  3. 생성자 주입 -  constructor based injection

 

필드 주입 방식

@Service
public class UserService {
  @Autowired private UserRepository userRepository;
}

수정자(Setter) 메서드 주입방식

@Service
public class UserService {
  private UserRepository userRepository;

  @Autowired
  public void setUserRepository(UserRepostory userRepository) {
    this.userRepository = userRepository;
  }
}

생성자 메서드 주입방식

@Service
public class UserService {
  private UserRepository userRepository;

  @Autowired
  public  UserService(UserRepository userRepository) {
    this.userRepository = userRepository   
  }
}

참고로 Spring4.3 버전 이후부터는 수정자와 생성자 주입방식에서 @Autowired 어노테이션을 생략할 수 있습니다.

 

아마 많은 분들이 별 다른 고민 없이 필드 인젝션 방식으로 의존성 주입을 사용했을 텐데,
언제부터인가 필드 인젝션 방식의 문제점에 대해서 여러 개발자 블로그에 올라오면서 필드 인젝션은 의존성 주입의 bad case가 되어 버렸습니다.

 

그리고 인텔리J와 같은 IDE툴에서도  필드 인젝션을 사용한 코드에 대해서 경고 메시지를 뿌려주기 시작했습니다.

그럼 왜 필드 인젝션은 사용하지 말아야 할 방식이 된 걸까요?

 


확인해야 할 포인트

  • 필드 인젝션의 문제점은 무엇인가? 정말 문제점인가?
  • 과연 필드 인젝션 방식으로 적용된 코드들로 인해 실제로 개발과정이나  서비스 운영 중에 이슈가 될 만한 케이스들이 많이 발생하는가?
  • 수정자 또는 생성자 인젝션으로 변경하면 1번의 문제점이 모두 해결되는가?

한 가지 명확하게 짚고 넘어가야 할 것은, 필드 인젝션을 ‘’권장하지 않는다’’이지 ‘사용하면 안 된다’가 아닙니다.
그냥 사용해도 됩니다.

 

필드 인젝션의 문제점이라고 많이 얘기하는 내용 중에 대표적인 두 가지 문제점입니다.

  • 주입받은 빈의 초기화(인스턴스화)를 보장하지 않음
  • 주입받은 빈의 변경이 가능

첫 번째, 주입받는 빈이 NPE를 일으킬 가능성이 있는 얼마나 될까요?

스프링 컨테이너가 컴포넌트 스캔을 통해 @Autowired가 붙어있는 빈객체의 주입시켜 줄텐데, 사용할 클래스에서 빈을 임의로 Null로 변경하지 않는 이상, 사실 주입받은 빈 객체가 NPE를 일으킬 가능성은 없습니다.

 

실제로 코딩을 하면서 필드 인젝션 방식으로 인해 주입받은 인스턴스를 참조할 때 초기화 이슈 등으로 NPE를 경험한 적은 한 번도 없었습니다. 주입받을 클래스는 스프링 컨테이너에서 빈 객체로 등록될 수 있는 상태여야 하기 때문에 애플리케이션이 구동될 때 빈으로 등록되는 과정에서 초기화가 제대로 이루어지지 않으면 빈 등록 자체가 되지 않습니다. 

 

두 번째, 주입받은 빈의 변경이 가능한 문제는 필드 인젝션 만의 문제가 아니라, 수정자/생성자 인젝션 방식도 동일한 문제를 일으킵니다.

너무 당연한 얘기이죠. 어떤 방식이 되었든 사용하고자 하는 클래스에서 주입받은 객체는 임의로 변경을 할 수 있습니다.

 

단, final로 선언된 빈의 경우에는 변경이 불가능합니다.

생성자 인젝션인 권장되는 이유 중에 하나가 필드 인젝션 또는 수정자 인젝션의 경우 final로 의존성 주입이 불가능하고, 생성자 인젝션을 통해서만 가능하기 때문입니다.

빈 객체의 ‘immutable’ 보장할 수 있는 유일한 주입방식인 거죠.


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

확실한 것은, 생성자 방식으로 객체의 의존성을 관리하게 되면, 의존성 객체가 추가되거나 삭제될 때 필드 인젝션보다는 코딩해야 하는 부분이 많아집니다. 생성자 메서드의 매개변수를 추가 또는 삭제해줘야 하며, 생성자 메서드 내의 소스도 수정이 필요합니다.

 

코드 한 줄 추가하거나 삭제하면 되는 필드 인젝션 방식과 비교하면 분명 번거로운 일입니다. 

 

어떨 때는 매개변수에만 추가하고 빈에 할당하는 코드를 빼먹는 경우도 있습니다.

물론  Lombok라이브러리를 사용하면 간단히 처리할 수 있습니다. 아래에서 간단히 언급하였습니다.

 

이 부분이 제 개인적인 생각으로는 의존성 관리(추가, 삭제)의 번거로운 부분이 역설적으로 생성자 주입방식을 사용해야 하는 이유 중 하나라고 생각합니다. 

 

 

생성자 방식을 사용해야 하는 이유, 아니 사용을 권장하는 이유는 사실 이런 이유이지 않을까 생각합니다. 2번은 위에서 언급했던 의존성 주입 작업의 번거로움이 그 이유라고 언급했던 내용과 일맥상통하는 부분이 있습니다. 

 

  • final 선언이 가능해지기 때문에 주입받은 클래스에서 임의로 빈 객체를 변경하는 것을 방지할 수 있습니다.
  • 무분별한 의존성 주입에 대한 나쁜 코드를 쉽게 들어낼 수 있는 방식입니다. 생성자의 매개변수(주입받을 객체)가 10개 정도 된다고 생각해보면 어떤 코드 일지 상상이 가실 겁니다. 무조건 리팩터링을 검토해봐야 하는 수준이겠죠.
  • 애플리케이션 구동 단계에서 상호 순환 참조 오류를 미리 발견할 수 있습니다.
  • DI의존성을 제거 POJO기반의 독립적인 객체로 테스트를 수행할 수 있습니다.

1번의 경우, 주입받은 객체를 임의로 변경하는 경우는 실제 서비스 구현 시 거의 일어나지 않는 케이스일 것 같습니다. 그렇게 해서도 안 됩니다. 그런 리스크를 원천적으로 차단할 수 있는 방법입니다.

 

2번의 경우, 주입받는 객체가 1,2개일 때는 그냥 필드 인젝션이나 수정자 인젝션을 사용해도 상관없다고 생각합니다.

의존관계가 1:1, 1:2 정도라는 것은 객체 간의 의존관계가 잘 설계되었다는 의미이고, 굳이 기존 코드들을 생성자 주입방식으로 변경할 필요가 없다고 봅니다.

 

 

클래스의 설계 원칙인 단일 책임원칙(SRP: Single-Responsibility Principle)을 위배할 정도로 의존성 관계가 많아진다면, 필드 인젝션의 경우 그 의존관계가 잘 드러나 지지 않기 때문에 생성자 주입방식을 사용함으로써 그 관계를 잘 드러낼 수 있습니다. 

 

생성자 메서드의 매개변수를 보면 얼마나 많은 의존성 객체들을 주입받았는지 확인할 수 있습니다.


필드 인젝션으로 그런 의존성 관계가 잘 드러나지 않는다는 문제는 사실, 코드가독성 범위의 문제이지 그 이상으로 필드인젝션 방식이 문제가 있다고 얘기해서는 안 됩니다.

 

아이러니한 것은, 생성자 주입방식을 쓸 때 코드 작성을 최소화하기 위해서 Lombok의 @RequiredArgConstructor 어노테이션을 많은 분들이 사용할 텐데, 이렇게 되면 방금 언급한 의존 성관계를 잘 드러낼 수 있는 생성자 주입방식의 장점이 사라져 버립니다.

 

lombok 어노테이션이 생성자 메서드를 컴파일 시 만들어 주기 때문에 코드에서는 드러나지 않게 되는 거죠.

 


정리하면,
생성자 주입방식은 필드 인젝션, 수정자 인젝션 방식보다
  - 좀 더 변경에 닫혀있는  safety 한 코드(사실은 거의 일어나지 않는 이슈)의 작성과
  - 애플리케이션 구동 시의 객체의 순환 참조 여부를 확인할 수 있는  

장점 정도의 수준으로 그 사용이 더 바람직하다는 접근이 필요하지 않을까 하는 개인적인 생각입니다.

 

더불어 테스트 코드 작성 시, 특정 DI 프레임워크에 의존하지 않고 POJO기반으로 작성이 가능하다는 장점도 있겠네요.
그런데 이 장점이 개발과 테스트 작업에 많은 영향을 줄 만큼 필드 인젝션 방식을 지양해야 할 이유라고 생각되지 않습니다.

 

생성자 인젝션 방식이 필드인젝션 방식보다 무조건 더 좋다는 접근은 피해야 하지 않을까 생각합니다.

 

미뤄뒀던 코드 리팩터링을 하면서,
필드 인젝션을 생성자 인젝션 방식으로 변경하는 와중에 문득 든 잡생각이었습니다. 

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기