본문 바로가기
개발/Spring

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

by baau 2025. 1. 13.

Spring Framework는 @Autowired 애노테이션을 통해 다양한 의존성 주입을 제공한다. 

  • 생성자 주입
  • 필드 주입
  • 수정자 주입
  • 일반 메서드 주입

우선 많은 개발자가 생성자 주입을 권장하고, 사용할 것이다.

하지만 우리 회사 코드에서는 필드 주입으로 되어 있는 코드들도 있다. 수정자 주입이나 일반 메서드 주입은 아예 없었던 것 같고, 생성자 주입과 필드 주입이 섞여있다.

 

필드 주입을 보면 생성자 주입으로 리펙터링을 하고 있고, 저뿐만 아니라 모든 팀원이 변경하고 있어 생성자 주입과 필드 주입이 공존하는 걸로 예상된다.

 

이전 개발자는 왜 필드 주입을 사용했을까?? 간결하기 때문인 것 같다. 실무에서는 한 객체가 여러 객체를 의존성 주입을 받는 경우가 있다.

만약 의존성 주입을 받는 객체가 10개 이상이라고 가정해 보자. 생성자 주입은 10개의 파라미터를 가지는 생성자가 있을 것이고, 수정자 주입은 10개 이상의 setter 메서드가 있을 것이다. 두 경우 코드가 의존성 주입을 받기 위한 코드들이 너무 많다. (롬복을 사용하지 않을 경우)

필드 주입은 필드 위에 @Autowired 만 붙이면 되니 간결하다.

 

하지만, 필드 주입의 단점은 너무 명확하고 생성자 주입을 통해 개발자가 얻을 수 있는 장점이 많다. 서론이 길어졌는데,.. 이번 글에서는 생성자 주입을 사용해야 하는 이유에 대해서 정리하려고 한다.

 

생성자 주입은 말 그대로 생성자를 통해서 의존 관계를 주입받는 방법이다.

@Service
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

    @Autowired
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    
    ...
 }

 

 

생성자 주입을 사용하면 아래의 장점을 얻을 수 있다.

  • 불변한 설계 가능
  • 순환 참조 방지
  • final 키워드 사용
  • 스프링에 의존적이지 않는 테스트 코드 작성
  • 롬복과 @Autowired의 특징을 통해 간결하게 사용

 

1. 불변하게 설계가 가능하다.

대부분 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 거의 없다.

  • 생성자 주입은 객체 생성 시 딱 1번만 호출됨을 보장하고, 이후에는 호출될 가능성이 없어 불변하게 설계가 가능하다.
  • 하지만, 수정자 주입이나 일반 메서드 주입의 경우 불필요하게 수정 가능성을 열어두기 때문에 유지보수성이 떨어진다.

 

2. 순환 참조를 방지할 수 있다.

생성자 주입은 다른 의존성 주입과 다르게, 스프링 컨테이너가 빈을 생성하는 시점에 의존성 주입도 같이 일어난다.

따라서 애플리케이션 구동 시점에 순환 참조 이슈를 확인할 수 있다.

 

MemberService와 BookService는 서로 의존하고 있어, 애플리케이션 구동 시 아래와 같은 오류를 확인할 수 있다.

@Service
@RequiredArgsConstructor
public class MemberService {

    private final BookService bookService;
}

@Service
@RequiredArgsConstructor
public class BookService {

    private final MemberService memberService;

}

 

생성자 주입이 아닌 다른 의존성 주입은 스프링 컨테이너가 빈을 생성하는 시점과 의존성 주입하는 시점이 분리되어 있기 때문에 애플리케이션 구동 시에 순환 참조 이슈를 확인할 수 없었다.

 

하지만, 스프링 부트 2.6 이후부터는 순환 참조가 기본적으로 허용되지 않아, 생성자 주입이 아닌 다른 의존성 주입에서도 애플리케이션 구동 시점에 순환 참조 이슈를 확인할 수 있다.

 

3. final 키워드

  • final 키워드를 사용하면, 컴파일 시점에 의존성이 누락되었는지 확인할 수 있다.
  • 또한 의존성 주입 객체의 재할당도 방지할 수 있다.

생성자 주입을 제외한 다른 의존성 주입은 모두 빈을 생성한 이후에 의존성 주입이 이루어지기 때문에, final 키워드를 사용할 수 없다.

오직 생성자 주입만 final 키워드를 사용할 수 있다.

 

4. 스프링에 의존적이지 않는 테스트 코드 작성

테스트 코드는 가능한 순수 자바로 테스트를 작성하는 것이 좋지만, 생성자 주입 외 다른 의존성 주입의 경우 순수한 자바 코드로 단위 테스트를 작성하기 어렵다.

 

필드 주입 방식의 코드에 대한 테스트 코드이다.

@Service
public class MemberService {

    @Autowired private BookService bookService;
    @Autowired private OrderService orderService;
}
class MemberServiceTest {

    @Test
    public void test() {
        MemberService memberService = new MemberService();
        memberService.logic();
    }
}
  • @SpringBootTest가 아니기 때문에, MemberService는 의존성 주입을 받지 못한다. 따라서 memberService.logic() 호출 시 NPE가 발생한다.
  • 또한 필드 주입의 경우 외부에서 변경이 불가능하기 때문에, Mock 객체를 주입할 수도 없다.
  • 스프링 컨테이너 없이는 아무것도 할 수 없어, 어쩔 수 없이 setter를 열어야 한다.

 

하지만, 생성자 주입의 경우 컴파일 시점에 객체를 주입받아 테스트 코드를 쉽게 작성하거나 Mock 객체를 주입할 수 있다. 또한 주입하는 객체가 누락될 경우에도 컴파일 서점에 발견할 수 있다.

 

마지막으로,

롬복과 @Autowired 가 생성자가 딱 하나일 경우 생략할 수 있는 특징을 활용하여, 생성자 주입을 필드 주입보다 더 편리하고 간결하게 사용할 수 있다.

@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;
    private final OrderService orderService;
    private final CouponService CouponService;
    ...
    
}
  • @Autowired 는 생성자가 딱 1개만 있는 경우 생략할 수 있다.
  • @RequiredArgsConstructor 를 사용하면 final 이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.

다른 의존성 주입은 final 키워드를 사용하지 못하고, 롬복을 사용하여 간결하게 코드를 작성할 수 없다.