본문 바로가기
빌드노트 검색

빌드노트

Spring getIfAvailable에 기본값을 줬는데 빈 두 개에서 예외가 난다

getIfAvailable의 기본값 Supplier는 빈이 없을 때 호출되며, 선택 기준 없는 빈 두 개의 모호함은 예외로 드러난다. Spring 6.2.11에서 반환값·예외·Supplier 호출 수를 비교해 getIfUnique로 모호함을 허용할지, primary나 qualifier로 빈을 선택할지 설명한다.

Spring의 ObjectProvider.getIfAvailable()에 기본값을 만드는 Supplier를 넘겨도, 같은 타입의 빈 두 개 중 하나를 고를 수 없으면 NoUniqueBeanDefinitionException이 발생한다. 빈이 없는 상태와, 빈은 있지만 선택이 모호한 상태를 다르게 처리하기 때문이다. 모호함까지 기본값으로 처리하려면 getIfUnique()를 쓰고, 두 빈 중 특정 빈을 써야 한다면 선택 조건을 명시한다.

Supplier가 있는데도 호출되지 않는 이유

먼저 예외가 난 소비자의 조회 코드를 본다. 아래에서 channels는 Spring이 주입한 ObjectProvider<Channel>이다. 예제 타입 Channel에는 String id() 메서드 하나가 있고, fallback도 문자열만 반환한다. 기존 코드에서는 자신의 타입과 기본값 생성 함수를 넣어 읽으면 된다.

Channel selected = channels.getIfAvailable(() -> () -> "fallback");

이 호출은 후보가 없을 때 fallback을 만든다. 후보가 여러 개일 때 아무 빈이나 고르거나, 조회 예외가 나면 Supplier로 넘어간다는 뜻은 아니다. Spring 6.2.11의 ObjectProvider API (새 탭에서 열림)는 인자 없는 getIfAvailable()이 부재에는 null, 단일 후보를 정하지 못하는 복수 후보에는 예외를 내는 것으로 구분한다.

Supplier를 받는 메서드도 먼저 인자 없는 조회를 실행한다. 그 결과가 null일 때만 Supplier를 호출한다. 조회 자체가 예외로 끝나면 기본값을 만드는 단계에 도달하지 못한다. 이 순서는 6.2.11 공식 구현의 두 Supplier 오버로드 (새 탭에서 열림)에서 확인할 수 있다.

따라서 이 예외를 보고 기본값 생성 함수부터 고칠 필요는 없다. 먼저 예외에 나오는 후보 이름과, 어느 후보를 선택하려던 코드인지 확인한다. 아래는 합성 빈 email, sms로 실행했을 때 메시지에서 후보를 알리는 부분이다.

expected single matching bean but found 2: email,sms

빈 수를 바꾸니 기본값 호출 조건이 갈렸다

Windows의 OpenJDK 17.0.2와 Spring Framework 6.2.11에서 새 컨텍스트를 각각 열어 비교했다. Channel 빈이 0개인 경우, email 하나인 경우, email과 sms 둘인 경우, 둘 중 email에만 @Primary를 붙인 경우다. 소비자에는 같은 ObjectProvider<Channel>을 주입했고, primary 대조 외에는 후보를 고를 qualifier나 우선순위를 지정하지 않았다. 컴포넌트 스캔과 앱 설정은 사용하지 않았다.

각 컨텍스트에서 네 메서드를 한 번씩 호출했다. Supplier 두 개의 호출 수는 별도 카운터로 셌으며, fallback은 빈으로 등록하지 않았다.

  • 빈 0개: getIfAvailable()과 getIfUnique() 모두 null이었다. Supplier를 넘긴 두 호출은 모두 fallback을 반환했고, 각 Supplier가 1회 호출됐다.
  • email 1개: 네 호출 모두 등록된 email 빈을 반환했다. 두 Supplier의 호출 수는 각각 0회였다.
  • email·sms 2개, 선택 조건 없음: getIfAvailable()과 getIfAvailable(Supplier) 모두 NoUniqueBeanDefinitionException이 발생했다. 후자의 Supplier 호출 수는 0회였다. getIfUnique()는 null, getIfUnique(Supplier)는 fallback을 반환했으며 이 Supplier는 1회 호출됐다.
  • email·sms 2개, email만 primary: 네 호출 모두 email 빈을 반환했다. 두 Supplier는 각각 0회 호출됐다.

반환된 빈은 이름만 비교하지 않고 컨텍스트에 등록된 객체와 같은 참조인지도 확인했다. 각 경우의 컨텍스트는 조회를 마친 뒤 닫았다. 이 결과에서 중요한 차이는 빈이 둘이라는 숫자 자체보다 하나를 선택할 기준이 있는지다. getIfUnique()도 물리적으로 빈이 꼭 하나일 때만 반환하는 메서드는 아니다.

모호함을 허용할 것인가, 설정 오류로 볼 것인가

선택 기능을 끄거나 기본 동작으로 돌아가도 되는 소비자라면, 앞의 조회 한 줄을 다음처럼 바꿀 수 있다. 이 변경은 이번처럼 별도 선택 기준이 없는 두 빈을 부재와 같은 fallback 경로로 보낸다.

Channel selected = channels.getIfUnique(() -> () -> "fallback");

위 비교의 빈 0개와 일반 빈 2개에서 이 코드는 fallback을 선택했다. 다만 두 빈이 잘못 중복 등록된 상황도 같은 결과가 된다. 반드시 email이나 sms 중 하나를 사용해야 하는 코드라면, 오류를 없애려고 이 메서드로 바꾸면 설정 문제를 놓칠 수 있다.

반대로 빈이 없는 것은 허용하되, 여러 후보 사이의 모호함은 오류로 드러내고 싶다면 getIfAvailable(Supplier)를 유지한다. 공통 기본 후보가 email이라면 기존 설정의 email 메서드에만 @Primary를 붙인다. 아래는 @Configuration 클래스 안의 두 빈 정의다.

@Bean @Primary Channel email() { return () -> "email"; }
@Bean Channel sms() { return () -> "sms"; }

이 조건에서는 getIfUnique(Supplier)도 fallback을 쓰지 않고 email을 선택했다. 6.2.11 Primary 설명 (새 탭에서 열림)도 적합한 후보 가운데 primary가 정확히 하나면 그 빈을 우선한다고 설명한다. 여러 빈을 등록하되 기본 후보를 정한 설계라면 이 선택을 유지하면 된다.

공통 기본값을 바꿀 이유 없이 이 소비자만 email을 써야 한다면, Provider를 받는 생성자나 Bean 메서드의 매개변수에 @Qualifier("email")을 붙인다. 아래는 기존 매개변수 선언에서 바꿀 부분이다.

@Qualifier("email") ObjectProvider<Channel> channels

primary가 없는 email·sms 두 빈을 둔 다섯 번째 새 컨텍스트에서 이 조건도 실행했다. 네 조회 모두 email을 반환했고 Supplier 호출 수는 각각 0회였다. 공식 qualifier 설명 (새 탭에서 열림)처럼 qualifier는 타입에 맞는 후보를 좁힌다. 이 예제에서는 빈 이름 email이 매칭 값이 된다. 해당 참조 문서는 확인 당시 6.2.19였고, 위 실행 결과는 6.2.11에서 얻었다.

직접 적용할 때 ObjectProvider의 import는 org.springframework.beans.factory.ObjectProvider, Qualifier는 org.springframework.beans.factory.annotation.Qualifier, Bean과 Primary는 org.springframework.context.annotation 패키지다. 별도 Java 예제로 확인하려면 JDK 17 이상과 org.springframework:spring-context:6.2.11 및 전이 의존성을 준비한다. 기존 Spring 앱에서는 이미 등록된 빈과 Provider의 조회 지점에 위 변경을 적용하고, 반환된 대상과 fallback 호출 여부를 함께 확인한다. 결과가 다르면 빈 개수만 세지 말고 해당 주입 지점의 qualifier와 후보의 primary 표시도 확인한다.

getIfUnique()가 모든 예외를 fallback으로 바꾸는 것은 아니다. API는 빈 생성 오류가 전파될 수 있음을 명시한다. 이번에 직접 확인한 범위는 정상 생성되는 singleton 합성 빈과 위 다섯 선택 조건이다. 여러 primary의 충돌, 빈 생성 실패, 부모·자식 컨텍스트, 비활성 scope는 실행하지 않았다. 부재만 허용하려는지, 선택 기준 없는 복수 후보도 허용하려는지를 먼저 정해야 두 조회 메서드 사이의 변경이 의도한 동작이 된다.

← 전체 글 보기