생성자 주입은 생성자를 이용해서 의존 관계를 주입하는 방법압니다.
private UserRepository userRepository;
private MemberService memberService;
@Autowired
public UserService(UserRepository userRepository, MemberService memberService) {
this.userRepository = userRepository;
this.memberService = memberService;
}
생성자 주입은 생성자가 1회만 호출 되는 것을 보장합니다. 그렇기 때문에 주입받은 객체는 변하지 않으며, 객체의 주입이 필요한 시점에 사용할 수 있습니다. 또한 Spring 프레임워크에서는 생성자 주입을 적극 지원하고 있기 때문에 위의 코드처럼 생성자가 1개만 있을 경우 @Autowired를 생략하더라도 주입이 가능하게 편의성을 제공합니다.
따라서 위의 코드는 하단의 코드와 같은 코드가 됩니다.
private UserRepository userRepository;
private MemberService memberService;
public UserService(UserRepository userRepository, MemberService memberService) {
this.userRepository = userRepository;
this.memberService = memberService;
}
하지만 생성자 파라미터가 늘어나면 코드의 가독성을 떨어뜨릴 수 있는데 이는 Lombok 의 @RequiredArgsConstructor 와 final 키워드를 활용하여 해결할 수 있습니다.
수정자 주입은 필드 값을 수정하는 Setter 를 사용해서 의존 관계를 주입하는 방법입니다. Setter 주입은 생성자 주입과 다르게 객체의 변경 가능성이 있을 경우에 사용합니다.
private UserRepository userRepository;
private MemberService memberService;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
스프링 초기에는 getX , setX 등 프로퍼티를 기반으로 하는 자바 기본 스펙 때문에 수정자 주입을 많이 사용했었는데, 시간이 지나고 난 뒤 수정자 주입이 아닌 다른 방식을 사용합니다.
수정자 주입은 필요한 의존 객체만 주입할 수 있어 많은 의존성을 세밀한 제어를 할 수 있으며 메모리를 최적화할 수 있습니다. 하지만 의존성이 누락될 수 있으며 메서드가 외부로 노출될 수 있습니다. 또한 객체의 생성과 주입이 떨어져 있기 때문에 코드의 가독성이 떨어질 수 있습니다.
필드 주입은 필드에 바로 의존 관계를 주입하는 방법입니다.
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private MemberService memberService;
}
필드 주입은 코드가 간결해지는 장점이 있습니다. 하지만 클래스 내부의 의존성을 숨기므로 코드의 가독성이 떨어질 수 있으며, 의존성 오류를 컴파일 시점에서 발견할 수 없고 런타임 시점에서 오류를 발견합니다.
실제로 개발을 하다보면 의존 관계를 변경해야 하는 경우가 거의 없습니다. 하지만 수정자 주입을 이용해서 불필요하게 수정의 가능성을 열어두면 유지보수성을 떨어뜨릴 수 있습니다. 따라서 생성자 주입을 사용해서 변경 가능성을 배제해 불변성을 보장하는 것이 좋습니다.
생성자 주입은 컴파일 시점에 객체를 생성하여 의존성을 주입하기 때문에 누락된 의존성에 대한 오류를 컴파일 시점에 발견할 수 있습니다.
생성자 주입을 사용하면 컴파일 시점에 객체를 주입받아 테스트 코드를 작성할 수 있으며, 의존성 없는 객체를 쉽고 빠르게 테스트할 수 있습니다.
만약 @Autowired를 사용한다면 필요한 의존성을 등록하고 초기화 하기 때문에 테스트의 비용이 증가하게 됩니다.
생성자 주입을 사용하면 필드 객체에 final 키워드를 사용할 수 있습니다. 반면 다른 방법들은 객체의 생성 ( 생성자 호출 ) 이후에 호출되므로 final 키워드를 사용할 수 없습니다.
또한 final 키워드를 붙이면 Lombok과 결합하여 코드를 간결하게 작성할 수 있습니다. Lombok은 final 변수를 위한 생성자를 대신 생성해주는 @RequiredArgsConstructor
를 이용하여 코드를 간결하게 작성할 수 있습니다.
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final MemberService memberService;
public void register(String name) {
userRepository.add(name);
}
}
이러한 코드는 Spring에서 생성자 주입을 사용할 시 생성자가 1개라면 @Autowired를 생략해도 주입해 주기 때문에 코드를 간결하게 작성할 수 있습니다.
import org.springframework.beans.factory.annotation.Autowired;
// 스프링 의존성이 UserService에 import되어 코드로 박혀버림
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private MemberService memberService;
}
사용자의 비즈니스 로직을 처리하는 Service 계층에서 프레임워크 코드가 침투하는 것은 큰 문제는 아니지만, 생성자 주입을 사용해서 스프링 코드가 없는 유연한 코드를 작성하는 것이 더 좋습니다.
프레임워크는 비즈니스 로직을 작성하는 서비스 계층에서 알아야 할 대상이 아닙니다.
생성자 주입을 사용하면 컴파일 시점( 객체 생성 시점 )에 순환 참조 에러를 예방할 수 있습니다.
그 이유는 Bean을 등록하기 위해 객체를 생성하는 도중 다음과 같은 순환 참조가 발생하기 때문입니다.
new UserService(new MemberService(new UserService(new MemberService()...)))
@Autowired 는 빈의 생성과 조립 시점이 분리되어 있기 때문에 컴파일 시점에 발견되지 않습니다. 반면 생성자 주입은 빈의 생성과 조립이 동시에 이루어 지다보니 위의 에러를 사전에 잡을 수 있습니다.
참고로 스프링부트 2.6부터는 순환 참조가 기본적으로 허용하지 않으므로 해당 내용은 스프링부트 2.6 이하의 버전에서 오류가 발생합니다.
참고 블로그 1 : https://mangkyu.tistory.com/125
참고 블로그 2 : https://alisyabob.tistory.com/327