관리 메뉴

나구리의 개발공부기록

의존관계 자동주입, 조회 빈이 2개 이상 - 문제, @Autowired 필드명/@Qualifier/@Primary, 애노테이션 직접 만들기, 조회한 빈이 모두 필요할 때(List/Map), 자동과 수동의 올바른 실무 운영 기준 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 핵심원리 - 기본편

의존관계 자동주입, 조회 빈이 2개 이상 - 문제, @Autowired 필드명/@Qualifier/@Primary, 애노테이션 직접 만들기, 조회한 빈이 모두 필요할 때(List/Map), 자동과 수동의 올바른 실무 운영 기준

소소한나구리 2024. 1. 31. 17:11

  출처 : 인프런 - 스프링 핵심원리 - 기본편(유료) / 김영한님  
  유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용  

https://inf.run/kCYMv


1. 조회 빈이 2개 이상 - 문제

1) 문제 발생

  • @Autowired는 타입(Type)으로 조회하기 때문에 ac.getBean(DiscountPolicy.class)처럼 동작함(실제로는 더 많은 기능을 제공)
  • 스프링 빈 조회에서 학습했듯이 타입으로 조회하면 선택된 빈이 2개 이상일 때 에러가 발생함
  • 아래의 예시의 코드처럼 FixDiscountPolicy에도 @Component를 적용하여 의존관계 자동 주입을 실행하면 하나의 빈을 기대했는데 fixDiscountPolicy, rateDiscountPolicy 2개가 발견되었다는 메시지와 함께 NoUniqueBeanDefinitionException 에러가 발생함
  • 전체 테스트를 돌려보면 AutoAppConfigTest의 basicScan에서 에러가 발생

 

@Autowired
private DiscountPolicy discountPolicy

@Component
public class FixDiscountPolicy implements DiscountPolicy {}

@Component
public class RateDiscountPolicy implements DiscountPolicy {}
  • 이때 하위 타입으로 지정하여 해결 할 수도 있지만 DIP를 위배하고 유연성이 떨어지게 되며 이름만 다르고 똑같은 타입의 스프링 빈이 2개 있을 때 해결이 안됨
  • 스프링 빈을 수동 등록해서 문제를 해결해도 되지만 의존관계 자동 주입으로 해결하는 여러 방법이 존재함.

2. @Autowired 필드명, @Qualifier, @Primary

1) @Autowired필드명 매칭으로 해결

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

(1) 사용예시

  • 필드명이나 생성자의 파라미터명을 지정하고자하는 빈으로 직접 지정하면 정상적으로 rateDiscountPolicy가 주입됨
  • 필드명 매칭은 먼저 타입 매칭을 시도하고 그 결과에 여러 빈이 있을 때 추가로 동작하는 기능임
  • 물론 필드 주입은 실무에서 사용하면 안됨
// @Autowired 필드명 매칭 - 필드명을 빈 이름으로 변경
@Autowired private DiscountPolicy rateDiscountPolicy;

// @Autowired 필드명 매칭 - 파라미터명을 빈 이름으로 변경
private final DiscountPolicy discountPolicy;

public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = rateDiscountPolicy;
}

 

(2) @Autowired 매칭 정리

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

2) @Qualifier 사용

  • 추가 구분자를 사용하는 방법
  • 주입 시 빈 이름을 변경하는 것이 아니라 추가적인 방법을 제공 하는 것임

(1) 사용 예시

  • 빈 등록시 @Qualifier("등록한 이름")를 붙여줌
  • 만약 @Qualifier("mainDiscountPolicy")를 못 찾으면 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 검색함
  • 그러나 Qualifier는 @Qualifier를 찾는 용도로만 사용하는 것이 명확하고 좋음
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}

 

(2) 생성자 자동 주입 예시

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

 

(3) 수정자 자동 주입 예시

public void setDiscountPolicy(@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
    this.discountPolicy = discountPolicy;
}

 

(4) 빈 직접 등록 시 예시

@Bean
@Qualifier("mainDiscountPolicy")
public DiscountPolicy discountPolicy() {
    return new ...
}

 

(5) @Qualifier 정리

  1. @Qualifier끼리 매칭
  2. 빈 이름 매칭
  3. 그럼에도 못찾으면 NoSuchBeanDefinitionException 예외 발생

3) @Primary사용

  • 우선순위를 정하는 방법
  • @Autowired시 여러 빈이 매칭이 될 경우 @Primary가 붙은 곳이 우선권을 가짐

(1) 사용예시

  • 별다른 코딩이 필요없이 우선권을 가져야하는 곳에 @Primary만 붙여 주면 되며 전체 테스트를 실행해보면 정상적으로 Primary가 동작함
 @Component
 @Primary // 우선권
 public class RateDiscountPolicy implements DiscountPolicy {}
 
 @Component
 public class FixDiscountPolicy implements DiscountPolicy {}

 

(2) @Primary와 @Qualifier의 활용

  • @Primary는 코드의 수정이 없지만 @Qualifier는 주입 받을 때 모든 코드에 @Qualifier를 붙여 줘야함
  • 메인 데이터베이스의 커넥션을 획득하는 스프링 빈은 @Primary를 적용해서 편리하게 조회하고 서브 데이터베이스의 커넥션을 획득을 할 때는 @Qualifier를 지정해서 명시적으로 획득 하는 방식으로 사용하면 코드를 깔끔하게 유지 할 수 있음
  • 이때 메인 데이터베이스의 스프링 빈을 등록할 때@Qualifier를 이용하는 것은 상관 없음

(3) 우선순위

  • @Primary는 기본값 처럼 동작하는 것이고 @Qualifier는 매우 상세하게 동작함
  • 스프링은 자동보다는 수동이, 넓은 범위보다는 좁은 범위의 선택권이 우선순위가 높기에 @Qualifier가 우선권이 높음

3. 애노테이션 직접 만들기

1) 타입 체크 문제 해결

  • @Qualifier("mainDiscountPolicy") 이렇게 문자를 적으면 컴파일시 타입 체크가 안되는데 애노테이션을 만들어서 문제를 해결할 수 있음

(1) @MainDiscountPolicy 애노테이션을 직접 생성

  • annotation 패키지를 생성하여 작성
  • @Qualifier에 들어가서 작성 되어있는 애노테이션들을 전부 직접 생성한 MainDiscountPolicy에 복사하고 @Qualifier()로 이름을 지정
  • 애노테이션은 상속이라는 개념이 없으며 여러 애노테이션을 모아서 사용하는 기능은 스프링이 지원해주는 기능임
  • @Qualifier뿐만 아니라 다른 애노테이션도 함께 조합해서 사용할 수 있음(@Autowired 같은 경우도 재정의 가능)
  • 물론 스프링이 제공하는 기능을 뚜렷한 목적 없이 무분별하게 재정의하는 것은 유지보수에 더 혼란만 가중 할 수 있으므로 사용에 주의해야함
package hello.core.annotation;

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
// @Qualifier의 내부에 들어가서 붙어있는 모든 애노테이션을 복사

@Qualifier("mainDiscountPolicy")    // 구분자 지정
public @interface MainDiscountPolicy {
}

 

(2) 직접 만든 애노테이션 적용 및 사용

  • 적용할 클래스에 만든 애노테이션 적용
  • 사용할 때는 @Qualifier를 사용한 것처럼 생성자, 수정자의 파라미터에 애노테이션 이름 적용하면 됨
@MainDiscountPolicy // 직접 생성한 애노테이션을 적용
public class RateDiscountPolicy implements DiscountPolicy {}


public class OrderServiceImpl implements OrderService {

    private final DiscountPolicy discountPolicy;
    
    // 생성자 자동 주입
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, 
                       @MainDiscountPolicy DiscountPolicy discountPolicy) {
       // ...
   }
   
    //수정자 자동 주입
    @Autowired
    public DiscountPolicy setDiscountPolicy(@MainDiscountPolicy DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
}

4. 조회한 빈이 모두 필요할 때 - List, Map

  • 위에서 제시한 여러가지 방법 중 한가지로 조회 빈이 2가지 이상일 때의 문제를 해결한 후 진행해야 함

1) 조회한 빈이 모두 필요할 때

  • 의도적으로 해당 타입의 스프링 빈이 다 필요한 경우가 발생할 수 있음
  • 예를 들어 모든 할인 서비스를 제공할 때 클라이언트가 할인의 종류를 선택할 수 있다고 가정

(1) AllBeanTest

  • List와 Map을 모두 사용할 수 있지만 여기서는 Map으로만 사용
  • 실행해보면 fixDiscountPolicy, rateDiscountPolicy의 할인정책이 모두 정상적으로 동작하여 테스트로직이 통과함
package hello.core.autowired;

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, "memberA", Grade.VIP);

        assertThat(discountService).isInstanceOf(DiscountService.class);

        int fixDiscountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");
        assertThat(fixDiscountPrice).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;

        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);

            System.out.println("discountCode = " + discountCode);
            System.out.println("discountPolicy = " + discountPolicy);

            return discountPolicy.discount(member, price);
        }
    }
}

 

(2) 로직 분석

  • DiscountService 클래스는 Map으로 모든 DiscountPolicy(할인정책)를 주입 받으며 이때 fixDiscountPolicy와 rateDiscountPolicy가 주입됨
  • discount()메서드는 discountCode로 넘어온 할인정책을 Map에서 동일한 스프링을 찾아 실행함

(3) 주입 분석

  • Map<String, DiscountPolicy> : map의 키에 스프링 빈의 이름을 map의 값에는 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아줌
  • List<DiscountPolicy> : DiscountPolicy타입으로 조회한 모든 스프링 빈을 담음(현재 테스트코드에선 주입만 받고 사용하지 않음)
  • 만약 해당하는 타입의 스프링 빈이 없으면 빈 컬렉션이나 Map을 주입함

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

  • 스프링 컨테이너는 생성자에 클래스 정보를 받는데 여기에 클래스 정보를 넘기면 해당 클래스가 스프링빈으로 자동으로 등록됨
  • 즉, 해당코드는 2가지로 나누어서 이해할 수 있음
  • 1. new AnnotationConfigApplicationCentext() 를 통해 스프링 컨테이너를 생성
  • 2. AutoAppConfig.class, DiscountService.class 파라미터의 클래스를 자동으로 스프링 빈으로 등록
  • 정리하면 스프링 컨테이너를 생성하면서 동시에 파라미터의 클래스를 자동으로 스프링빈으로 등록함
ApplicationContext ac = new AnnotationConfigApplicationContext
                                (AutoAppConfig.class, DiscountService.class);

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

1) 편리한 자동 기능을 기본으로 사용

  • 스프링이 나오고 시간이 갈수록 점점 자동을 선호하는 추세임
  • 스프링은 @Component뿐 아니라 @Controller, @Service, @Repository처럼 계층에 맞춰 일반적인 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원함
  • 최근 스프링 부트는 컴포넌트 스캔을 기본으로 사용하고 스프링 부트의 다양한 스프링 빈들도 조건이 맞으면 자동으로 등록하도록 설계 되어있음
  • 설정 정보를 기반으로 애플리케이션을 구성하는 부분과 실제 동작하는 부분을 명확히 나누는 것이 이상적이지만 @Component 하나만 넣어주면 끝나는 것을 @Configuration 설정 정보에 @Bean을 적고, 객체를 생성하고, 주입할 대상을 적는 것은 상당히 번거로운 작업이며 관리할 빈이 많아서 설정 정보가 커지면 설정 정보를 관리하는 것 자체가 부담이 됨
  • 자동으로 빈 등록을 사용해도 일부 애노테이션을 수정하는 등의 작업만하면 OCP, DIP를 어느정도 지킬 수 있음

2) 수동 빈 등록은 언제 사용해야 할까?

  • 애플리케이션에 광범위하게 영향을 미치는 기술지원 객체는 수동 빈으로 등록해서 설정 정보에 바로 나타나게 하는 것이 유지보수하기에 좋음

(1) 업무 로직 빈

  • 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리 등이 모두 업무 로직임
  • 보통 비즈니스 요구사항을 개발할 때 추가 되거나 변경이 됨
  • 숫자도 매우 많고 한번 개발해야 하면 컨트롤러, 서비스, 리포지토리처럼 어느정도 유사한 패턴이 있으며 보통 문제가 발생해도 어떤 곳에서 문제가 발생했는지 명확하게 파악하기 쉬움
  • 이런 경우 자동 기능을 적극적으로 사용하는 것이 좋음

(2) 기술 지원 빈

  • 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용
  • 데이터베이스 연결이나, 공통 로그 처리처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들
  • 업무로직과 비교해서 그 수가 적고 보통 애플리케이션 전반에 걸쳐서 영향을 미침
  • 적용이 잘 되고 있는지 아닌지 조차 파악하기 어려운 경우가 많아 가급적 수동 빈 등록을 사용해서 명확하게 드러내는 것이 좋음

(3) 비즈니스 로직 중에서 다형성을 적극 활용할 때

  • 조회한 빈이 모두 필요할 때 작성했던 테스트코드를 보면 DiscountPolicy가 의존관계 자동 주입으로 Map에 어떤 빈들이 주입 될지, 각 빈들의 이름은 무엇인지 한번에 파악할 수 없음(물론 타고 들어가서 보면 알 수 있으나 한눈에 파악이 어렵고 명확하지 않음)
  • 이런 경우 수동 빈으로 등록하거나 자동으로 할 경우 특정 패키지에 묶어 두는 것이 좋음, 핵심은 딱 보고 이해가 되어야 함
  • 아래의 코드처럼 할인 정책 관련된 별도의 설정 정보로 만들어 수동으로 빈 등록을 하면 한눈에 파악하기가 좋으며 만일 자동으로 등록하고자 할 경우 파악하기 좋게 DiscountPolicy의 구현 빈들만 모아서 특정 패키지에 모아두어야 파악하기가 좋음
@Configuration
public class DiscountPolicyConfig {
    @Bean
    public DiscountPolicy rateDiscountPolicy() {
        return new RateDiscountPolicy();
    }
    @Bean
    public DiscountPolicy fixDiscountPolicy() {
        return new FixDiscountPolicy();
    }
}

 

** 참고

  • 스프링과 스프링 부트가 자동으로 등록하는 수 많은 빈들은 예외임
  • 이런 부분들은 스프링 자체를 이해하고 의도대로 잘 사용하는 것이 중요함
  • 스프링부트의 경우 DataSource같은 데이터베이스 연결에 사용하는 기술 지원 로직까지 내부에서 자동으로 등록하는데 이런부분은 매뉴얼을 잘 참고해서 스프링 부트가 의도한 대로 편리하게 사용 하면 됨
  • 스프링부트가 아니라 내가 직접 기술 지원 객체를 스프링 빈으로 등록하면 수동으로 등록해서 명확하게 드러내는 것이 좋음

(4) 정리

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