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

빌드노트

@Bean 메서드를 직접 불렀더니 주입된 객체와 다르다

proxyBeanMethods=false에서 @Bean 메서드를 직접 호출하면 등록된 singleton과 별도의 객체가 생길 수 있다. 독립 컨텍스트 네 조건의 생성 수와 == 결과를 비교하고, 두 팩터리 메서드를 매개변수 주입으로 연결하는 수정안을 설명한다.

@Configuration(proxyBeanMethods = false)로 바꾼 뒤, 다른 @Bean 메서드에서 client()를 불렀더니 객체가 하나 더 만들어질 수 있다. 이 설정은 singleton을 해제하지 않는다. 설정 메서드의 직접 호출을 컨테이너 조회로 연결하던 처리를 끄므로, 메서드 안의 new Client()가 그대로 실행된다. 등록된 빈을 공유하려면 @Bean 메서드의 매개변수로 받는다.

직접 호출 한 줄을 매개변수로 옮긴다

자기 설정 클래스에서 의존성을 만드는 부분을 먼저 확인한다. 아래는 Client를 기본 생성자로 만들고, OrderService는 생성자로 받은 참조를 보관하는 경우다. @Bean과 @Configuration의 import는 각각 org.springframework.context.annotation.Bean, org.springframework.context.annotation.Configuration이다.

@Configuration(proxyBeanMethods = false)
class ShopConfig {
    @Bean
    Client client() { return new Client(); }

    @Bean
    OrderService orderService() { return new OrderService(client()); }
}

여기서 orderService() 안의 client()는 일반 Java 호출이다. 컨테이너가 client 빈을 만들기 위해 실행하는 호출과 별개로 new Client()에 도달한다. @Bean이 붙어 있다고 해서 이 직접 호출의 반환값까지 자동으로 빈으로 등록되는 것은 아니다.

client() 선언은 두고, orderService()만 아래처럼 교체한다. proxyBeanMethods = false도 유지한다.

@Bean
OrderService orderService(Client dependency) {
    return new OrderService(dependency);
}

컨테이너는 orderService 빈을 만들 때 매개변수에 맞는 Client 빈을 찾아 전달한다. Client 후보가 하나인 이 예제에서는 별도의 @Autowired가 필요 없다. 매개변수 이름을 client가 아닌 dependency로 둔 것도 이름 일치에 기대지 않기 위해서다. 메서드 매개변수로 빈의 의존성을 표현할 수 있다는 것은 Spring의 Bean Dependencies 설명 (새 탭에서 열림)에 나오는 규칙이다.

같은 두 선언을 네 조건에서 실행했다

Windows, OpenJDK 17.0.2, Spring Framework 6.2.11에서 외부 자원을 사용하지 않는 Client와 OrderService만 넣어 비교했다. 의존성은 org.springframework:spring-context:6.2.11과 전이 의존성을 사용했다. 조건마다 새 AnnotationConfigApplicationContext를 열고 닫았으며, Client 생성자에서 증가하는 카운터도 매번 0으로 시작했다. 두 빈은 모두 기본 singleton이고 지연 초기화는 지정하지 않았다.

첫 코드를 그대로 둔 직접 호출과, 위 수정처럼 매개변수로 받은 경우를 각각 실행했다. full은 기본 @Configuration이고 lite는 @Configuration(proxyBeanMethods = false)다. 다음은 실제 출력이다. created는 컨텍스트 초기화 직후의 Client 생성 수, service.client==bean은 서비스가 보관한 참조와 등록된 빈의 == 비교, lookup==lookup은 같은 빈을 두 번 조회한 비교다.

full/direct: created=1; service.client==bean=true; lookup==lookup=true
lite/direct: created=2; service.client==bean=false; lookup==lookup=true
full/parameter: created=1; service.client==bean=true; lookup==lookup=true
lite/parameter: created=1; service.client==bean=true; lookup==lookup=true

lite의 직접 호출에서도 컨테이너에 등록된 Client 빈은 하나였다. getBean()을 두 번 호출하면 같은 참조를 반환했고 생성 수도 더 늘지 않았다. 추가 객체는 orderService()가 일반 호출로 만든 것이었다. 생성된 객체 두 개와 등록된 singleton 빈 두 개는 다른 상황이다.

full 설정에서는 Spring이 생성한 CGLIB 하위 클래스가 설정 메서드 사이의 호출을 가로채 컨테이너의 빈 관리로 연결한다. 이번 singleton 비교에서는 그 덕분에 직접 호출도 같은 참조를 받았다. lite에는 이 처리가 없다는 것이 공식 full·lite 설명 (새 탭에서 열림)이다. 이 차이는 Spring이 등록하고 처리한 설정 객체를 사용할 때의 이야기다. 직접 new ShopConfig()로 만든 객체에는 full 설정의 가로채기도 적용되지 않는다.

직접 호출을 유지해야 한다면 기본값인 proxyBeanMethods = true를 사용하는 선택도 있다. 기본값과 가로채기 조건은 6.2.11 Configuration API (새 탭에서 열림)에서 확인할 수 있다. 메서드끼리 호출할 필요가 없다면 매개변수로 관계를 드러내면 된다. 이번 실행에서는 이 방식이 full과 lite에서 같은 결과를 냈다.

자기 코드에서는 서비스가 보관한 참조를 비교한다

생성 로그만으로는 어느 객체가 실제 서비스에 들어갔는지 알기 어렵다. 자신의 컨텍스트 테스트에서 이미 초기화한 ApplicationContext를 ctx로 두고, 아래처럼 서비스 안의 참조와 이름으로 조회한 빈을 비교한다. Client, OrderService, 빈 이름 client는 실제 대상으로 바꾼다. 여기서 client()는 OrderService가 보관한 의존성을 그대로 반환하는 접근자다. 예제에서는 record OrderService(Client client) {}를 사용했다.

Client registered = ctx.getBean("client", Client.class);
OrderService service = ctx.getBean(OrderService.class);
System.out.println(service.client() == registered);

위 비교는 equals()가 아니라 객체 동일성을 확인한다. 이번 lite 직접 호출에서는 false, 매개변수로 바꾼 뒤에는 true였다. full 설정의 가로채기에 의존하던 연결이 있는지 찾을 때도 이 비교를 사용할 수 있다.

매개변수로 바꾼 뒤 후보가 모호하다는 오류가 나면 같은 타입의 빈이 여럿인지 확인하고, 필요한 후보를 @Qualifier 등으로 지정한다. 비교가 계속 false라면 서비스가 받은 참조 대신 새 객체를 보관하는지, 비교 대상의 빈 이름과 scope가 맞는지 확인한다. 이 글의 결과는 기본 singleton 하나를 공유하는 조건이다.

링크한 6.2 참조 문서는 확인 당시 6.2.19를 표시했다. full·lite 구분과 매개변수 주입은 실행 버전인 6.2.11의 basic-concepts 원본 (새 탭에서 열림), bean-annotation 원본 (새 탭에서 열림)에서도 대조했다. 직접 실행한 범위는 일반 인스턴스 @Bean 메서드 네 조건이며, Boot 자동 구성·prototype·별도 AOP 프록시가 섞인 경우는 검증하지 않았다.

← 전체 글 보기