[Spring] 의존 관계 자동 주입
이전 포스트에서 공부했던 ComponentScan은 @Component가 붙은 객체를 스프링 빈으로 자동으로 등록해주었다.
ComponentScan을 사용하면 개발자가 일일이 스프링 빈을 등록하지 않아도 된다는 편리함이 생긴다.
하지만 문제는 ComponentScan을 사용하면 설정 클래스에 의존 관계를 기재해주지 않기 때문에 스프링은 객체들 간의 의존 관계를 알지 못한다.
그래서 스프링이 의존 관계를 식별할 수 있게 @Autowired 애너테이션을 붙이면 스프링이 자동으로 의존관계를 주입해준다.
그렇다면 @Autowired를 어디에 붙여주어야 할까?
스프링에선 4개의 방법을 제시해준다.
수정자 주입(Setter)
setter 메서드를 통해 의존관계 주입
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
public class OrderServiceImp implements OrderService{
private MemberService memberService;
private DiscountPolicy discountPolicy;
@Autowired
public void setMemberService(MemberService memberService) {
this.memberService = memberService;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
...
}
public 으로 열려있으므로 선택,변경 가능성이 있는 의존관계에 사용한다.
단, 의존관계의 변경 가능성이 있다.
필드 주입
필드에 의존관계 주입
1
2
3
4
5
6
7
8
9
@Component
public class OrderServiceImp implements OrderService{
@Autowired private MemberService memberService;
@Autowired private DiscountPolicy discountPolicy;
...
}
권장하지 않는 방법이다.
코드가 간결하지만 외부에서 변경이 불가능해서 테스트 하기 힘들다는 문제가있다.
순수 자바 테스트 코드는 @Autowired가 동작하지 않는다.
그렇기 때문에 순수 자바 테스트코드를 작성할때 의존 관계 주입 자체를 하지 못한다.(private 으로 막혀있고, 생성자나 setter가 없기 때문)
일반 메서드 주입
일반 메서드를 통해 의존 관계를 주입.
1
2
3
4
5
6
7
8
9
@Component
public class OrderServiceImp implements OrderService{
@Autowired
public void init(MemberService memberService, DiscountPolicy discountPolicy){
this.memberService = memberService;
this.discountPolicy = discountPolicy;
}
}
필드 주입처럼 권장하지 않는 방법이다.
의존 관계 자동 주입은 스프링 컨테이너에서 등록되어있는 스프링 빈이어야만 동작한다.
예를 들어 스프링 빈으로 등록되지 않은 객체를 자동주입 시키면 오류를 발생 시킬것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class OrderServiceImp implements OrderService{
private MemberService memberService;
private DiscountPolicy discountPolicy;
private Member member;
@Autowired
public void init(MemberService memberService, DiscountPolicy discountPolicy, Member member){
this.memberService = memberService;
this.discountPolicy = discountPolicy;
this.member = member;
}
}
스프링 컨테이너에 등록되지 않은 Member 객체를 매개변수로 넣어주었다.
1
2
3
4
5
6
@Test
void initTest(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
OrderService orderService = ac.getBean(OrderService.class);
System.out.println(orderService);
}
간단한 조회 테스트를 돌려보자.
굉장히 친절하게 ‘Member’ 객체는 스프링 빈으로 등록되어있지 않다는 오류를 발생시켜준다.
이런 상황이 발생할 수 있으니 사용하지 않는 것이 좋다.
생성자 주입
생성자를 통해 의존관계 주입
이전 포스트에서 사용한 방법이고, 가장 많이 사용하는 방법이다. 그냥 이 방법을 사용하면 된다.
생성자 주입은 스프링 빈으로 등록과 동시에 의존 관계를 주입한다.
그리고 생성자 호출 시점때 딱 1번 실행되기 때문에 불변이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class OrderServiceImp implements OrderService{
private final MemberService memberService;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImp(MemberService memberService, DiscountPolicy discountPolicy){
this.memberService = memberService;
this.discountPolicy = discountPolicy;
}
...
}
또한 생성자 주입을 사용하면 필드에 final키워드를 사용할 수 있다. 그렇게 때문에 객체 생성 시 값이 누락되면 오류를 컴파일 시점에서 막아준다.
한번 setter주입과 비교를 해보자.
orderServiceImp에 주문을 생성하는 테스트에서 오류가 발생할때 어떤 차이가 있는지 확인해보자.
- setter 주입
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Component
public class OrderServiceImp implements OrderService{
private MemberService memberService;
private DiscountPolicy discountPolicy;
@Autowired
public void setMemberService(MemberService memberService) {
this.memberService = memberService;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
public Order createOrder(long memberId, String itemName, int itemPrice) {
Member findMember = memberService.findMember(memberId);
int discountPrice = discountPolicy.discount(findMember,itemPrice);
Order order = new Order(memberId, itemName, itemPrice, discountPrice);
return order;
}
}
우선 setter 주입을 사용하기 때문에 final 키워드를 사용하지 못한다.
테스트 코드를 작성해보자.
- 테스트코드
1
2
3
4
5
6
7
8
9
10
class OrderServiceImpTest {
// 스프링 컨테이너 생성
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AutoAppConfig.class);
@Test
void setterTest(){
OrderServiceImp orderServiceImp = new OrderServiceImp();
orderServiceImp.createOrder(1L,"kim",1000);
}
}
실행하면 런타임 오류인 NullPointerException을 발생한다.
주문을 생성하기 위해선 Member객체가 필요하기 때문이다.
그럼 같은 테스트를 생성자 주입방법으로 한다면 어떻게 되는지 보자.
- 생성자 주입
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
public class OrderServiceImp implements OrderService{
private final MemberService memberService;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImp(MemberRepository memberService , DiscountPolicy discountPolicy) {
this.memberService = memberService;
this.discountPolicy = discountPolicy;
}
public Order createOrder(long memberId, String itemName, int itemPrice) {
Member findMember = memberService.findMember(memberId);
int discountPrice = discountPolicy.discount(findMember,itemPrice);
Order order = new Order(memberId, itemName, itemPrice, discountPrice);
return order;
}
}
생성자 주입으로 코드를 수정하면
IDE 자체에서 빨간줄로 오류가 나타난 부분을 보여준다.
이렇게 생성자 주입을 사용하면 컴파일 시점에 오류를 잡을 수 있다.
많은 방법들이 있지만 기본적으론 생성자 주입을 사용하고 변경이 필요한 부분은 수정자 주입을 사용하자.
그렇다면 @Autowired는 어떤 식으로 스프링빈을 조회할까?
@Autowired 조회
1
2
@Autowired
private DiscountPolicy discountPolicy;
기본적으로 @Autowired는 타입으로 조회를 한다.
1
applicationContext.getBean("DiscountPolicy");
스프링 컨테이너의 getBean(”객체타입”)
과 비슷하게 동작한다.
1
2
3
4
5
6
7
8
9
@Component
public class FixDiscountPolicy implements DiscountPolicy{
...
}
@Component
public class RateDiscountPolicy implements DiscountPolicy {
...
}
만약 DiscountPolicy의 구현객체가 2개(RateDiscountPolicy, FixDiscountPolicy)이고 2개 모두 스프링 컨테이너에 스프링 빈으로 등록되어있다면 @Autowired는 누구를 주입해줄까?
그냥 오류를 발생 시킨다.
같은 타입의 빈이 여러개 있을 경우 오류를 발생시키는데 해결 방법은
스프링 빈을 수동 등록해 각각의 빈들의 이름을 지정해주는 방법이있고, 자동주입에서 해결하는 방법이 있다.
자동 주입에서 해결하는 방법들을 알아보자.
필드명 매칭
변수의 이름을 구현객체이름으로 변경하는 방법
- 기존코드
1
2
@Autowired
private DiscountPolicy discountPolicy;
- 필드명 매칭
1
2
@Autowired
private DiscountPolicy rateDiscountPolicy;
@Autowired는 타입매칭을 하고 매칭된 결과가 2개 이상이라면 필드 명으로 빈이름을 추가 매칭 한다.
@Qualifier
추가 구분자를 붙여주는 방법
1
2
3
4
5
6
7
8
9
10
11
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy{
...
}
@Component
@Qualifier("rateDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {
...
}
스프링 컨테이너에 등록할 객체에 @Qulifier를 붙여준다.
실험 해보니 객체에 @Qulifier를 붙여주지 않아도 정상적으로 실행이 된다.
1
2
3
4
5
6
@Autowired
public OrderServiceImp(MemberService memberService,
@Qualifier("fixDiscountPolicy") DiscountPolicy discountPolicy){
this.memberService = memberService;
this.discountPolicy = discountPolicy;
}
생성자 주입일 경우 매개변수에 주입할 구현객체를 @Qulifier로 명시해준다.
@Primary
우선순위를 지정해주는 방법
매칭 후 결과가 2개 이상일 경우 @Primary가 붙은 스프링 빈을 주입시켜준다.
1
2
3
4
5
6
7
8
9
10
@Component
@Primary
public class FixDiscountPolicy implements DiscountPolicy{
...
}
@Component
public class RateDiscountPolicy implements DiscountPolicy {
...
}
@Autowired가 조회 할 때 먼저 DiscountPolicy 타입으로 조회를 하고 결과가 2개 이상이므로
@Primary 가 붙은 FixDiscountPolicy를 자동 주입해줄것이다.
@Qulifier 와 @Primary의 우선순위
결론은 @Qulifier가 우선순위가 더 높다.
스프링은 자동보다는 수동, 넓은 범위보다는 좁은 범위의 선택권을 더 높은 우선순위로 둔다.
끝
오늘은 자동 의존 관계 주입에 대해 공부했다.
스프링이 어떤식으로 의존관계를 구별하고 주입시켜주는지에 대해 간소하게나마 알수있게되었다.