Q. 생성자 주입 외에, 다른 방법으로 의존관계를 주입할 수 있나요?
A. 다른 방법도 가능하지만, 생성자 주입을 권장합니다.
안녕하세요.
스프링에서는 의존관계를 주입하는 4가지 방법을 지원합니다.
4가지는 생성자 주입, 수정자 주입(setter 주입), 필드 주입, 일반 메서드 주입입니다.
이 중 생성자 주입이 가장 권장됩니다.
한 개씩 자세히 알아보고, 생성자 주입을 사용해야 하는 이유와 편리한 생성자 주입을 지원하는 어노테이션을 알아보겠습니다.
생성자 주입
생성자 주입은 가장 많이 사용되는 방법이며, 이름 그대로 생성자를 통해서 의존 관계를 주입 받는 방법입니다.
생성자 위에 @Autowired를 명시해주면 됩니다.
예제 코드를 봅시다.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired // 생략 가능
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
스프링은 @Component를 보고 등록해야 함을 인지하고, 생성자를 호출합니다. 생성자를 호출할 때 @Autowired가 있으면 스프링 컨테이너에서 필요한 객체를 꺼내서 주입해줍니다.
필드를 final 변수로 사용하는 것을 볼 수 있습니다. final 변수는 선언과 동시에 값을 할당해야 합니다[2]. private final int score = 0; 처럼요. 그렇지 않다면, 생성자에서 반드시 초기화 해야 합니다. 생성자는 객체를 생성하기 위해 반드시 거쳐야 하는 과정이고, 여기서 상수를 초기화를 하고 있다면 상수가 확실히 초기화됨을 보장하기 때문입니다. 생성자 주입을 사용하는 경우, 의존관계를 주입해야 할 필드를 final로 사용해서 값이 변하지 않음을 보장하고, 생성자에서 의존관계를 주입해줍니다.
생성자 주입은 중요한 특징 2가지가 있습니다.
첫 번째는 생성자 호출 시점에 딱 1번만 호출되는 것이 보장된다는 것이고, 두 번째는 불변, 필수 의존관계에 사용된다는 것입니다. 객체의 생성자는 당연히 한 번만 호출됩니다. 따라서 생성자를 통해서만 의존 관계가 주입되고, 외부에서는 변경하지 못하게 막을 수 있습니다. final 키워드를 사용할 수 있죠. 의존 객체 인스턴스가 변하지 않음을 보장할 수 있습니다. 그리고 객체를 생성할 때, 생성자를 부르기 위해서 반드시 의존 객체들이 주입되어야 하므로 필수적인 의존관계에 사용할 수 있습니다.
그리고 클래스에 생성자가 딱 1개만 있으면 @Autowired를 생략해도 의존관계가 자동으로 주입됩니다. 위의 예시 코드에서도 생성자 위의 @Autowired를 생략해도 자동 의존관계 주입이 동작합니다.
수정자 주입(setter 주입)
다음으로, 수정자 주입입니다. 수정자 메소드(setter)를 사용해서도 의존 관계를 주입할 수 있습니다.
setter함수 위에 @Autowired를 명시하면 됩니다.
예시 코드를 봅시다.
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
의존 관계를 주입받아야 하는 필드의 setter 메소드를 만들고, 그 위에 @Autowired를 명시합니다.
스프링은 @Component를 보고 객체를 생성합니다. 현재 코드에는 생성자가 없으니 default constructor로 객체가 만들어질 것입니다. 스프링 빈이 다 등록된 다음에, 스프링은 @Autorwired가 있는 setter를 호출해서 의존 관계를 주입합니다. 즉 스프링 빈 등록과 의존 관계 주입이라는 두 사이클이 따로 작동하는 것입니다.
참고로 생성자 방식은 두 사이클이 나뉘지 않고 함께 동작합니다. 스프링 빈에 등록할 객체를 만들기 위해 생성자를 부를 때 주입될 객체가 필요하기 때문입니다.
수정자 주입의 특징은 선택, 변경 가능성이 있는 의존 관계에 사용한다는 것입니다. 먼저 '선택'은 멤버 변수가 등록되지 않아도 됨을 의미합니다. 이를 위해서 @Autowired(required=false)로 지정하면 됩니다. 다음으로 '변경'은 의존 관계가 주입된 객체를 바꾸고 싶으면 외부에서 public인 setter 함수를 호출하여 변경할 수 있음을 의미합니다.
생성자 주입은 불변, 필수 의존 관계에, 수정자 주입은 선택, 변경 의존 관계에 사용합니다.
보통은 생성자 주입을 더 많이 사용합니다. 스프링 빈은 등록되면 여러 클라이언트가 사용하는 싱글톤으로 유지되기 때문에, 최대한 변경하지 않고 필드의 값을 다 주입하기 위해서 생성자 주입을 사용하는 것이겠죠.
강의해주신 김영한 개발자님께서는 '불변'이 중요하다고 하셨습니다. 제약은 좋은 개발 습관이라고 말씀해주셨습니다. 필요한 객체로 의존 관계를 구성해서, 낭비가 없이 코드를 설계하는 것이 바람직하기 때문에 생성자 주입을 활용하는 것이 아닐까 생각해보았습니다.
필드 주입
필드 주입은 이름 그대로 필드에 바로 주입하는 방법입니다.
코드를 봅시다.
@Component
public class OrderServiceImpl implements OrderService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
}
필드에 @Autowired를 붙이면, 스프링이 자동으로 의존 관계를 주입해줍니다!
코드가 간결하기 때문에 사용이 용이하지만 두 가지 문제점이 있습니다.
첫 번째는, 외부에서 변경이 불가능해서 테스트하기 힘들고 두 번째는, DI 프레임워크가 없으면 아무것도 할 수 없습니다. 하나씩 알아봅시다.
첫 번째로, 필드 주입은 외부에서 변경이 불가능해서 테스트하기 어렵습니다.
두 번째로, DI 프레임워크가 없으면 아무것도 할 수 없습니다.
DI 프레임워크는 의존관계를 주입해주는 스프링과 같은 프레임워크를 의미합니다.
만약 스프링이 없다면, 필드 주입으로는 의존관계를 주입할 수 없습니다.
예를 들어서, orderService 객체는 memberRepository와 discountPolicy 객체를 주입받아야 한다고 생각해봅시다.
스프링 컨테이너를 띄워서 getBean으로 객체를 등록받으면 상관이 없지만,
스프링 컨테이너를 사용하지 않는 순수 자바 코드로 실행하는 테스트를 한다면 필드 주입은 문제가 발생합니다.
생성자 주입은 new를 써서 생성자를 만들고, 의존 관계를 직접 주입해주면 됩니다.
하지만 필드 주입은 의존 관계를 직접 주입할 수 없습니다.
아래 테스트 코드를 보시면 이해가 쉬우실 것입니다.
public class injectionTest {
@Test
void constructorInjection(){
MemberRepository memberRepository = new MemoryMemberRepository();
DiscountPolicy discountPolicy = new RateDiscountPolicy();
OrderService orderService = new OrderServiceImpl(memberRepository, discountPolicy);
orderService.printRepository(); // MemberRepository = hello.core.member.MemoryMemberRepository@4988d8b8
}
@Test
void fieldInjection(){
OrderService orderService = new OrderServiceImpl();
orderService.printRepository(); // MemberRepository = null
}
}
위의 테스트 코드는 스프링을 사용하지 않는 순수 자바 테스트코드입니다.
위의 constructorInjection 테스트는 생성자 주입 방법으로 의존 관계를 주입하고 테스트한 것이고, 아래의 fieldInjection 테스트는 필드 주입 방법으로 의존 관계를 주입하고 테스트한 것입니다.
생성자 주입 방법을 사용하면, 테스트 코드에서 객체를 만들고 의존 관계를 주입할 수 있지만, 필드 주입 방법으로는 불가능합니다. MemberRepository 필드가 null입니다. 원하는 기능을 제공할 수 없겠죠.
참고로 이런 식으로 특정 클래스를 스프링 컨테이너의 도움 없이 해당 단위로 테스트하는 것을 단위 테스트라고 부르고, 실무에서는 이런 단위 테스트를 많이 진행한다고 합니다[3]. 스프링을 띄우는 것보다 빠르고 간단하므로 테스트가 용이하기 때문입니다. 필드 주입을 사용하면 이런 테스트를 할 수 없습니다.
일반 메소드 주입
일반 메소드를 통해서도 의존 관계를 주입 받을 수 있습니다.
메소드 위에 @Autowired를 붙이면 되는데요, 코드를 살펴봅시다.
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
수정자 주입과 유사하죠. 하지만 한 번에 여러 필드를 주입받을 수 있다는 차이점이 있습니다.
일반적으로 일반 메소드 방식은 잘 사용하지 않습니다.
생성자 주입의 장점
과거에는 수정자 주입과 필드 주입을 많이 사용했지만, 최근에는 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장합니다. 그 이유는 3가지 불변, 누락, final 키워드입니다. 하나씩 자세히 알아봅시다.
1. 불변
대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료 시점까지 의존관계를 변경할 일이 없습니다. 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안 됩니다. 수정자 주입을 사용하면, setter 메소드를 public으로 열어두어야 하고 누군가 실수로 변경할 수 있습니다. 생성자 주입은 객체를 생성할 때 한 번만 호출되므로 이후에 호출되는 일이 없고, 불변하게 설계할 수 있습니다.
2. 누락
스프링 프레임워크 없이 순수한 자바 코드로 단위 테스트를 할 때, 수정자 주입과 필드 주입은 NPE(Null Point Exception)가 발생합니다. 필드 주입은 앞서 null이 나오는 것을 살펴봤고, 수정자 주입은 객체를 생성할 때 의존관계가 주입되지 않으므로 set을 누락하면 당연히 null이 나옵니다. null 값으로 인한 Null Point Exception은 런타임 에러입니다.
그러나 생성자 주입을 사용하면 주입 데이터를 누락했을 때 컴파일 오류가 발생하고, IDE에서 바로 주입해야 하는 값을 알려줍니다. 컴파일 오류는 세상에서 가장 빠르고, 좋은 오류입니다! 따라서 생성자 주입은 누락을 방지할 수 있는 장점이 있습니다.
3. final 키워드
앞서 살펴보았듯이 생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있고, 생성자에서 값이 설정되지 않으면 컴파일 에러를 날려줍니다. 수정자 주입과 필드 주입은 생성자 호출 이후에 호출되므로, 필드에 final 키워드를 사용할 수 없습니다. final은 또한 값이 변하지 않도록 보장해주는 역할을 합니다.
따라서 기본적으로 생성자 주입을 사용하고, 필수 값이 아닌 경우 수정자 주입을 사용하는 것이 권장됩니다.
@RequiredArgsConstructor
생성자 주입 코드는 필드 주입보다 길다는 단점이 있습니다. 생성자를 만들어야 하기 때문입니다.
위에서 본 생성자 주입 코드를 다시 봅시다.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired // 생략 가능
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
생성자 부분 코드를 작성하기가 번거롭죠. 이를 하나의 어노테이션으로 대체할 수 있다면 얼마나 좋을까요?
@RequiredArgsConstructor 어노테이션이 이를 지원합니다!
@RequiredArgsConstructor는 final이 붙은 필드를 모아서 생성자를 자동으로 만들어줍니다.
최종 결과는 다음과 같습니다!
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
정말 간단하죠! @RequiredArgsConstructor는 롬복 라이브러리가 제공하는 어노테이션이기 때문에, 롬복을 설치해주셔야 합니다.
여러 사이트에서 롬복 설치 방법을 설명하고 있는데, 인텔리제이를 사용한다면 아래 게시글에 상세한 설명이 있어서 참고하시면 좋을 것 같습니다!
https://gre-eny.tistory.com/303
[IntelliJ] Lombok 설치 및 Lombok Annotation
Lombok 이란? 롬복(Lombok) 은 자바 Domain(DTO, VO) 에서 반복적으로 작성되는 getter/setter나 toString, 생성자 코드 등의 소스들을, 어노테이션(Annotation)을 사용하여 생략할 수 있도록 컴파일 시점 에 자동..
gre-eny.tistory.com
인프런 '스프링 핵심 원리 - 기본편' 강의를 듣고 공부하며 정리한 자료입니다.
잘못된 부분은 피드백 주시면 감사하겠습니다!
글 읽어주셔서 감사합니다:-)
참고자료
[1] 스프링 핵심 원리 - 기본편, 섹션 7. 의존관계 자동 주입 https://www.inflearn.com/course/스프링-핵심-원리-기본편
[2] https://www.inflearn.com/questions/305707
[3] https://www.inflearn.com/questions/72092
'Spring > 스프링 핵심 원리 - 기본편' 카테고리의 다른 글
8-1. 빈 생명주기 콜백 (0) | 2022.01.10 |
---|---|
7-2. 의존 관계 주입 조회 빈이 2개 이상일 때 (0) | 2022.01.09 |
6-1. 컴포넌트 스캔 (0) | 2022.01.07 |
5-2. 싱글톤 방식의 주의점: stateless로 설계하기 (0) | 2022.01.07 |
5-1. 싱글톤 컨테이너 + static method (0) | 2022.01.06 |
댓글