@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가 두 개 생깁니다.
- 커넥션 풀이 두 개라는 뜻이므로, DB 커넥션이 이중으로 관리됩니다.
userRepository와orderRepository가 서로 다른 DataSource를 사용하면, 하나의 트랜잭션으로 묶이지 않습니다.@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 ← 프록시 클래스
}
}
주의할 점
1. Lite 모드에서 @Bean 메서드를 직접 호출하면 싱글톤이 깨진다
@Configuration(proxyBeanMethods = false) 또는 @Component 안에서 @Bean 메서드를 다른 @Bean 메서드 내부에서 호출하면, 매번 새 인스턴스가 생성됩니다. DataSource 같은 커넥션 풀이 여러 개 생기면 DB 커넥션이 낭비되고 트랜잭션이 의도대로 동작하지 않습니다. Lite 모드에서는 반드시 메서드 파라미터로 의존성을 주입받아야 합니다.
2. @Component 안의 @Bean은 암묵적으로 Lite 모드다
@Configuration이 아닌 @Component, @Service 등에서도 @Bean을 선언할 수 있지만, CGLIB 프록시가 적용되지 않습니다. 이를 모르고 @Component 클래스 안에서 @Bean 메서드를 서로 호출하면 싱글톤이 보장되지 않아, 같은 빈이 여러 인스턴스로 존재하는 버그가 발생합니다.
3. final 클래스를 @Configuration으로 선언하면 CGLIB 프록시 생성이 실패한다
CGLIB은 대상 클래스를 상속하여 프록시를 생성합니다. @Configuration 클래스를 final로 선언하거나 Kotlin의 기본 클래스(open이 아닌)를 사용하면 프록시 생성에 실패하여 BeanDefinitionStoreException이 발생합니다. @Configuration 클래스는 상속 가능한 상태로 유지해야 합니다.
정리
| 항목 | Full 모드 (기본) | Lite 모드 |
|---|---|---|
| 설정 | @Configuration | proxyBeanMethods = false |
| CGLIB 프록시 | 생성됨 | 생성 안 됨 |
| 메서드 간 호출 | 싱글톤 보장 | 매번 새 인스턴스 |
| 의존성 연결 | 메서드 직접 호출 OK | 파라미터 주입 필수 |
| 사용처 | 빈 간 의존관계 있을 때 | 독립 빈, Auto-Configuration |