참고
Spring core basic 시리즈는 김영한 님의 "스프링 핵심 원리 - 기본편" 강의를 정리한 글입니다. 글에 첨부된 사진은 해당 강의의 강의 자료에서 캡쳐한 것입니다. 제 Github에만 올려뒀다가, 정보 공유와 강의 홍보(?)를 위해 블로그에도 업로드합니다. 마크다운을 잘 쓰지 못해서 가독성이 조금 떨어지는 점 양해 바랍니다.
스프링 핵심 원리 - 기본편 - 인프런 | 강의
스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...
www.inflearn.com
생성자 주입(Constructor injection)을 선택하라!
과거에는 setter injection과 field injection을 많이 사용했지만, 최근에는 스프링을 포함한 DI 프레임워크 대부분이 constructor injection을 권장한다. 그 이유는 다음과 같다.
1. 의존관계의 불변성
- 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다. 아니, 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안된다 (불변해야 한다).
- Setter injection을 사용하면, setXxx 메서드를 public으로 열어두어야 한다. 하지만 불변해야 하는 의존관계를 setter 호출을 통해 변경이 가능하게 열어두는 것은 좋은 설계 방법이 아니다.
- 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다. 따라서 불변하게 설계할 수 있다.
2. 의존관계 주입 누락 가능성 제거
프레임워크 없이 순수한 자바 코드만을 가지고 단위 테스트 하는 경우를 예시로 들어보자 (이런 경우는 매우 많고, 좋은 테스트 습관이다)
순수한 자바 코드로 OrderServiceImpl을 테스트 하려고 하는데, 만약 OrderServiceImpl이 setterr injection을 사용하는 경우 다음과 같은 문제가 생길 수 있다.
OrderServiceImpl.java
//package, import 생략
@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;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
순수 자바 코드로 OrderServiceImpl을 테스트 하는 코드를 작성해보자.
OrderServiceImpleTest.java
//package, import 생략
public class OrderServiceImpleTest {
@Test
void createOrder() {
OrderServiceImpl orderService = new OrderServiceImpl();
orderService.createOrder(1L, "itemA", 10000);
}
}
@Autowired
annotation이 프레임워크 안에서 동작할 때는 의존관계가 없으면 오류가 발생하지만, 지금은 프레임워크 없이 순수한 자바 코드로만 단위 테스트를 수행하고 있으므로 @Autowired
에 의한 오류가 발생하지 않는다.
그런데 이 테스트 코드를 실행시키면 NullPointerException이 발생한다.
createOrder()
메서드에서 MemberRepository 객체와 DiscountPolicy 객체를 사용하는데 (두 객체와 의존관계가 존재하는데), 해당 테스트 코드에서는 setter를 호출해서 의존관계 주입을 하지 않았기 때문이다.
하지만 순수 자바 코드 테스트를 작성하는 개발자 입장에서는 해당 사실을 미리 숙지하고 있지 않는 이상은, OrderServiceImpl 코드를 직접 확인해야만 setter를 직접 호출해서 DI를 해줘야 한다는 사실을 알아낼 수 있다.
반면에 constructor injection을 사용한다면?
OrderServiceImpl.java
//package, import 생략
@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;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
OrderServiceImpl을 constructor injection을 사용하게 끔 수정한 순간, 위에서 작성했던 테스트 코드에서 컴파일 에러가 난다고 IDE가 친절하게 알려준다.
즉 개발자가 테스트 코드를 작성하는 과정에서 OrderServiceImpl에 필수로 주입해줘야 할 의존관계를 바로 인지할 수 있다.
OrderServiceImpleTest.java
//package, import 생략
public class OrderServiceImpleTest {
@Test
void createOrder() {
OrderServiceImpl orderService = new OrderServiceImpl(
new MemoryMemberRepository(), new RateDiscountPolicy());
orderService.createOrder(1L, "itemA", 10000);
}
}
3. final 키워드 사용 가능
생성자 주입을 사용하면 필드에 final
키워드를 사용 가능하다. 그래서 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아준다.
//package, import 생략
@Component
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
//discountPolicy 대입 누락
}
}
잘 보면 필수 필드인 discountPolicy 에 값을 설정해야 하는데, 이 부분이 누락되었다. 자바는 컴파일 시점에 다음 오류를 발생시킨다.
java: variable discountPolicy might not have been initialized
기억하자! 컴파일 오류는 세상에서 가장 빠르고 좋은 오류다!
참고: 수정자 주입을 포함한 나머지 주입 방식은 모두 생성자 이후에 호출되므로, 필드에 final 키워드를 사용할 수 없다. 오직 생성자 주입 방식만 final 키워드를 사용할 수 있다.
정리
- 생성자 주입 방식을 선택하는 이유는 여러가지가 있지만, 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살리는 방법이기도 하다.
- 기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면 된다.
- 생성자 주입과 수정자 주입을 동시에 사용할 수 있다.
- 항상 생성자 주입을 선택해라! 그리고 가끔 옵션이 필요하면 수정자 주입을 선택해라. 필드 주입은 사용하지않는게 좋다.
'Spring > Spring core basic' 카테고리의 다른 글
[Spring core basic] 36 - 주입 받을 타입의 빈이 2개 이상인 경우 (0) | 2022.05.03 |
---|---|
[Spring core basic] 35 - Lombok과 최신 트렌드 (0) | 2022.05.03 |
[Spring core basic] 33 - 의존관계 자동 주입 옵션 (0) | 2022.05.03 |
[Spring core basic] 32 - 다양한 의존관계 주입 방법 (0) | 2022.05.03 |
[Spring core basic] 31 - 컴포넌트 스캔, 중복 등록과 충돌 (0) | 2022.05.03 |
댓글