한 타입(Interface)에 대해 여러 Bean 이 등록되어 있으면 어떻게 해결할 것인가?
Autowired 동작 순서
- Type Matching
- Type Matching 결과가 2개 이상인 경우 필드명이나 파라미터 명으로 빈 이름 매칭.
public class OrderServiceImpl implements OrderService{
// 구현체가 아닌 Interface (역할) 에만 의존해야한다. => DIP 원칙 준수
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
// Type Matching 시도
// 타입 매칭 결과가 2개 이상인 경우 필드명 혹은 파라미터 명 (여기서는 rateDiscountPolicy)으로 이름 매칭.
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = rateDiscountPolicy;
}
}
@Qualifier annotation 사용하기
class, field 모두 사용가능하다.
alias를 지정하는 기능으로 이해하면된다. 특정 alias 를 찾지 못하면 해당 alias 이름으로 스프링빈을 추가로 찾는다.
@Qualifer 는 @Qaulifier 를 찾는데만 사용하는 것이 좋다.
작동 순서
- @Qualifer 끼리만 먼저 매칭 시도
- 빈 이름으로 매칭 시도
- 둘다 못찾으면 `NoSuchBeanDefinitionException` 예외 발생
예제
fixDiscount.java
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {
private final int discountFixAmount = 1_000;
}
rateDiscount.java
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {
private final int discountPercent = 10;
}
OrderServiceImple.java
public class OrderServiceImpl implements OrderService{
// 구현체가 아닌 Interface (역할) 에만 의존해야한다. => DIP 원칙 준수
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
// @Qualifier 로 mainDiscountPolicy 찾기
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Primary 사용
When to use
어떤 클래스 or 필드를 우선권을 가지게 할 것인지를 지정하려할 때
주로 사용하는 클래스나 필드가 존재할 때
ex) 메인 DB 와 보조 DB 가 있는 경우 로직의 90%가 메인 DB 일 때는 메인 DB Connection Code 에 @Primary 어노테이션을 미리 붙여놓는다.
RateDiscount.java
@Component
// 우선권 부여
@Primary
public class RateDiscountPolicy implements DiscountPolicy {
private final int discountPercent = 10;
}
OrderServiceImple.java
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
// discountPolicy 에 @Primary 가 걸려있는 rateDiscount 를 DI 한다.
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
@Primary vs @Qualifier 둘다 걸려있다면?
우선권은 더 좁은 범위, 상세한 값이 가진다.
스프링은 자동보다는 수동, 범용적인것 보다 특정 기능을 하는 것에 우선순위를 둔다. 여기서는 @Qaulifier 가 더 높은 우선순위를 가진다.
Annotation 을 수동으로 만들어 사용하기
When to use?
@Qaulifier 를 떡칠하면서 쓰고있는데, 아쉽게도 annotation '문자열'에 대해서는 직접 실행해보지 않고는 오류를 잡을 수가 없다.
Compile time 에 IDE 의 도움을 받으며 유지보수하기 좋은 방식으로 개발할 순 없을까?
직접 Annotation 을 재정의한다.
// @Qaulifier 를 복사한다.
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
// 단순 문자열로 명시한 것보다
// IDE Compile 타임에 에러를 미리 잡을 수 있다.
}
RateDiscount.java
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {
private final int discountPercent = 10;
}
Annotation 'MainDiscountPolicy' 를 붙여 `@Qualifier("mainDiscountPolicy")` 를 붙인 것과 동일한 효과를 볼 수 있다.
OrderServiceImpl.java
@Service
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
// Compile time 에 에러를 잡을 수 있다.
// IDE 단에서 이 어노테이션을 사용한 위치를 추적할 수 있다.
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
⚠️ 보통의 경우는 기본적으로 Spring 에서 제공하는 기본 Annotation 으로 해결이 가능하다.
명확한 이유가 존재할 때만 Annotation 을 재정의하여 사용하도록 하자.
Map, List 를 통한 DI, 동적 bean 사용
When to use?
동적으로 여러 빈을 조회, 사용해야할 때
Map 과 List를 통해 여러 빈을 동적으로 매핑시켜 다형성을 지키며 테스트 코드 작성 가능.
전략 메소드 패턴이라고도 한다.
AllBeanTest.java
각각의 Map, List 로 매핑시켜 DI.
public class AllBeanTest {
static class DiscountService {
// AppConfig.java 를 통해 쫙 DI 가 주입된다.
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policyList;
@Autowired
public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policyList) {
this.policyMap = policyMap;
this.policyList = policyList;
System.out.println("policyMap = " + policyMap);
System.out.println("policyList = " + policyList);
}
// key: Bean name (class name as camelCase)
public int getDiscountPrice(Member member, int price, String discountCode) {
var discountPolicy = policyMap.get(discountCode);
return discountPolicy.getDiscountPrice(member, price);
}
}
@Test
void findAllBean() {
// Component Scan 을 하면서 fixDiscountPolicy, rateDiscountPolicy (camelCase) 모두 읽어들인다.
// 모두 list, map 에 각각 DI 된다.
var ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
DiscountService discountService = ac.getBean(DiscountService.class);
var member = new Member(1L, "userA", Grade.VIP);
// Component Scan 시 자동으로 camelCase 로 Spring Bean 을 Container 에 등록.
var fixDiscountPrice = discountService.getDiscountPrice(member, 10_000, "fixDiscountPolicy");
assertThat(fixDiscountPrice).isEqualTo(1_000);
var rateDiscountPrice = discountService.getDiscountPrice(member, 20_000, "rateDiscountPolicy");
assertThat(rateDiscountPrice).isEqualTo(2_000);
}
}
'JVM > Spring' 카테고리의 다른 글
[SpringBoot] Naming methods in each layer (0) | 2023.02.09 |
---|---|
[Spring] Library 살펴보기 및 스프링 환경설정 (0) | 2022.12.12 |
[Spring] Bean Scope (0) | 2022.12.07 |
[Spring] 빈 생명주기 콜백 (0) | 2022.12.06 |
[Spring] Component Scan, DI autowired (0) | 2022.11.30 |