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

빌드노트

@Primary를 붙였는데 주입된 List의 첫 번째 빈이 아니다

@Primary를 바꿔도 List 주입 순서는 바뀌지 않는다. 세 전략 빈의 단일 선택과 목록을 비교하고, @Order 변경 및 등록 순서 반전 결과로 기본 후보와 처리 순서를 따로 설정하는 방법을 설명한다.

여러 전략을 List<Strategy>로 주입받아 앞에서부터 처리하려는데, @Primary를 붙인 빈이 맨 뒤에 있을 수 있다. @Primary는 하나를 주입할 때의 기본 후보를 고르는 표시다. 목록의 첫 번째 자리를 지정하지 않는다. 주입 조건에 맞는 다른 빈도 목록에 들어간다. 실행 순서가 필요하면 목록의 순서를 @Order 등으로 따로 정해야 한다.

주입받은 목록을 정렬하지 않고 확인한다

먼저 전략 목록을 받는 생성자와 각 전략의 등록 코드를 함께 본다. 단일 매개변수 Strategy selected와 목록 매개변수 List<Strategy> strategies가 있다면, 각각 무엇을 받았는지 따로 출력해야 한다. 아래 두 줄은 그 생성자 본문에 넣는 진단 코드다. 예제의 name()은 전략을 실행하지 않고 이름만 반환한다. 자기 코드에서는 부작용 없는 식별값으로 바꿔 확인한다.

System.out.println("single=" + selected.name());
System.out.println("list=" + strategies.stream().map(Strategy::name).toList());

여기서 sorted()를 붙이면 주입 순서가 사라진다. @Primary가 붙은 빈이 목록에 포함되는지, 어느 위치에 있는지를 그대로 읽어야 한다. Primary API 문서 (새 탭에서 열림)는 단일 주입의 우선 선택과 컬렉션에 들어갈 후보를 구분한다.

@Primary만 옮겨도 목록은 그대로였다

확인을 위해 alpha, beta, gamma 세 합성 빈을 만들었다. Windows 11, OpenJDK 17.0.2, Spring Framework 7.0.10에서 매번 새 AnnotationConfigApplicationContext를 열고 닫았다. 아래는 빈을 정의한 부분이다. 기존 Spring Java 설정에서 비교하려면 @Configuration 클래스 안에 넣고, 앞의 두 매개변수를 받는 소비자도 Spring 빈으로 등록한다. 필요한 의존성은 org.springframework:spring-context:7.0.10과 그 전이 의존성이다. Bean과 Primary는 org.springframework.context.annotation, Order는 org.springframework.core.annotation에서 가져온다.

interface Strategy { String name(); }
record NamedStrategy(String name) implements Strategy {}

@Bean @Primary @Order(30)
Strategy alpha() { return new NamedStrategy("alpha"); }

@Bean @Order(10)
Strategy beta() { return new NamedStrategy("beta"); }

@Bean @Order(20)
Strategy gamma() { return new NamedStrategy("gamma"); }

각 객체의 name()은 실제 빈 이름과 같게 했다. 빈 정의 등록 순서는 alpha, beta, gamma였고, 소비자에는 qualifier를 붙이지 않았다. 주입 직후의 출력은 다음과 같았다.

single=alpha
list=[beta, gamma, alpha]

단일 주입에서는 primary인 alpha가 선택됐다. 목록에는 세 빈이 모두 있었고, 순서는 order 값 10 → 20 → 30이었다. Order API 문서 (새 탭에서 열림)가 설명하는 대로 작은 값이 앞선다.

이제 order 값과 등록 순서는 그대로 두고, alpha()의 @Primary를 제거해 gamma()로 옮겼다. 새 컨텍스트에서 출력한 결과다.

single=gamma
list=[beta, gamma, alpha]

기본 선택만 gamma로 바뀌었다. 목록의 위치는 그대로다. 이 차이는 단일 의존성에 대한 Primary 설명 (새 탭에서 열림)과 일치한다. primary를 바꾸는 작업으로 전략 체인의 순서를 고칠 수는 없다.

alpha를 먼저 처리하려면 order 값을 바꾼다

이번에는 @Primary를 다시 alpha()에만 두고, 그 메서드의 order 값만 30에서 5로 바꿨다. beta의 10과 gamma의 20은 유지했다.

@Bean @Primary @Order(5)
Strategy alpha() { return new NamedStrategy("alpha"); }

새 컨텍스트에서는 단일 선택을 유지하면서 목록의 첫 원소가 바뀌었다.

single=alpha
list=[alpha, beta, gamma]

따라서 기본 전략도 alpha이고 순차 처리의 첫 전략도 alpha여야 한다면 두 의도를 각각 표시한다. 하나만 받는 곳의 기본 선택은 @Primary로, 여러 전략을 받는 곳의 순서는 각 빈의 @Order로 정한다. 목록 순서가 실제 처리 순서가 되려면 소비자도 그 목록을 순차적으로 사용해야 한다.

자기 코드에 적용할 때는 전략 구현의 Ordered.getOrder()나 기존 @Order도 확인한다. @Bean으로 등록한다면 위 예제처럼 각 메서드에 순서를 적을 수 있다. @Configuration 클래스에만 붙인 @Order가 그 안의 모든 빈에 적용된다고 보면 안 된다. Autowired 참조 문서 (새 탭에서 열림)는 설정 클래스의 처리 순서와 개별 빈의 순서를 구분한다.

순서를 생략하면 등록 순서에 기대게 된다

세 메서드의 @Order를 모두 제거하고 alpha의 @Primary만 남겼다. 이 합성 전략에는 Ordered 구현이나 다른 정렬 지정도 없다. 빈 정의를 alpha, beta, gamma 순으로 등록한 컨텍스트의 출력이다.

registered=[alpha, beta, gamma]
single=alpha
list=[alpha, beta, gamma]

정렬 지정은 계속 없는 상태에서 빈 정의 등록 순서를 gamma, beta, alpha로 뒤집었다. 소스 파일에서 메서드의 위아래 위치만 바꾼 실험은 아니다. 별도 설정 클래스를 등록하는 순서를 바꾸고, 컨텍스트가 가진 실제 빈 이름 순서도 함께 출력했다.

registered=[gamma, beta, alpha]
single=alpha
list=[gamma, beta, alpha]

@Primary 덕분에 단일 선택은 여전히 alpha였지만 목록은 등록 순서를 따라갔다. 같은 역순 등록에서 원래의 order 값 alpha=30, beta=10, gamma=20을 복원하자 목록은 다시 [beta, gamma, alpha]였다. 지금 우연히 첫 번째로 들어온 빈이 있다고 해서 그 위치를 처리 정책으로 삼으면 안 되는 이유다. 참조 문서도 정렬 지정이 없을 때는 빈 정의 등록 순서를 따른다고 설명한다. 컴포넌트 스캔의 순서를 소스 파일의 배치로 보장하려 하지 말고, 필요한 우선순위를 빈에 명시한다.

직접 확인한 범위는 세 합성 빈과 생성자 주입, 서로 다른 order 값이다. 실제 서비스 전략의 업무 동작은 실행하지 않았다. order 값이 같은 경우, qualifier로 좁힌 목록, 사용자 정의 비교자와 부모·자식 컨텍스트는 비교하지 않았다. 후보 수가 예상과 다르면 먼저 실제 등록 여부와 해당 주입 지점의 조건을 확인한다.

마지막으로 이 글의 순서는 주입된 목록의 순서다. @Order가 singleton 빈의 생성 순서까지 보장하지 않는다는 점은 공식 문서에 명시돼 있다. 초기화 의존성을 해결할 목적으로 order 숫자를 바꾸는 것은 이 실험에서 확인한 해결 범위에 들어가지 않는다.

← 전체 글 보기