@Configuration의 비밀 — 같은 메서드를 두 번 호출하면 어떻게 될까
@Configuration 클래스에서 @Bean 메서드를 두 번 호출하면 객체가 두 개 만들어질까요, 아니면 같은 객체가 반환될까요? 스프링은 이걸 어떻게 제어할까요?
개념 정의
@Configuration은 단순한 설정 클래스 표시가 아닙니다. 스프링이 이 클래스를 CGLIB 프록시로 감싸서, @Bean 메서드를 호출할 때 싱글톤을 보장하는 메커니즘입니다. 이것을 Full 모드라고 합니다.
왜 필요한가
다음 코드를 봅시다.
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
return new HikariDataSource(); // 커넥션 풀 생성
}
@Bean
public UserRepository userRepository() {
return new UserRepository(dataSource()); // dataSource() 호출
}
@Bean
public OrderRepository orderRepository() {
return new OrderRepository(dataSource()); // dataSource() 또 호출
}
}
일반 자바 코드라면 dataSource()가 두 번 호출되므로 HikariDataSource가 두 개 생깁니다. 커넥션 풀이 두 개라는 뜻이죠. 이건 의도한 동작이 아닙니다.
@Configuration의 CGLIB 프록시가 이 문제를 해결합니다. dataSource()를 아무리 많이 호출해도 컨테이너에 등록된 하나의 빈만 반환됩니다.
내부 동작
CGLIB 프록시가 하는 일
스프링은 @Configuration 클래스를 로드할 때 다음과 같이 처리합니다.
1. AppConfig 클래스 발견
2. CGLIB으로 AppConfig의 서브클래스(프록시) 생성
→ AppConfig$$SpringCGLIB$$0
3. 이 프록시 클래스를 빈으로 등록
4. @Bean 메서드 호출 시 프록시가 가로챔
→ 이미 빈이 있으면 기존 빈 반환
→ 없으면 원본 메서드 실행 후 빈으로 등록
개념적으로 프록시가 하는 일을 코드로 표현하면 이렇습니다.
// CGLIB 프록시의 동작 (개념적 표현)
public class AppConfig$$SpringCGLIB$$0 extends AppConfig {
@Override
public DataSource dataSource() {
if (beanFactory.containsBean("dataSource")) {
return beanFactory.getBean("dataSource", DataSource.class);
}
// 최초 호출 시에만 실제 메서드 실행
DataSource ds = super.dataSource();
beanFactory.registerSingleton("dataSource", ds);
return ds;
}
}
Full 모드 vs Lite 모드
| 구분 | Full 모드 | Lite 모드 |
|---|---|---|
| 설정 | @Configuration (기본) | @Configuration(proxyBeanMethods = false) |
| CGLIB 프록시 | 생성됨 | 생성 안 됨 |
| 메서드 간 호출 | 싱글톤 보장 | 매번 새 인스턴스 생성 |
| 성능 | 약간 느림 (프록시 오버헤드) | 약간 빠름 |
| 용도 | 빈 간 의존관계가 있을 때 | 독립적인 빈 등록 |
Lite 모드의 함정
@Configuration(proxyBeanMethods = false) // Lite 모드
public class AppConfig {
@Bean
public DataSource dataSource() {
return new HikariDataSource();
}
@Bean
public UserRepository userRepository() {
return new UserRepository(dataSource()); // 새 HikariDataSource 생성!
}
@Bean
public OrderRepository orderRepository() {
return new OrderRepository(dataSource()); // 또 새 HikariDataSource 생성!
}
}
Lite 모드에서는 CGLIB 프록시가 없으므로 dataSource()가 일반 자바 메서드처럼 동작합니다. HikariDataSource가 3개 생기게 됩니다.
Lite 모드에서 안전하게 의존성 연결하기
Lite 모드를 쓰면서도 의존성을 올바르게 연결하려면, 메서드 파라미터로 받아야 합니다.
@Configuration(proxyBeanMethods = false)
public class AppConfig {
@Bean
public DataSource dataSource() {
return new HikariDataSource();
}
@Bean
public UserRepository userRepository(DataSource dataSource) {
// 파라미터로 주입받으면 컨테이너의 싱글톤 빈을 받음
return new UserRepository(dataSource);
}
@Bean
public OrderRepository orderRepository(DataSource dataSource) {
return new OrderRepository(dataSource); // 같은 DataSource 인스턴스
}
}
코드 예제
Full 모드 동작 확인
@Configuration
public class FullModeConfig {
@Bean
public SimpleBean simpleBean() {
System.out.println("simpleBean 생성");
return new SimpleBean();
}
@Bean
public CompoundBean compoundBean() {
SimpleBean bean1 = simpleBean();
SimpleBean bean2 = simpleBean();
System.out.println("같은 인스턴스인가? " + (bean1 == bean2)); // true
return new CompoundBean(bean1);
}
}
출력:
simpleBean 생성 ← 한 번만 출력됨
같은 인스턴스인가? true
Lite 모드 동작 확인
@Configuration(proxyBeanMethods = false)
public class LiteModeConfig {
@Bean
public SimpleBean simpleBean() {
System.out.println("simpleBean 생성");
return new SimpleBean();
}
@Bean
public CompoundBean compoundBean() {
SimpleBean bean1 = simpleBean();
SimpleBean bean2 = simpleBean();
System.out.println("같은 인스턴스인가? " + (bean1 == bean2)); // false
return new CompoundBean(bean1);
}
}
출력:
simpleBean 생성 ← 세 번 출력됨 (빈 등록 1번 + 메서드 호출 2번)
simpleBean 생성
simpleBean 생성
같은 인스턴스인가? false
@Component 안의 @Bean은 Lite 모드
@Component // @Configuration이 아니라 @Component
public class ServiceConfig {
@Bean
public SomeService someService() {
return new SomeService();
}
@Bean
public AnotherService anotherService() {
// someService()를 호출하면 새 인스턴스가 생성됨 (Lite 모드)
return new AnotherService(someService());
}
}
@Component 안에서도 @Bean을 사용할 수 있지만, CGLIB 프록시가 적용되지 않습니다. 이것도 Lite 모드입니다.
실무에서의 선택 기준
// 빈 간 의존관계가 있을 때 → Full 모드 (기본)
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() { ... }
@Bean
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource()); // 안전
}
}
// 독립적인 빈만 등록할 때 → Lite 모드 (성능 이점)
@Configuration(proxyBeanMethods = false)
public class UtilConfig {
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
스프링부트 Auto-Configuration은 대부분 Lite 모드
스프링부트의 자동 설정 클래스들을 보면 대부분 proxyBeanMethods = false입니다.
// 스프링부트 내부 코드
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(DataSource.class)
public class DataSourceAutoConfiguration {
// ...
}
부팅 속도를 빠르게 하기 위해 불필요한 CGLIB 프록시 생성을 피합니다. 대신 의존성은 모두 메서드 파라미터로 주입받는 패턴을 사용합니다.
CGLIB 프록시 확인 방법
@SpringBootApplication
public class App {
public static void main(String[] args) {
var context = SpringApplication.run(App.class, args);
var config = context.getBean(AppConfig.class);
System.out.println(config.getClass());
// com.example.AppConfig$$SpringCGLIB$$0 ← 프록시 클래스
}
}
정리
@Configuration은 CGLIB 프록시를 생성하여@Bean메서드 간 호출 시 싱글톤을 보장합니다 (Full 모드)proxyBeanMethods = false로 설정하면 CGLIB 프록시 없이 동작합니다 (Lite 모드)- Lite 모드에서 빈 간 의존관계가 필요하면 메서드 파라미터로 주입받아야 합니다
@Component안의@Bean은 자동으로 Lite 모드입니다- 스프링부트 Auto-Configuration은 성능을 위해 대부분 Lite 모드를 사용합니다