BackEnd/Spring

의존성 주입 시, 같은 Type의 빈이 2개 이상일 경우

PgmJUN 2024. 4. 13. 14:29

 

 

우리는 스프링을 사용할 때, 생성자 주입을 통해 의존성을 주입하곤 한다.

 

 

DiscountPolicy 정적 의존관계

 

그런데 의존성을 주입하는 경우, 2개의 의존성 중 하나를 주입해야하는 경우가 있을 것이다.

 

public interface DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
@Component
public class RateDiscountPolicy implements DiscountPolicy {}

 

@Autowired
private DiscountPolicy discountPolicy

 

만약 위와 같이 FixDiscountPolicy와 RateDiscountPolicy가 스프링 빈으로 존재할 때,
다음과 같이 @Autowired를 수행하면 어떻게 될까?

 

 

예외 발생

NoUniqueBeanDefinitionException: No qualifying bean of type
 'hello.core.discount.DiscountPolicy' available: expected single matching bean
 but found 2: fixDiscountPolicy,rateDiscountPolicy

 

결과는 다음과 같이 NoUniqueBeanDefinitionException 예외가 발생할 것이다.

1개의 빈이 매칭되어야 하지만, 2개의 빈이 매칭되었기 때문에 예외가 발생하는 것이다.

 

@Autowired는 Type으로 빈을 매칭하기 때문에
`applicationContext.getBean(DiscountPolicy.class)` 이렇게 빈을 조회하는 것과 유사하다.

때문에 두 구현체 중에 어떤 것을 선택할 지 스프링이 정하지 못해서 예외가 발생한다.

 

 


 

문제 해결 방안

이에 대한 해결 방안으로 다음 3가지 방식이 존재한다.

  • @Autowired 필드 명 매칭
  • @Qualifier 사용
  • @Primary 사용

 

1. @Autowired 필드 명 매칭

@Autowired는 타입 매칭을 시도할 때 같은 타입의 빈이 여러 개 존재하면 필드 이름, 파라미터 이름으로 추가 매칭을 시도하는 방식이다.

 

 

@Autowired
private DiscountPolicy discountPolicy

 

이렇게 타입의 이름을 필드 명으로 사용하는 대신

 

@Autowired
private DiscountPolicy rateDiscountPolicy

 

이렇게 타입의 이름을 필드 명으로 정의하게 되면 동일한 타입의 빈이 2개 이상 있을 경우,
필드 명과 스프링 빈의 이름을 매칭하여 의존성 주입을 할 수 있다.

 

 

2. @Qualifier 사용

Qualifier는 '예선의' 라는 뜻으로, 추가 구분자를 붙여주고 의존성 주입 시 어떤 빈을 주입할 지 지정하여
충돌없이 의존성 주입을 수행할 수 있는 방식이다.

 

ComponentScan 등록 시 예시

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}

 

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

 

다음과 같이 빈 등록 시, @Qualifier 애노테이션과 함께 추가 구분자(이름)를 설정하면 된다.

 

 

@Bean 애노테이션을 통한 빈 직접 등록 시 예시

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

 

@Bean 애노테이션을 통해 빈을 직접 등록하는 경우에도 다음과 같이 @Qualifier 애노테이션을 사용할 수 있다.

 

 

사용법

 

생성자 자동 주입 예시

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

 

생성자 자동 주입을 하는 경우, 생성자의 파라미터 옆에 @Qualifier 애노테이션과 함께 어떤 이름의 빈을 주입시킬 지 정할 수 있다.

 

수정자 자동 주입 예시

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

 

수정자 자동 주입을 하는 경우, 수정자 메서드의 파라미터 옆에 @Qualifier 애노테이션과 함께 어떤 이름의 빈을 주입시킬 지 정할 수 있다.

 

 

@Qualifier 구분자를 찾지 못할 경우

 

만약 @Qualifier("mainDiscountPolicy")를 찾지 못한다면 어떻게 될까?

그렇게 되면 스프링은 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다.


하지만 @Qualifier는 @Qualifier를 찾는 용도로만 사용하는 것이 명확하고 유지보수성과 휴먼에러 방지에 탁월하다.

 

 

@Primary 사용

@Primary는 여러 빈이 매칭되는 경우에 우선 순위를 부여할 수 있는 애노테이션이다.

같은 타입의 빈이 여러 개 조회되는 경우, @Primary 애노테이션을 가진 빈이 우선권을 가진다.

 

 

사용법

 

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
public class FixDiscountPolicy implements DiscountPolicy {}

 

사용법은 다음과 같이 우선권을 부여할 빈에 애노테이션을 붙여주면 끝이다.

 

@Qualifier 를 사용할 경우 해당 애노테이션을 사용하는 모든 생성자 주입 과정에는

@Qualifier 애노테이션을 통해 주입할 빈을 특정해주는 코드를 작성해주어야 하는데

 

그 방식에 비해 굉장히 간단하고 쉬운 방식이다.

 

 


 

@Primary, @Qualifier 함께 사용하는 방법

만약 서비스가 대체로 사용되는 메인DB와, 가끔 특정 동작을 위해 사용되는 서브DB가 모두 연결되었다고 가정하자.

그럼 서비스는 메인DB의 커넥션을 사용하는 빈과, 서브DB의 커넥션을 사용하는 빈이 따로 존재할 것이다.

 

이렇게 2개의 빈이 모두 사용되는데 하나는 자주 사용되고, 하나는 가끔 사용되는 경우 @Primary, @Qualifier를 함께 사용함으로써
편리하게 사용이 가능하다.

 

 

스프링은 자동 보단 수동이, 넓은 범위의 선택권 보다는 좁은 범위의 선택권이 우선순위가 훨씬 높다.

 

때문에 @Qualifier 애노테이션 설정이 없는 경우, @Primary로 설정해놓은 빈이 주입될 것이고

@Qualifier를 설정한 경우 해당 설정이 적용될 것이다.

 

 


 

Custom Annotation을 통한 @Qualifier 사용 방식 개선

 

@Qualifier는 위에서 설명한 예제와 같이 사용하면 정말 편리하게 의존성을 조절하며 개발을 해나갈 수 있다.

 

하지만 @Qualifier는 한 가지 단점이 존재한다.

 

 

컴파일 시점에서 구분자 오류가 잡히지 않음

 

@Qualifier는 컴파일 시점에서 구분자(이름)이 잘못되었음을 체크하지 못한다.

때문에 런타임에서 예외가 발생하게 된다.

 

뿐만 아니라 문자열로 이름을 설정해주기 때문에, 이름에 변경이 발생하면 모든 @Qualifier 애노테이션을 찾아다니면서
이름을 변경해주어야 한다.

 

이러한 문제를 해결해야 @Qualifier를 더욱 안전하게 사용할 수 있을 것 같은데, 어떻게 해결할 수 있을까?

 

 

Custom Annotaion 만들기

 

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)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {}

 

위 문제는 직접 커스텀 애노테이션을 만들어 해결책을 제시해볼 수 있다.

 

커스텀 애노테이션 내부에 정확한 구분자를 설정해놓은 @Qualifier 애노테이션을 추가해놓으면

매번 @Qualifier를 사용할 때마다, 구분자를 문자열로 설정해줄 필요없이 애노테이션만 추가해주면 되어

문자열을 잘못 입력함으로써 발생하는 런타임 오류를 방지할 수 있을 것이다.

 

뿐만 아니라 구분자가 변경된 경우 해당 애노테이션 내부의 @Qualifier 애노테이션의 구분자만 수정해주면 전체적으로 변경이 발생하기에 관리가 편리해진다.

 

 

사용법

 

생성자 자동 주입 예시

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

 

생성자 자동 주입을 하는 경우, 생성자의 파라미터에 애노테이션을 설정하여 원하는 빈을 주입시킬 수 있다.

 

 

수정자 자동 주입 예시

@Autowired
 public DiscountPolicy setDiscountPolicy(@MainDiscountPolicy DiscountPolicy discountPolicy) {
     this.discountPolicy = discountPolicy;
 }

 

수정자 자동 주입을 하는 경우, 수정자 메서드의 파라미터에 애노테이션을 설정하여 원하는 빈을 주입시킬 수 있다.

 

 


 

조회한 빈이 모두 필요한 경우 (with. Collection)

 

개발을 하다보면 같은 타입의 빈이 모두 필요한 경우도 존재할 것이다.

예를 들어서 할인 서비스를 제공하는데, 클라이언트가 할인의 종류(rate, fix)를 선택할 수 있다고 가정해보자.

스프링을 사용하면 소위 말하는 전략 패턴을 매우 간단하게 구현할 수 있어 이러한 문제에 간편한 해결책을 제시한다.

 

 

예제 코드

예제코드를 통해 어떻게 이 문제를 해결할 수 있는 지 알아보자.

 

import hello.core.AutoAppConfig;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
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;
import static org.assertj.core.api.Assertions.assertThat;
 
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, "userA", Grade.VIP);
        int discountPrice = discountService.discount(member, 10000,
"fixDiscountPolicy");

        assertThat(discountService).isInstanceOf(DiscountService.class);
        assertThat(discountPrice).isEqualTo(1000);
    }
    
    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);
		}
	}
}

 

고정 할인 정책(fixDiscountPolicy)은 1000원을 고정으로 할인해주는 전략이다.

우리는 클라이언트로 부터 "fixDiscountPolicy" 라는 discountCode를 입력받은 상태이고,
이에 맞게 고정 할인 정책을 적용해주어야 하는 상황이다.

 

DiscountService를 살펴보면

Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies 두 개의 필드가 존재하는 것을 알 수 있다.

 

이 컬렉션들에는 생성자 주입을 통해 무언가가 주입되는데 무엇이 존재되는 것일까?

 

 

주입 동작 설명

 

Map<String, Type>

 

Map<String,DiscountPolicy> 형태의 필드가 존재하면 스프링은 의존성 주입 시, DiscountPolicy 타입의 빈을 모두 탐색한다.

그리고 Key(String) 에는 discountPolicy 빈의 이름, Value(DiscountPolicy) 에는 빈 객체를 삽입한 Map을 생성하여

의존성을 주입한다.

 

 

List<Type>

 

List<DiscountPolicy> 형태의 필드가 존재한다면 스프링은 의존성 주입 시, DiscountPolicy 타입의 빈을 모두 탐색한다.

그리고 List 내부에 DiscountPolicy 타입의 빈을 모두 담아 의존성을 주입한다.

 

 

찾는 타입의 빈이 존재하지 않을 경우

 

찾는 타입의 빈이 존재하지 않는 경우 빈 컬렉션이 주입된다.

 

 

컬렉션 주입 사용 시 주의점!

 

해당 로직과 같이 Map,List를 통해 다형성을 적극적으로 활용하는 경우 내가 개발한 코드라면 이해하기 쉽겠지만

남이 개발한 로직인 경우,

컬렉션에 어떤 빈들이 주입될 지 단번에 파악하기가 어려울 것이다.

 

때문에 해당 방식을 사용하는 경우 같은 타입의 빈을 Config 파일에서 수동으로 등록하여 응집도를 높게 관리하거나,

같은 타입의 빈과 다형성 사용 객체를 특정 패키지에 같이 묶어 관리하는 것이 코드의 가독성에 도움이 된다.

 

 

 

728x90
반응형