오느르은 저번에 다룬 의존관계 자동주입과 관련하여 여러 디테일들에 대해 다루어 보겠다.
1. 조회한 빈이 2개 이상
예를 들어 A라는 인터페이스를 상속받은 a,b가 있는데 기존에는 a와 b 둘 중 하나만 @Bean을 사용하던지 @Component을 사용해서 의존관계를 설정해 주었다. 하지만 만약 a,b 둘 다 스프링 컨테이너에 등록 되어 있다면..? 둘 중 하나를 지우면 되겠지만,, 어떤 상황에는 a가 필요하고 다른 상황에는 b가 필요하다고 하면,, 곤란하다. 물론 @Congfiguration 어노테이션을 붙인 두 Config 클래스를 만들면 되겠지만, a와 b말고 모두 다 같다면 복붙하기만 하면 되지만,,, ㅋㅋㅋㅋㅋ 우리 좀 간지나게 살아보자. 이 간지나는 삶을 위해 스프링이 제공하는 방식 3가지가 있다.
1.1 @Autowired를 이용하는 방식.
정말 간단하다. @Autowired는 일단 맨 먼저 타입 매칭을 한다. 그 후에 타입 매칭이 여러개가 되어 있다면 필드 명, 파라미터 명으로 빈 이름을 매칭하는데, 필드 명 또는 파라미터명을 우리가 원하는 타입의 이름으로 하면 된다. 잊으면 안되는것은 당연히 첫 글자는 소문자!! 컨테이너에 첫 글자는 소문자로 등록되기 때문이랍니다아아~ 아래 코드가 예시 코드!
@Autowired private DiscountPolicy rateDiscountPolicy;
////////////////////////////////////////////////////////
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {//파라미터 주입
this.memberRepository = memberRepository;
this.discountPolicy = rateDiscountPolicy;
}
DiscountPolicy라는 인터페이스를 상속한 RateDiscountPolicy와 FixDiscountPolicy 모두 스프링 컨테이너에 등록되어 충돌 되었던 상황었다. 하지만 이젠 위 두 버전으로 정확히 가져올 객체를 명시해서 충돌을 피했다.
//////// 부분 위에는 저번에 다룬 필드 명(스프링이 비추한다는,,) 을 이용해서 rateDiscountPolicy라고 명시했다. 그 아래 부분은 파라미터의 변수 명을 사용하는 객체를 명시해서 충돌을 피했다.
1.2 @Qualifier 사용하기 (별칭)
빈 등록시 아래 코드와 같이 @Qualifier와 그 옆에 문자열을 이용하여 별칭을 정해준다. 이번 충돌 상황도 1.1과 똑같은 상황이라고 하자.
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy{
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy{
이후 DiscountPolicy 객체를 사용하는 객체인 OrderServiceImpl 객체에서 다음과 같이 명시해준다.
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
이렇게 @Qualifier을 생성자 함수 파라미터에 넣어 명시해 줄 수 있다. 물론 이 @Qualifier 어노테이션을 저번에 다룬 setter 주입이나 필드 주입에 붙여서 사용 가능하다.
여기서 한 가지 의문... 만약 "mainDiscountPolicy"를 못 찾으면 어떻게 될까? 만약 못 찾는 경우 한정,, 컨테이너에 등록된 스프링 빈들과 이름을 비교하여 가져온다. 이를 노리고 코드를 짤 수 있겠지만 비추! 헷갈려!!! 만약에 빈들에도 없다면 NoSuchBeanDefinitionException 을 반환한다.
그렇다면 만약에 실수로 오타가 나버렸어... 제엔장,, 그래서.. 찾는데 오래 걸렸어.. 라는 경험을 없게 만드는 하나의 방법이 있는데 이건 다음 문단에서 다루겠다!
1.3 @Primary 이용하기
말 그대로 우선순위를 이용한다. 이미 직관적이죠?! 보나마나 @Primary 어노테이션 붙이면 그게 우선권이겠죠?! 너무 간단하죠~? 아래 코드처럼 하면 된다.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy{
이렇게 하면 아까의 그 충돌에서 벗어나 DiscountPolicy를 가져오게 된다.
1.4 충돌 피하는 방법 정리하자면
대게 1.3방식을 사용한다. 간단하니깐. 그렇지만 당연히 1.3 방식을 이용하면 A를 상속받은 a,b,c,d 중 하나만 사용하니 의미가 없다. 그래서 대게 1.2방식을 섞어서 사용한다. 그리고 만약 @Primary와 @Qualifier 둘 다 명시되어 있다면? @Qualifier를 우선순위로 하여서 동작한다. 저번 스프링 자동 빈 수동 빈의 맞짱 대결을 생각해 보면 수동 빈이 이긴다고 했다. 물론 스프링 부트는 이를 감지해 내지만. 이처럼 스프링은 자동보다는 수동이, 넓은 범위의 선택권 보다는 좁은 범위의 것을 선호하기에 @Qualifier가 이긴다고 보면 된다.
2. 어노테이션을 만들어서 충돌 피하기
아까 언급한 @Qualifier 사용하다가,,, 오타가 난다면..? 이런 제엔장 을 막는 방법이 바로 어노테이션을 만들면 된다. 자바는 컴파일 할 때 문자열을 체크하지 않기 때문에 이런 오류를 찾아내지 못하고 정상 작동한다. 이제 이걸 방지해보자. 귀찮은거 아냐? 복잡해 보여.. 실은 간단하다.
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
위 코드와 같은 어노테이션을 만든다. @Qualifier 위의 어노테이션들은 @Qualifier 클래스에 명시되어 있어서 그 클래스로 옮긴 다음 코드를 복붙하면 된다. 추가로 자바에서는 저 어노테이션들끼리 묶어주지 않지만 스프링이 싹 묶어주어 @MainDiscountPolicy에 위 어노테이션의 성질들을 다 넣어준다.
그 다음 아래 코드처럼 저 어노테이션을 붙일 클래스 위에 붙여준다.
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy{
이후 이를 의존하는 클래스의 생성자 주입이든, setter 주입이든 우리가 만든 어노테이션을 인자 앞에 붙여 주면 된다. 아래 코드처럼!
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {//파라미터 주입
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
3. 만약 조회한 빈이 모두 필요하다면..?
우선순위고 나발이고 정말 다 필요해서 유연하게 사용해야 하는 상황이 있을 수도 있을 수도 있지 않을까? 많을 수도. 왜냐면 client에 따라 다룰 비즈니스 로직 자체가 너무 달라서 결국 그 로직들을 나누어야 할 수도 있으니깐.
암튼 그렇다면 간단하게 map이나 list를 이용해서 하면 된다. 아래 코드를 보고 설명하겠당.
void findAllBean(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
DiscountService discountService = ac.getBean(DiscountService.class);
Member member = new Member(1L, "userA", Gradle.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);
}
}
new AnnotationConfigurationApplicationContext를 이용해서 DiscountService를 자바 빈으로 등록한다. 이 클래스를 보게 되면 Map과 List 두 가지가 있다. 생성자 함수의 @Autowired를 통해 의존관계를 묶는다. DiscountPolicy 를 상속한 모든 클래스들을 빈으로 등록과 의존관계를 주입하여 Map과 List에 넣어준다. 이때 Map의 key값은 스프링 빈의 이름이다. 만약 DiscountPolicy에 해당하는 스프링 빈이 없으면 비어있는 상태이다.
그런데 여기서 의문이 들 수 있다.. 아니 다 알겠는데 Map과 List를 이룰 DiscountPolicy 상속한 구현체들은 어떻게 가져오는데? 가져와야 의존관계를 묶든 말든 하지. 첫번째 줄 코드를 보면 DiscountService 를 등록한 것 뿐만 아니라 AutoAppConfig 또한 등록 했다. 이 AutoAppConfig에 다 들어 있어요~~
암튼 여기까지는 배경이고 중요한 것은 discount 함수에 인자로 들어가는 discountCode 파라미터에 Map의 key값 역할을 해줄 "fixDiscountPolicy"와 "rateDiscountPolicy"를 가져와 할인 정책을 다르게 적용하는 것을 볼 수 있다.
이처럼 실제로 충돌 될 빈들이 정말 모두 다 필요하게 되면 Map이나 List를 이용하여 각 상황에 맞는 빈을 꺼내 비즈니스 로직을 적용해 낼 수 있다.
마무리~
여기까지 스프링 빈 등록과 의존관계들에 대해서 많은 이야기를 했다. 추가적으로 다룰 이야기가 수동 빈에 대한 이야기다. 여기까지 보면 수동 빈... 너무 귀찮고 빈으로 등록할 객체가 많아진다면 귀찮음x100 이다.. 뜨악
그렇다면 언제 사용할까? 사용하라고 있는 거잖슴~~.
애플리케이션은 크게 두 가지로 나뉜다고 한다. 업무 로직 빈 - MVC, 비즈니스 로직, 리포지토리 같은 것들 이다. 이들은 양이 방대하기도 하고 하다보면 이미 정형화된 패턴대로들 하기 때문에 자동 기능을 사용하는 것이 좋다. 그러면 문제가 발생해도 잘 알 수 있으니.
두 번째 애플리케이션은 '기술지원 빈' 으로서 디비 연결이나, 공통 로그 처리 같은 업무 로직을 지원하기 위한 것들로서 기술적인 문제나 AOP를 처리하는 애플리케이션들을 말한다. 이 로직은 수가 적고 대게 애플리케이션 전반에 걸쳐서 광범위하게 영향을 미치기 때문에 수동 빈 등록을 쓰는 것이 좋다고 한다.
이렇다고 하는데 사실 잘 모르겠다.. 아마 프로젝트를 하게 된다면 뼈저리게 느끼면서 싸그리다 고치는 고생을 해볼 거 같은데 ㅋㅋㅋ 소중한 경험일 것 같다.
뿐만 아니라 수동 빈이 쓸모 있는 경우는 3문단에서 말한 다형성이 필요할 때이다. 남이 내 코드를 보고 이용하게 된다면, 자동 의존관계 주입으로 해버리면 사실 나는 편하지만 상대방 입장에서는 의존관계에 걸쳐있는 모든 클래스들을 무엇이 있는지 하나씩 보아야 한다. 하지만 아래 코드 처럼 딱 이쁘게 정리해 놓으면 그 사람 입장에서 의존관계를 파악하기 쉽고 유지 보수도 쉬워지기에 이럴 땐 수동 빈이 좋다!
@Configuration
public class DiscountPolicyConfig{
@Bean
public DiscountPolicy rateDiscountPolicy(){
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy(){
return new FixDiscountPolicy();
}
}
마무리 이야기가 사실 너무 와닿지 않을 것 같다. 사실 나도 와닿지 않으니깐... 아마 수많은 경험으로 얻는 이야기니깐 일이 꼬일 때 하나의 해결책 아이디어로 생각해 내면 좋을 거 같다!
'백앤드(스프링)' 카테고리의 다른 글
빈 스코프 2편 (0) | 2022.01.28 |
---|---|
빈 생명주기, 빈 스코프 1편 (0) | 2022.01.27 |
의존관계 자동 주입 1편 (0) | 2022.01.22 |
컴포넌트 스캔 (2) | 2022.01.20 |
싱글톤! (2) | 2022.01.18 |