이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
@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 클래스로 고정하는 식의 설계는 피해야 한다.
이 프록시는 다음 흐름으로 동작한다.
@Bean메서드 호출이 발생한다.- 컨테이너에 해당 Bean이 이미 있는지 확인한다.
- 있으면 기존 Bean을 반환한다.
- 없으면 원본 메서드를 호출해 객체를 만들고 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메서드 파라미터로 의존성을 받는 방식이 좋다.