[Spring-기본] 조회할 빈이 2개 이상일 때

DANI·2023년 11월 26일

Spring[김영한T]

목록 보기
27/31
post-thumbnail

기존에 RateDiscountPolicy만 스프링 빈으로 등록해두었는데, FixedDiscountPolicy까지 스프링 빈으로 등록해 두면 어떤 문제가 발생할까?



@Component // 애너테이션 추가
public class FixedDiscountPolicy implements DiscountPolicy{

    private  int discountFixAmount = 1000; // 1000원 할인
    ...(생략)...
}
@Component
public class RateDiscountPolicy implements DiscountPolicy{
    private int discountRate = 10;

    ...(생략)...
}



🚫 에러 발생

테스트 파일 전체 실행하기

위와 같이 에러가 발생한다.



🔑 이를 해결할 수 있는 방법은?

✅ 1. @Autowired 필드 명 매칭
✅ 2. @Qualifier -> @Qualifier끼리 매칭 -> 빈 이름 매칭
✅ 3. @Primary 사용
✅ 4. 애너테이션 직접 만들기



🔑 1. @Autowired 필드 명 매칭


💾 OrderServiceImpl 파일 기존

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService{

  private final MemberRepository memberRepository;
  private final DiscountPolicy discountPolicy;
  @Override
  public Order createOrder(Long id, String name, int price) {
      Member member = memberRepository.findById(id);
      int discount = discountPolicy.fixedDiscount(member, price);
      return new Order(id, name, price, discount);
  }
  public MemberRepository getMemberRepository(){
      return memberRepository;
  }
}

💾 OrderServiceImpl 파일 수정

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService{

  private final MemberRepository memberRepository;
  private final DiscountPolicy rateDiscountPolicy;
  @Override
  public Order createOrder(Long id, String name, int price) {
      Member member = memberRepository.findById(id);
      int discount = rateDiscountPolicy.fixedDiscount(member, price);
      return new Order(id, name, price, discount);
  }
  public MemberRepository getMemberRepository(){
      return memberRepository;
  }
}

🔵 테스트 파일 실행 결과

전체 통과 @Autowired는 타입으로 우선 매칭 후 같은 타입이 두개라면 파라미터명으로 빈 이름을 매칭해준다.
따라서 구현체의 필드명을 rate로 변경하게 되면 실제로 스프링 빈에 등록된 DiscountPolicy 타입의 빈이 ratefixed 두개여도 필드명으로 조회하게 된다.




🔑 2. @Qualifier 붙이기


@Component 
@Qualifier("sub") // 애너테이션 추가
public class FixedDiscountPolicy implements DiscountPolicy{

  private  int discountFixAmount = 1000; // 1000원 할인
  ...(생략)...
}
@Component
@Qualifier("main") // 애너테이션 추가
public class RateDiscountPolicy implements DiscountPolicy{
    private int discountRate = 10;

    ...(생략)...
}

💾 OrderServiceImpl 파일 기존

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService{

  private final MemberRepository memberRepository;
  private final DiscountPolicy rateDiscountPolicy;
  @Override
  public Order createOrder(Long id, String name, int price) {
      Member member = memberRepository.findById(id);
      int discount = rateDiscountPolicy.fixedDiscount(member, price);
      return new Order(id, name, price, discount);
  }
  public MemberRepository getMemberRepository(){
      return memberRepository;
  }
}

💾 OrderServiceImpl 파일 수정

@Component
public class OrderServiceImpl implements OrderService{

  private final MemberRepository memberRepository;
  private final DiscountPolicy discountPolicy;

  
  // @Required~ 애너테이션을 
  // 삭제 후 생성자를 다시 만들어 주었다.
  // DiscountPolicy에 @Qualifier("main")를 붙여줬다
  @Autowired 
  public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("main") DiscountPolicy discountPolicy) {
      this.memberRepository = memberRepository;
      this.discountPolicy = discountPolicy;
  }

🔵 테스트 파일 실행 결과

만약에 @Qualifier로 주입 시 @Qualifier("main")를 못 찾을 경우 추가로 main라는 이름의 스프링 빈을 찾는다.
직접 빈 등록 시에도 @Qualifier를 사용할 수 있다.

@Bean
@Qualifier("main")
public DiscountPolicy discountPolicy(){
    System.out.println("call Appconfig.discountPolicy");
    return new RateDiscountPolicy();
}

이 마저도 찾을 수 없게 된다면, noSuchBeanDefinitionException을 발생시킨다.

<br>


🔑 3. @Primary 사용


@Component 
public class FixedDiscountPolicy implements DiscountPolicy{

  private  int discountFixAmount = 1000; // 1000원 할인
  ...(생략)...
}
@Component
@Primary // 애너테이션 추가
public class RateDiscountPolicy implements DiscountPolicy{
    private int discountRate = 10;

    ...(생략)...
}

💾 OrderServiceImpl 파일 기존

@Component
public class OrderServiceImpl implements OrderService{

  private final MemberRepository memberRepository;
  private final DiscountPolicy discountPolicy;

  @Autowired 
  public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("main") DiscountPolicy discountPolicy) {
      this.memberRepository = memberRepository;
      this.discountPolicy = discountPolicy;
  }

💾 OrderServiceImpl 파일 수정

@Component
public class OrderServiceImpl implements OrderService{

  private final MemberRepository memberRepository;
  private final DiscountPolicy discountPolicy;

  // `@Qualifier`를 삭제했다.
  @Autowired 
  public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
      this.memberRepository = memberRepository;
      this.discountPolicy = discountPolicy;
  }

🔵 테스트 파일 실행 결과

@Primary 는 우선권을 가진다. 빈에 빈에 @Primary를 붙이게 될 경우 파라미터에는 별도로 붙일 애너테이션이 없다. 만약, @Primary@Qualifier를 동시에 사용할 경우 우선권은 @Qualifier가 가져간다.

<br>


🔑 4. 애너테이션 직접 만들기


💾 MainDiscountPolicy 애너테이션 생성

package hello.core.annotation;

import org.springframework.beans.factory.annotation.Qualifier;


import java.lang.annotation.*;

// @Qualifier API에서 다음과 같은 애너테이션을 복사할 수 있다.
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}

@Component 
public class FixedDiscountPolicy implements DiscountPolicy{

  private  int discountFixAmount = 1000; // 1000원 할인
  ...(생략)...
}
@Component
// @Primary 애너테이션 삭제
@MainDiscountPolicy // 생성한 애너테이션 추가
public class RateDiscountPolicy implements DiscountPolicy{
    private int discountRate = 10;

    ...(생략)...
}

💾 OrderServiceImpl 파일 기존

@Component
public class OrderServiceImpl implements OrderService{

  private final MemberRepository memberRepository;
  private final DiscountPolicy discountPolicy;

  // `@Qualifier`를 삭제했다.
  @Autowired 
  public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
      this.memberRepository = memberRepository;
      this.discountPolicy = discountPolicy;
  }

💾 OrderServiceImpl 파일 수정

@Component
public class OrderServiceImpl implements OrderService{

  private final MemberRepository memberRepository;
  private final DiscountPolicy discountPolicy;

  // @MainDiscountPolicy 를 추가했다
  @Autowired
  public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
      this.memberRepository = memberRepository;
      this.discountPolicy = discountPolicy;
  }

🔵 테스트 파일 실행 결과

@Qualifier("main")은 문자열이기 때문에 컴파일 시 타입체크가 되지 않는다. 그래서 애너테이션을 생성해서 문제를 해결할 수 있다. 애너테이션은 재정의가 가능하기 때문에 @Autowired의 애너테이션도 재정의 할 수 있지만, 혼란을 가중할 수 있다.




📝 결론

✅ 1. @Autowired 필드 명 매칭

타입매칭을 먼저 시도 함 -> 타입이 여러개일 경우 필드명, 파라미터명으로 매칭하게 된다.

✅ 2. @Qualifier -> @Qualifier끼리 매칭 -> 빈 이름 매칭

추가 구분자를 붙여주는 방법. 구분자를 붙여주는 것이지, 빈이름을 변경하는 것이 아님

만약, 구분자를 못 찾을 경우 해당 이름으로된 스프링 빈을 추가로 찾게 된다.

✅ 3. @Primary 사용

우선순위를 정하는 방법. 파라미터에 별도로 애너테이션을 붙일 필요가 없고, 스프링 빈에만 붙여주면 된다.

✅ 4. 애너테이션 직접 만들기

@Qualifier 를 사용할 경우 문자열이라 컴파일 시 타입 체크를 할 수 없다. 이럴 때 애너테이션을 만들어서 해당 애너테이션을 추가로 넣어주면 된다.
애너테이션을 직접 만들땐 @Qualifier API에 있는 애너테이션을 모두 사용해야 한다.





🔍 만약, 할인정책 선택이 가능해서 두가지 빈 모두 조회가 필요한 상황이라면?

💾 AllBeanFind 테스트 파일

import hello.core.AutoAppConfig;
import hello.core.Discount.DiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.util.List;
import java.util.Map;

public class AllBeanFind {


  @Test
  @DisplayName("모든 빈을 조회해야 할 때")
  void allBean(){
      // AutoAppConfig.class, DiscountService.class 모두 스프링 빈으로 등록한다.
      ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
      // 스프링 빈으로 등록한 DiscountService.class를 조회한다.
      DiscountService discountService = ac.getBean(DiscountService.class);


      Member member1 = new Member(1L, "A", Grade.VIP);
      Member member2 = new Member(2L, "B", Grade.VIP);

   
      // DiscountService에 등록된 discount 사용
      int fixedDiscountPolicy = discountService.discount(member1, 1000, "fixedDiscountPolicy");
      int rateDiscountPolicy = discountService.discount(member2, 1000, "rateDiscountPolicy");
      
      // 등록된 빈의 타입확인    
      Assertions.assertThat(discountService).isInstanceOf(DiscountService.class);
      // 정액할인 정책 선택 시, 1000원 할인
      Assertions.assertThat(fixedDiscountPolicy).isEqualTo(1000);
      // 정률할인 정책 선택 시, 100원 할인
      Assertions.assertThat(rateDiscountPolicy).isEqualTo(100);

  }


  static class DiscountService{
      // 할인정책을 이름과 정책 map 으로 받는다.
      private final Map<String, DiscountPolicy> policymap;
      // 할인정책을 리스트로 받는다
      private final List<DiscountPolicy> policies;

      // 생성자
      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 dicountcode){
          DiscountPolicy discountPolicy = policymap.get(dicountcode);
          System.out.println("dicountcode = " + dicountcode);
          System.out.println("discountPolicy = " + discountPolicy);
          return discountPolicy.fixedDiscount(member, price);
      }
  }
}

🔵 실행결과

map에 key값은 빈 이름으로 등록된다.





💡 자동 빈 등록 vs 수동 빈 등록

1. 자동 빈 등록이 좋은 경우

  • 비즈니스 요구사항을 개발할 때
  • 컨트롤러, 서비스, 리포지토리 등 업무로직 시

2. 수동 빈 등록이 좋은 경우

  • 기술적인 문제나 공통 관심사(AOP)를 처리할 때
  • DB연결이나, 공통 로그 처리처럼 업무 로직을 지원하기 위한 하부 기술이나 공통기술을 사용할 때
  • 다형성을 적극 활용할 때

위의 상황처럼 만약, 정액할인과 정률할인 중 고객이 선택해서 사용해야 할 경우 위 테스트 코드처럼 만들게 되면 어떠한 할인 정책이 있는지 확인하기 어렵다. 다음과 같이 별도의 설정 정보를 만들고 수동으로 등록하면 좋다.

@Configuration
public class DiscountConfig {

    @Bean
    public DiscountPolicy rateDiscountPolicy(){
        return new RateDiscountPolicy();
    }

    @Bean
    public DiscountPolicy fixedDiscountPolicy(){
        return new FixedDiscountPolicy();
    }
}

0개의 댓글