Backend/Spring

스프링 빈 등록과 조회

olsohee 2023. 12. 29. 16:32

스프링 컨테이너에서 빈 조회 

다음 메소드를 통해 스프링 컨테이너에 있는 빈을 조회할 수 있다.

  • getBeanDefinitionNames(): 스프링에 등록된 모든 빈 이름을 조회한다. 따라서 사용자가 정의한 빈 외에 스프링이 내부에서 사용하는 빈들도 함께 조회된다.
  • 스프링이 내부에서 사용하는 빈은 제외하고 사용자가 등록한 빈만 조회하려면, getRole() 메소드를 통해 빈을 구분하면 된다.
    • ROLE_APPLICATION일반적으로 사용자가 정의한 빈
    • ROLE_INFRASTRUCTURE스프링이 내부에서 사용하는 빈
  • getBean(): 빈 이름 또는 타입으로 빈을 조회한다. (ex, getBean("memberService", MemberService.class))
    • 조회하려는 빈이 없으면 NoSuchBeanDefinitionException 예외가 발생한다.
    • 타입으로 조회시 같은 타입의 빈이 둘 이상이면 NoUniqueBeanDefinitionException 예외가 발생한다.
    • 타입으로 조회시 부모 타입으로 조회하면 자식 타입도 함께 조회된다. 따라서 getBean() 메소드를 통해 인터페이스 타입으로 빈을 조회할 때 그 구현체가 둘 이상이면 NoUniqueBeanDefinitionException 예외가 발생한다.
  • getBeansOfType() 메소드를 사용하면 해당 타입의 모든 빈을 조회할 수 있다. 이때는 빈 이름이 key, 빈 객체가 value인 Map이 반환된다.

컴포넌트 스캔으로 인한 빈 등록 시 빈 이름 충돌

컴포넌트 스캔에서 스프링 빈 이름이 중복되면 어떻게 될까?

  • 자동 빈 등록 vs 자동 빈 등록
    • 컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 이때 이름이 중복되면 ConflictingBeanDefinitionException 예외가 발생한다. 
  • 수동 빈 등록 vs 자동 빈 등록
    • 이 경우 수동 빈 등록이 우선권을 가진다. (수동 빈이 자동 빈을 오버라이딩해버린다.)
    • 그러나 현실적으로 개발자의 의도가 아닌 의도치 않게 빈 이름이 중복되는 경우가 많다. 따라서 스프링 부트는 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 기본 설정을 해두었다
    • 만약 오류가 발생하지 않고 수동 빈이 우선권을 가지며 빈이 등록되기를 원하면 기본 설정을 바꾸면 된다.

조회 빈이 2개 이상일 때 해결 방법

@Autowired는 빈을 조회할 때 타입으로 조회한다. 따라서 다음 예제의 경우 getBean(DiscountPolicy.class)와 유사하게 동작한다.

@Autowired
private DiscountPolicy discountPolicy;

 

그런데 스프링은 타입으로 조회할 때 그 하위 클래스까지 조회한다. 따라서 만약 DiscountPolicy 인터페이스를 구현한 FixDiscountPolicy와 RateDiscountPolicy 둘 다 빈으로 등록되어 있으면, 두 개의 빈이 조회된다. 따라서 이때 NoUniqueBeanDefinitionException 예외가 발생한다.

 

이렇게 두 개 이상의 빈이 조회될 때 해결방법은 다음과 같다.

@Autowired

@Autowired는 타입으로 매칭을 시도하고(이때는 빈 이름이 중요하지 않음), 조회되는 빈이 두 개 이상이면 필드 이름 또는 파라미터 이름으로 빈 이름을 추가 매칭한다.

  1. 타입으로 빈을 찾는다.
  2. 타입 매칭 결과가 두 개 이상이면 필드명 또는 파라미터명을 기반으로 빈 이름을 찾는다.

@Qualifier 

@Qualifier는 추가 구분자를 붙여주는 방법이다. 이는 추가 구분자일 뿐 빈 이름을 변경하는 것은 아니다.

 

다음과 같이 빈에 등록될 객체에 추가 구분자를 붙여주고,

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

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

빈을 조회할 때도 추가 구분자를 붙여주면 된다.

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

만약 mainDiscountPolicy라는 추가 구분자를 가진 빈을 찾지 못하면, mainDiscountPolicy라는 빈 이름으로 다시 찾는다. 

  1. @Qualifier로 지정한 구분자를 가진 빈을 찾는다.
  2. @Qualifier로 지정한 빈 이름을 가진 빈을 찾는다.
  3. NoSuchBeanDefinitionException 예외 발생

그런데 @Qualifier("mainDiscountPolicy")와 같이 문자를 적으면 오타가 발생해도 컴파일 시 체크가 되지 않는다. 따라서 다음과 같이 어노테이션을 만들어서 활용하면 실수를 방지할 수 있다.

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy") // 여기에 추가 구분자를 붙여주면 된다.
public @interface MainDiscountPolicy {
}

이제는 문자가 아닌 어노테이션을 사용하면 되므로 실수를 방지할 수 있다.

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

@Primary

@Primary는 우선순위를 정하는 방법이다. 여러 빈이 조회될 때, 우선권을 가질 빈의 클래스에 @Primary를 붙여주면 해당 객체가 우선권을 갖는다.

 

다음 예제의 경우 DiscountPolicy 타입으로 빈 조회 시, RateDiscountPolicy가 우선권을 갖는다.

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
public class FixDiscountPolicy implements DiscountPolicy {}

활용 예시 1: @Primary와 @Qualifier 동시 사용

코드에서 자주 사용하는 메인 데이터베이스의 커넥션을 획득하는 스프링 빈이 있고, 코드에서 특별한 기능으로 가끔 사용하는 서브 데이터베이스의 커넥션을 획득하는 스프링 빈이 있다고 생각해보자. 메인 데이터베이스의 커넥션을 획득하는 스프링 빈은 @Primary를 적용해서 조회하는 곳에서 @Qualifier 지정 없이 편리하게 조회하고, 서브 데이터베이스 커넥션 빈을 획득할 때는 @Qualifier를 지정해서 명시적으로 획득하는 방식으로 사용하면 코드를 깔끔하게 유지할 수 있다

 

활용 예시 2: 여러 개 조회된 빈이 모두 필요할 때

여러 개의 빈이 조회될 때, 의도적으로 해당 타입의 스프링 빈들이 모두 필요한 경우도 있다(ex, 할인 서비스를 제공하는데 클라이언트가 할인의 종류(rate, fix)를 선택할 수 있는 경우). 이때는 다음 예제처럼 DiscountPolicy 타입의 스프링 빈을 Map에 모두 받을 수 있다(List도 가능).

@Component
public class DiscountService {

	private final Map<String, DiscountPolicy> policyMap;
	private final List<DiscountPolicy> policies;
    
	@Autowired
	public DiscountService(Map<String, DiscountPolicy> policyMap) {
		this.policyMap = policyMap;
	}
}

Reference

  • 인프런, 스프링 핵심 원리 - 기본편, 김영한