[Spring] 기본편 07. 의존관계 자동 주입2

somyeong·2022년 4월 2일
0

Spring

목록 보기
8/17
post-thumbnail

이 글은 스프링 [핵심원리 - 기본편]을 듣고 정리한 내용입니다

📌 조회 빈이 2개 이상 - 문제

  • @Autowired는 타입으로 조회한다.
@Autowired
private DiscountPolicy discountPolicy
  • 타입으로 조회하므로 ac.getBean(DiscountPolicy.class)와 유사하게 동작한다.
  • 만약 DiscountPolicy의 하위타입인 FixDiscountPolicyRateDiscountPolicy가 둘다 스프링 빈으로 선언되어있으면 오류가 발생한다.
 @Component //스프링 빈 등록
  public class FixDiscountPolicy implements DiscountPolicy {}
@Component //스프링 빈 등록
  public class RateDiscountPolicy implements DiscountPolicy {}
  • 의존관계 자동 주입을 실행하면
   @Autowired
  private DiscountPolicy discountPolicy
  • NoUniqueBeanDefinitionException 오류가 발생한다.
 NoUniqueBeanDefinitionException: No qualifying bean of type
  'hello.core.discount.DiscountPolicy' available: expected single matching bean
  but found 2: fixDiscountPolicy,rateDiscountPolicy
  • 하위 타입으로 지정하여 해결할 수 도 있지만, 이는 DIP를 위배하는것이고 유연성이 떨어진다.
  • 그리고 이름만 다르고 완전히 똑같은 타입의 스프링 빈이 2개 있을 때는 해결이 안된다.
  • 스프링 빈을 수동 등록해서 해결 할 수는 있지만, 의존 관계 자동 주입에서 해결하는 여러가지 방법을 살펴보자!

📌 @Autowired 필드 명, @Qualifier, @Primary

  • 조회 대상 빈이 2개 이상일 때 해결방법
  1. @Autowired 필드 명 매칭
  2. @Qualifier -> @Qualifier 끼리 매칭
  3. @Primary 사용

🌱 @Autowired 필드 명 매칭

  • @Autowired는 타입 매칭을 시도하고, 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.

  • 기존코드

   @Autowired
  private DiscountPolicy discountPolicy
  • 필드 명을 빈 이름으로 변경
@Autowired // 필드명이 rateDiscountPolicy이므로 정상 주입된다.
  private DiscountPolicy rateDiscountPolicy

@Autowired 매칭 정리

  1. 타입 매칭
  2. 타입 매칭의 결과가 2개 이상일 때 필드명, 파라미터 명으로 빈 이름 매칭

🌱 @Qualifier 사용

  • @Qulifier는 추가 구분자를 붙여주는 방법이다.

  • 주입 시 추가적인 방법을 제공하는 것이고, 빈 이름을 바꾸는건 아니다.

  • 빈 등록시

@Component
  @Qualifier("mainDiscountPolicy") //Qulifier를 붙여준다.
  public class RateDiscountPolicy implements DiscountPolicy {}
@Component
  @Qualifier("fixDiscountPolicy")
  public class FixDiscountPolicy implements DiscountPolicy {}
  • @Qulifier를 이용한 생성자 자동 주입 예시
   @Autowired // 주입시에 @Qualifier를 붙여주고 등록한 이름을 적어준다.
  public OrderServiceImpl(MemberRepository memberRepository,
                          @Qualifier("mainDiscountPolicy") DiscountPolicy
  discountPolicy) {
      this.memberRepository = memberRepository;
      this.discountPolicy = discountPolicy;
}
  • 수정자 작동 주입 예시
@Autowired
  public DiscountPolicy setDiscountPolicy(@Qualifier("mainDiscountPolicy")
  DiscountPolicy discountPolicy) {
      return discountPolicy;
  }
  • @Qualifier로 주입할 때 Qualifier("mainDiscountPolicy")를 못찾는다면, mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다.

  • 그러나, @Qualifier@Qualifier를 찾는 용도로만 사용하자

    @Qualifier 정리

  1. Qualifier 끼리 매칭

  2. 빈 이름 매칭

  3. NoSuchBeanDefinitionException 예외 발생

    🌱 @Primary 사용

  • @Primary는 우선순위를 정하는 방법이다.
  • @Autowired 할 때, 여러 빈이 매칭되면 @Primary 어노테이션 붙은 곳이 우선권을 가진다
  • 아래 경우에, rateDiscountPolicy가 우선권을 가진다.
    @Component
     @Primary
     public class RateDiscountPolicy implements DiscountPolicy {}
     @Component
     public class FixDiscountPolicy implements DiscountPolicy {}
  • 사용 코드
    //생성자
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository,
                             DiscountPolicy discountPolicy) {
         this.memberRepository = memberRepository;
         this.discountPolicy = discountPolicy;
     }
    //수정자
    @Autowired
    public DiscountPolicy setDiscountPolicy(DiscountPolicy discountPolicy) {
         return discountPolicy;
     }

@Primiary, @Qulifier 활용

  • 코드에서 메인 데이터베이스 커넥션을 획닥흐난 스프링 빈은 @primary를 적용해서 조회하는 곳에서 @Qualifier 지정 없이 편하게 조회하고,
  • 서브 데이터베이스 커넥션 빈을 획득할 때는 @Qualifier를 지정해서 명시적으로 획득하는 방식으로 사용하면 코드를 깔끔하게 유지할 수 있다. (물론 이때 메인 데이터베이스의 스프링 빈을 등록할 때 @Qualifier를 지정해주는 것은 상관없다.)

우선순위

  • @Primiary@Qualifier중에 @Qualifier의 우선수위가 더 높다.
  • 스프링은 자동 보다는 수동이, 넓은 범위의 선택권 보다는 좁은 범위의 선택권이 우선순위가 높다.

📌 Annotation 직접 만들기

  • @Qualifier("mainDiscountPolicy") 이렇게 문자를 만들면 String이므로, 컴파일 시 타입 체크가 안된다.
  • 어노테이션을 새로 만들어서 해결할 수 있다.
package hello.core.annotation;
import org.springframework.beans.factory.annotation.Qualifier;

import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy") //이부분도 추가해야함.
public @interface MainDiscountPolicy {
}
@Component
  @MainDiscountPolicy //만든 어노테이션 추가
  public class RateDiscountPolicy implements DiscountPolicy {}
/생성자 자동 주입
@Autowired //파라미터에 Qualifier대신 만든 어노테이션 사용
public OrderServiceImpl(MemberRepository memberRepository,
                          @MainDiscountPolicy DiscountPolicy discountPolicy) {
      this.memberRepository = memberRepository;
      this.discountPolicy = discountPolicy;
  }
//수정자 자동 주입
@Autowired
public DiscountPolicy setDiscountPolicy(@MainDiscountPolicy DiscountPolicy discountPolicy) {
      return discountPolicy;
  }
  • 어노테이션에는 상속 이라는 개념이 없다.
  • 이렇게 여러 어노테이션을 모아서 사용하는 기능은 스프링이 지원해주는 기능이다.
  • 기존에 있는 어노테이션들을 재정의 할 수는 있지만, 스프링이 제공하는 기능을 뚜렷한 목적없이 무분별하게 재정의 하는것은 유지보수 측면에서 안좋다.

📌 조회한 빈이 모두 필요할 때, List, Map

  • 의도적으로 해당 타입의 스프링 빈이 전부 필요한 경우가 있을 수 있다.
    ex) 고객이 할인서비스 종류 전체 중 선택할 수 있는 상황
package hello.core.autowired;

import static org.assertj.core.api.Assertions.*;

public class AllBeanTest {

    @Test
    void findAllBean(){
        //기존 AutoAppConfig에서도 주입받고, DiscountService(코드 아래 생성: Map이 있는 코드)에서도 주입받음.
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);

        DiscountService discountService = ac.getBean(DiscountService.class);
        Member member = new Member(1L, "userA", Grade.VIP);
        //fixDiscountPolidcy 적용했을 때 할인 금액
        int discountPrice = discountService.discount(member,10000,"fixDiscountPolicy");
        
        //당연한 검증 - 인스턴스 맞는지 확인
        assertThat(discountService).isInstanceOf(DiscountService.class);
        
        //fixDiscountPolicy 적용했을때, 할인액이 1000원 맞는지 검증
        assertThat(discountPrice).isEqualTo(1000);

        //rateDiscountPolicy 적용했을때, 할인액이 2000원 맞는지 검증
        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 //disCountPolidct 정책들이 전부 주입됨.
        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, List 출력 결과

주입 분석

  • Map<String, DiscountPolicy>: map의 key에 스프링 빈의 이름을 넣어주고, value는 DiscountPolicy타입으로 조회한 모든 스프링 빈을 담는다.
  • List<DiscountPolicy>: DiscountPolicy타입으로 조회한 모든 스프링 빈을 담는다.
  • 해당하는 타입이 없으면, 빈 컬렉션이나 map을 주입한다.

로직 분석

  • DiscountService는 Map으로 모든 DiscountPolicy를 주입받는다. 위 코드에서는 fixDiscountPolicy, rateDiscountPolicy가 주입되었다.
  • discount() 메서드는 discountCode를 통해 선택한 정책이 넘어오면, 해당 정책에 해당하는 스프링 빈을 찾아서 실행한다.

*참고 - 스프링 컨테이너를 생성하면서 스프링 빈 등록하기

  • new AnnotationConfigApplicationContesxt(AutoAppConfig.class, DiscountService.class); 부분에서
    • new AnnotationConfigApplicationContext(): 스프링 컨테이너 생성
    • AutoAppConfig.class, DiscountService.class 를 파라미터로 넘기면서 해당 클래스들을 자동으로 스프링 빈으로 등록
  • 즉, 스프링 컨테이너를 생성하면서 동시에 , 해당 컨테이너에 클래스들을 스프링 빈으로 자동 등록 한것이다.

📌 자동, 수동의 올바른 실무 운영 기준

🌱 기억할 것 (강노 내용 이해하기)

  • 편리한 자동 기능을 기본으로 사용하자
  • 직접 등록하는 기술 지원 객체는 수동 등록
  • 다형성을 적극 활용하는 비즈니스 로직은 수동 등록은 고민해보자

애플리케이션 빈 종류

  • 업무 로직 빈
    • 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리 등.
    • 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.
    • 업무 로직 빈은 숫자도 많고, 컨트롤러-서비스-리포지토리 처럼 유사한 패턴이 있으므로 자동 기능을 적극 사용하는 것이 좋다.
  • 기술 지원 빈
    • 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다.
    • 데이터베이스 연결이나, 공통 로그 처리 처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.
    • 기술 지원 로직은 상대적으로 그 수가 적고, 문제가 발생 했을 때, 기술 지원 로직이 잘 적용되고 있는지 아닌지 파악하기 어려우므로, 가급적 수동 빈 등록을 사용해서 명확하게 들어내는 것이 좋다.

*참고

  • 스프링과 스프링 부트가 자동으로 등록하는 수많은 빈들은 예외이다.
  • 스프링 부트가 아니라 내가 직접 기술 지원 객체를 스프링 빈으로 등록한다면 수동으로 등록해서 명확하게 들어내는 것이 좋다.
profile
공부한 내용 잊어버리지 않게 기록하는 공간!

0개의 댓글