다양한 의존관계 주입 방법
- 생성자 주입
- 수정자 주입(setter 주입)
- 필드 주입
- 일반 메서드 주입(잘 사용 X)
생성자 주입
1
2
3
4
5
6
7
8
9
10
11
@Component
public class OrderServcieImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServcieImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
생성자가 딱 1개만 있으면
@Autowired
를 생략해도 자동 주입 된다. 물론 스프링 빈에만 해당한다.
수정자 주입(setter 주입)
- setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법
1
2
3
4
5
6
7
8
9
10
11
12
13
@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;
}
}
- 선택, 변경 가능성이 있는 의존관계에 사용된다.
필드 주입
1
2
3
4
5
6
7
@Component
public class OrderServiceImpl implements OrderService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
}
- 코드가 간결해서 많은 개발자들을 유혹하지만 외부에서 변경이 불가능해서 테스트 하기 힘들다는 치명적인 단점이 있다.
- DI 프레임워크가 없으면 아무것도 할 수 없다.
- 사용하지 말것!
옵션 처리
- 주입할 스프링 빈이 없어도 동작해야 할 때가 있다.
- 그런데
@Autowired
만 사용하면required
옵션의 기본값이true
로 되어 있어서 자동 주입 대상이 없으면 오류가 발생한다.
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
public class AutoWiredTest {
@Test
void AutoWiredOption() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
}
static class TestBean {
@Autowired(required = false)
public void setNoBean1(Member noBean1) {
System.out.println("noBean1 = " + noBean1);
}
@Autowired
public void setNoBean2(@Nullable Member noBean2) {
System.out.println("noBean2 = " + noBean2);
}
@Autowired
public void setNoBean3(Optional<Member> noBean3) {
System.out.println("noBean3 = " + noBean3);
}
}
}
@Autowired(required=false)
: 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨org.springframework.lang.@Nullable
: 자동 주입할 대상이 없으면 null이 입력된다.Optional<>
: 자동 주입할 대상이 없으면 Optional.empty 가 입력된다.
- 생성자 주입 방식을 선택하는 이유는 여러가지가 있지만, 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살리는 방법이기도 하다.
- 기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면 된다. 생성자 주입과 수정자 주입을 동시에 사용할 수 있다.
- 항상 생성자 주입을 선택해라! 그리고 가끔 옵션이 필요하면 수정자 주입을 선택해라. 필드 주입은 사용하지 않는게 좋다.
조회 빈이 2개 이상 - 문제
@Autowired
는 타입(Type)으로 조회한다.
1
2
@Autowired
private DiscountPolicy discountPolicy
타입으로 조회하기 때문에, 마치 ac.getBean(DiscountPolicy.class)
와 유사하게 동작한다. (실제로는 더 많은 기능을 제공한다.)
근데 만약 타입으로 조회했을 시 선택된 빈이 2개 이상일 때 문제가 발생한다. 이 문제를 의존관계 자동 주입으로 해결할 수 있는 방법 3가지가 있다.
@Autowired
필드명@Qualifier
@Primary
**
@Autowired 필드 명 매칭
@Autowired
는 타입 매칭을 시도하고, 이때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.
기존코드
1
2
@Autowired
private DiscountPolicy discountPolicy
필드 명을 빈 이름으로 변경
1
2
@Autowired
private DiscountPolicy rateDiscountPolicy
@AutoWired
는 타입을 먼저 매칭하고, 타입 매칭의 결과가 2개 이상일 때 필드 명, 파라미터 명으로 빈 이름 매칭
@Qualifier 사용
@Qualifier
는 추가 구분자를 붙여주는 방법이다. 주입시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아니다.
빈 등록시 @Qualifier를 붙여 준다.
1
2
3
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
주입시에 @Qualifier를 붙여주고 등록한 이름을 적어준다.
1
2
3
4
5
6
7
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy") DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Primary 사용
@Primary
는 우선순위를 정하는 방법이다.@Autowired
시에 여러 빈이 매칭되면@Primary
가 우선권을 가진다.
RateDiscountPolicy가 우선권을 가진 코드
1
2
3
4
5
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
@Primary, @Qualifier
활용코드에서 자주 사용하는 메인 데이터베이스의 커넥션을 획득하는 스프링 빈이 있고, 코드에서 특별한 기능으로 가끔 사용하는 서브 데이터베이스의 커넥션을 획득하는 스프링 빈이 있다고 생각해보자. 메인 데이터베이스의 커넥션을 획득하는 스프링 빈은@Primary
를 적용해서 조회하는 곳에서@Qualifier
지정 없이 편리하게 조회하고, 서브 데이터베이스 커넥션 빈을 획득할 때@Qualifier
를 지정해서 명시적으로 획득 하는 방식으로 사용하면 코드를 깔끔하게 유지할 수 있다. 물론 이때 메인 데이터베이스의 스프링 빈을 등록할 때@Qualifier
를 지정해주는 것은 상관없다.
조회한 빈이 모두 필요할 때, List, Map
예를 들어서 할인 서비스를 제공하는데, 클라이언트가 할인의 종류(rate, fix)를 선택할 수 있다고 가정해보자. 스프링을 사용하면 소위 말하는 전략 패턴을 매우 간단하게 구현할 수 있다.
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
28
29
30
31
32
33
34
public class AllBeanTest {
@Test
void findAllBean() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
DiscountService discountService = ac.getBean(DiscountService.class);
Member member = new Member(1L, "userA", Grade.VIP);
int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");
assertThat(discountService).isInstanceOf(DiscountService.class);
assertThat(discountPrice).isEqualTo(1000);
int rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
assertThat(rateDiscountPrice).isEqualTo(2000);
}
static class DiscountService {
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
@Autowired
public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
System.out.println("policyMap = " + policyMap);
System.out.println("policies = " + policies);
}
public int discount(Member member, int price, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);
return discountPolicy.discount(member, price);
}
}
}
- Map<String, DiscountPolicy> : map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
- List
: DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다. 만약 해당하는 타입의 스프링 빈이 없으면, 빈 컬렉션이나 Map을 주입한다.