이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • @Configuration 클래스의 @Bean 메서드는 왜 일반 Java 메서드 호출과 다르게 보아야 하는가?
  • Spring은 @Bean 메서드 간 호출에서 어떻게 싱글톤을 보장하는가?
  • @Configuration(proxyBeanMethods=false)는 언제 안전하고 언제 위험한가?

개요

Spring 공식 문서는 Java 설정의 핵심 산물을 @Configuration 클래스와 @Bean 메서드로 설명한다. @Bean 메서드는 객체를 생성하고 설정하고 초기화해서 Spring IoC Container가 관리하도록 만든다. @Configuration 클래스는 단순 factory 객체가 아니라 BeanDefinition의 출처이며, full @Configuration 모드에서는 @Bean 메서드끼리 호출하는 inter-bean dependency까지 컨테이너 관리 흐름으로 해석된다.

기존 MVC 하위 문서에 정리되어 있던 @Configuration 프록시, CGLIB 설정 클래스, @Bean 메서드 호출 시 기존 등록 Bean 반환 내용은 모두 이 주제에 흡수된다. 핵심은 @Configuration 프록시가 AOP용 부가 기능 프록시가 아니라 싱글톤과 생명주기 보장을 위한 컨테이너 장치라는 점이다.

원리

다음 설정을 보자.

@Configuration
public class AppConfig {
 
    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
 
    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }
}

순수 Java 관점에서는 memberService()memberRepository()를 호출하므로 새 MemoryMemberRepository가 만들어질 것처럼 보인다. 그러나 Spring의 full @Configuration 모드에서는 설정 클래스가 CGLIB 기반 프록시로 처리된다. 이 프록시는 @Bean 메서드 호출을 가로채고 컨테이너에 이미 등록된 Bean이 있으면 그것을 반환한다.

CGLIB 설정 클래스

Spring은 @Configuration 클래스를 그대로 사용하는 대신 하위 클래스를 만들어 @Bean 메서드 호출을 제어할 수 있다. 런타임 클래스 이름이 AppConfig$$SpringCGLIB$$0처럼 보일 수 있는 이유가 여기에 있다. full @Configuration 프록시가 필요하다면 설정 클래스가 CGLIB으로 확장될 수 있어야 하므로 final 클래스로 고정하는 식의 설계는 피해야 한다.

이 프록시는 다음 흐름으로 동작한다.

  1. @Bean 메서드 호출이 발생한다.
  2. 컨테이너에 해당 Bean이 이미 있는지 확인한다.
  3. 있으면 기존 Bean을 반환한다.
  4. 없으면 원본 메서드를 호출해 객체를 만들고 Bean으로 등록한다.

그래서 같은 @Bean 메서드를 여러 번 호출하는 코드처럼 보여도 결과적으로 컨테이너의 싱글톤 원칙이 유지된다.

AOP 프록시와의 차이

@Configuration 프록시는 프록시라는 점에서 AOP와 비슷해 보이지만 목적이 다르다.

  • AOP 프록시는 Target 메서드 호출 전후에 Advice를 적용한다.
  • @Configuration 프록시는 @Bean 메서드 호출을 컨테이너의 Bean 조회와 생명주기 관리로 연결한다.

따라서 @Configuration 프록시는 로깅이나 트랜잭션 같은 횡단 관심사를 위한 것이 아니라 Bean 중복 생성을 막고 inter-bean dependency를 컨테이너 관리 안으로 넣기 위한 장치다.

proxyBeanMethods=false

@Configuration(proxyBeanMethods=false)는 full @Configuration 클래스의 @Bean 메서드 호출 가로채기를 끄는 선택이다. 이 방식은 CGLIB subclassing 비용을 줄이고 설정 클래스를 더 단순하게 만들 수 있지만, 같은 클래스 안의 @Bean 메서드 호출을 컨테이너 조회로 바꾸어 주지는 않는다.

하지만 같은 설정 클래스 안에서 @Bean 메서드끼리 직접 호출해 의존성을 표현하면 위험하다. 프록시가 없으므로 그 호출은 일반 Java 메서드 호출처럼 동작한다. 새 객체가 만들어질 수 있고, 컨테이너가 관리하는 Bean과 다른 인스턴스가 생길 수 있다.

안전한 방식은 @Bean 메서드 파라미터로 의존성을 받는 것이다.

@Configuration(proxyBeanMethods = false)
public class AppConfig {
 
    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
 
    @Bean
    public MemberService memberService(MemberRepository memberRepository) {
        return new MemberService(memberRepository);
    }
}

이렇게 하면 메서드 직접 호출이 아니라 컨테이너가 파라미터를 주입하므로 proxyBeanMethods=false에서도 의존 관계가 안전하게 표현된다. 즉 proxyBeanMethods=false의 안전성은 “설정 클래스가 단순하다”가 아니라 “inter-bean dependency를 메서드 직접 호출이 아닌 생성자나 메서드 파라미터 주입으로 표현한다”에 달려 있다.

실전 팁

  • 같은 설정 클래스 안에서 @Bean 메서드를 직접 호출한다면 full @Configuration 프록시 의미를 이해해야 한다.
  • 성능이나 단순성을 위해 proxyBeanMethods=false를 쓰려면 inter-bean method call이 없는지 확인한다.
  • @Bean 메서드 파라미터로 의존성을 받는 방식은 full/lite 모드 차이에 덜 민감하다.
  • @Configuration 프록시는 AOP와 다르다. AopUtils.isAopProxy() 같은 검사로 같은 방식으로 이해하려 하면 혼란스럽다.
  • 기존 문서의 @Bean 메서드 호출 시 기존 등록 빈 반환은 “Spring이 Java 메서드 호출을 컨테이너 생명주기로 재해석하는 장치”로 기억하면 좋다.
  • Kotlin이나 final을 기본으로 하는 언어에서는 full @Configuration 프록시가 필요한 설정 클래스가 확장 가능한지 함께 확인한다.

위험 신호!

  • proxyBeanMethods=false를 넣고도 같은 클래스의 @Bean 메서드를 직접 호출한다.
  • @Configuration 프록시를 AOP 부가 기능 프록시와 같은 개념으로 설명한다.
  • full @Configuration이 필요한 설정 클래스를 final로 만들어 CGLIB subclassing을 막는다.
  • 설정 클래스가 너무 많은 객체 생성 책임을 떠안아 복잡한 factory가 된다.
  • @Bean 메서드에서 상태를 변경하거나 호출 순서에 의존한다.
  • 테스트에서 설정 클래스 메서드를 직접 호출해 컨테이너 동작과 다른 결과를 만든다.

확인 질문

확인 질문

  • full @Configuration에서 @Bean 메서드 호출이 특별한 이유는 무엇인가?
    • Spring이 설정 클래스를 프록시 처리해 이미 등록된 Bean을 반환하고 싱글톤을 보장하기 때문이다.
  • @Configuration 프록시는 AOP 프록시와 무엇이 다른가?
    • AOP 프록시는 부가 기능을 적용하고, @Configuration 프록시는 Bean 생명주기와 싱글톤 보장을 위해 @Bean 메서드 호출을 제어한다.
  • proxyBeanMethods=false를 안전하게 쓰려면 어떻게 의존성을 표현하는 것이 좋은가?
    • 다른 @Bean 메서드를 직접 호출하기보다 @Bean 메서드 파라미터로 의존성을 받는 방식이 좋다.

참고 문서