
김영한 강사님의 스프링 핵심 원리의 강의 내용과 자료를 이용했음을 밝힙니다.
앞에서 @ComponentScan을 통해서 자동으로 빈을 등록하는 방법을 배웠다.
그 과정에서 자동으로 의존 관계도 등록하기 위해 우리는 생성자와 @Autowired 이용해 의존관계 주입하는 방법을 배웠다.
하지만 의존관계를 주입하는 방법은 생성자만 있는게 아니라 3개가 더 있다.
사실 제목에서 보이듯 요즘 권장되는 방식은 생성자를 이용한 방식이지만 그래도 알기는 해야하니 알아보자:)
- 생성자 주입
- 수정자 주입(setter 주입)
- 필드 주입
- 일반 메서드 주입
이렇게 4가지가 있다.
우리가 앞에서 했던 방식이다.
특징은
- 필드에 생성자를 통해 의존관계를 주입하기에 생성자를 호출하는 시점에 딱 한번만 호출되는 것이 보장된다.
- 필드가 생성자를 통해서만 값이 입력되고 그 후에는 불변한다.
- 필드를 final로 선언하기에 값이 필수적으로 있어야 한다.
예시로 코드를 보자
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = rateDiscountPolicy;
}
여기서 한가지 팁을 주자면 생성자가 한 개만 존재한다면 @Autowired는 생략이 가능하다.
즉 이렇게
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = rateDiscountPolicy;
}
생성자가 여러개라면 메인 의존관계 주입 생성자에 @Autowired를 꼭 붙여줘야 한다.
setter 잘 알고 있듯이 필드 값을 수정하는 수정자 메소드이다.
이 수정자 메소드를 통해 의존관계를 주입하는 방법이다.
우선 코드를 한번 보자
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
자 이 코드가 수정자 코드와 다른 건 생성자와 수정자의 차이도 있지만 필드를 보면 final이 사라졌다.
왜냐면 수정자 주입은 선택 가능성이 있는 의존관계에서 사용할 수 있기 때문이다.
예를 들어 setDiscountPolicy에서 파라미터인 discountPolicy가 생성되지 않아도 사용이 가능하다.
@Autowired는 주입할 대상이 없으면 오류를 일으키지만 뒤에 (required = false)를 붙여주면 주입할 대상이 없어도 오류를 일으키지 않는다.
`
@Autowired(required = false)
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
그러면 필드에 의존관계가 무조건 주입되지는 않는다라는 말이고 그렇기에 final이 없는 것이다.
(필드에 final을 넣으면 오류가 일어난다.)
자 그러면 이런 의문이 들수도 있다.
"생성자의 @Autowired에도 (required = false)를 붙이면 선택 가능성이 있는 의존관계에서 사용가능한 거 아닌가?"
답부터 말하자면 안된다.

현재 클래스의 필드에서 final을 지우고
discountPolicy로 들어올 수 있는 모든 클래스에서 @Component 애노테이션을 지우고 실행해보면 파라미터가 없다며 UnsatisfiedDependencyException이 발생하는 것을 확인할 수 있다.
참고로 생성자는 @Bean을 등록을 하면서 호출해야 하기에(객체 생성) 무조건 호출이 되고 그 뒤에 setter 같은 얘들이 호출된다고 한다.(필수 x)
-> 아마 위 내용이 생성자와 수정자 사이에 선택 가능성 여부를 가르는 것 같은데.. 잘은 ㅎ
정말 간단해서 개발자로서 보기엔 매우 매력적인 방법이기도 하다.
바로 그냥 필드에 @Autowired만 붙여주면 알아서 의존관계를 주입해주는 방식이다.
수업 자료에 이런 내용이 있었다.
DI 프레임워크가 없으면 아무것도 할 수 없다.
이에 대한 테스트를 하는데
public class OrderServiceImpl implements OrderService {
@Autowired private MemberRepository memberRepository;
@Autowired private 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);
}
//for test
public MemberRepository getMemberRepository() {
return memberRepository;
}
이렇게 필드 주입을 만들어 주고
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
OrderServiceImpl bean = ac.getBean(OrderServiceImpl.class);
MemberRepository memberRepository = bean.getMemberRepository();
System.out.println("memberRepository = " + memberRepository);
이런 식으로 불러와서 사용하면 잘되는 것을 확인할 수 있다.
하지만
@Test
void fieldInjectionTest() {
OrderServiceImpl orderService = new OrderServiceImpl();
orderService.createOrder(1L, "itemA", 10000);
}
이 테스트는 createOrder에서 안에 필드들에 의존관계 주입이 안되서 NullPointInjection으로 실패한다.
사실 나는 이게 이해가 정말 안됐다.
아니 필드 인젝션을 하면 걍 넣어지니까 사용되어야 하는 것 아닌가?
그래서 stackoverflow에 질문을 올려보았다.
why-field-injection-makes-nullpointexception
그리고 알게 되었다.
우리가 무엇을 배우는가? 바로 스프링이다.
인젝션을 해주는 것도 스프링의 기능이다.
첫 번째 테스트에서는 AnnotationConfigApplicationContext를 이용해 인젝션을 했다.
하지만 두 번째 테스트에서는 순수 자바코드이기에 프레임워크가 개입하지 않았고 즉 의존관계 주입이 되지 않았던 것이다. ㅇㅇ
즉 프레임워크가 없는 순수 자바코드로는 아무것도 할 수 없는 것이다.
또한 외부에서 변경도 불가능해 테스트도 어렵다.
그러니 결론은?

일반 메서드를 통해서도 주입할 수 있다.
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = rateDiscountPolicy;
}
생성자인데 이름만 바꿔서 생성자가 아닌 메서드라고 생각하면 된다.
어떻게 보면 그냥 파라미터 한번에 받는 수정자 주입이라고 생각할 수도 있다.
자 현재는 의존관계 주입을 하면 대부분 생성자 주입을 권장하는데 왜 그런지, 어떤 장점들이 있는지 알아보자.
대부분의 의존관계 주입은 한번 일어나고 종료시점까지 변경할 일이 없는데
수정자 주입 같은 것을 사용하면 수정자 메소드를 public으로 열어놓아야 한다.
그러면 누군가 실수로 변경할 수도 있다.
하지만 생성자 주입은 객체 생성 때, 딱 1번만 일어남으로 걱정 ㄴㄴ
아까의 필드 주입 때 본 test를 한번 다시 가져와 보자
@Test
void fieldInjectionTest() {
OrderServiceImpl orderService = new OrderServiceImpl();
orderService.createOrder(1L, "itemA", 10000);
}
만약 수정자, 필드 주입이었다면 createOrder에서 위에서 말한 것처럼 NPE로 걸린다.
하지만 만약 생성자 주입이었다면 바로 생성자에 주입 데이터 누락했을 때 오류가 뜨기에 누락을 알아채기 쉽다.
생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있는데 이것이 생성자에서 혹시라도 값이 주입되지 않는 오류를 막아준다.
뭐 필드에 = 붙여서 넣으면 생성자로 값 안넣을 수 있기는 한데.. 이거 빼면 생성자 밖에 없다.
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
this.memberRepository = memberRepository;
}
이런 식으로 안 넣으면 바로 오류가 나타난다.
그러니 그냥 생성자 주입해라

혹시 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 사용하고 필드 주입은 제발 사용하지 말자.