@Configuration이 싱글톤을 보장하는 원리 — CGLIB 프록시와 컴포넌트 스캔
@Configuration
public class AppConfig {
@Bean
public A a() { return new A(b()); } // b()를 직접 호출
@Bean
public B b() { return new B(); }
}
a()에서 b()를 직접 호출하면 new B()가 두 번 실행될까? — 아니다. @Configuration 클래스는 CGLIB 프록시로 감싸져서, b()를 호출하면 컨테이너에 이미 등록된 bean을 반환한다. 이것이 Spring의 싱글톤 보장 메커니즘이다. (Spring Framework Reference - Java-based Container Configuration)
이 글은 @Configuration의 CGLIB 프록시, 컴포넌트 스캔의 작동 방식, @Conditional로 bean 등록을 제어하는 방법을 풀어간다.
@Configuration과 CGLIB 프록시
@Configuration이 붙은 클래스는 Spring이 CGLIB 서브클래스 프록시를 생성한다. 이 프록시는 @Bean 메서드 호출을 가로채서, 컨테이너의 singleton bean을 반환한다.
flowchart LR
A["@Bean a() 호출"] --> B{CGLIB 프록시 가로채기}
B --> C{"컨테이너에 b bean 있음?"}
C -->|있음| D["기존 bean 반환 (new B() 호출 안 함)"]
C -->|없음| E["b() 실행 → bean 생성 → 등록"]
// Spring 7 / Java 25 — @Configuration이 싱글톤을 보장하는 예
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
System.out.println("dataSource() 호출됨");
return new HikariDataSource();
}
@Bean
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource()); // dataSource() 직접 호출
}
@Bean
public NamedParameterJdbcTemplate namedJdbcTemplate() {
return new NamedParameterJdbcTemplate(dataSource()); // 또 직접 호출
}
// dataSource()가 3번 호출되지만, "dataSource() 호출됨"은 1번만 출력됨
}
@Configuration(proxyBeanMethods = false)를 쓰면 CGLIB 프록시를 끌 수 있다 (Spring Boot의 경량화 모드). 이 경우b()직접 호출이 실제 새 객체를 만들므로, bean 간 의존성은 매개변수 주입으로만 전달해야 한다.
CGLIB 프록시가 어떻게 싱글톤을 보장하는가 — 내부 메커니즘: @Configuration 클래스가 로드될 때, Spring은 CGLIB으로 이 클래스의 서브클래스를 동적 생성한다. 이 서브클래스는 각 @Bean 메서드를 override하여 — 메서드 본문 실행 전에 BeanFactory에 "이 타입의 bean이 이미 있나?"를 확인한다. 있으면 기존 bean 반환(메서드 본문 건너뜀), 없으면 원래 메서드 실행하여 생성 + 등록.
비유: @Configuration은 단 1개만 만드는 자판기다. "커피" 버튼을 3번 눌러도 첫 번째에 만든 커피 1잔만 나온다. CGLIB 프록시가 "이미 만들었으면 또 만들지 마"라는 로직을 버튼 앞에 끼워넣는다. proxyBeanMethods = false는 이 로직을 빼는 것 — 버튼 누를 때마다 새 커피가 나온다.
컴포넌트 스캔 — 자동 bean 등록
@ComponentScan은 지정된 패키지에서 @Component 계열 어노테이션이 붙은 클래스를 자동으로 찾아 bean으로 등록한다.
// Spring 7 / Java 25 — 컴포넌트 스캔 설정
@Configuration
@ComponentScan(basePackages = "com.example.myapp")
public class AppConfig { }
스캔 대상 어노테이션:
| 어노테이션 | 용도 |
|-----------|------|
| @Component | 일반 컴포넌트 |
| @Service | 서비스 계층 |
| @Repository | 데이터 접근 계층 (DAO) |
| @Controller | 웹 컨트롤러 (MVC) |
| @RestController | REST API 컨트롤러 |
| @Configuration | 설정 클래스 |
@Component에 이름과 스코프 지정
// Spring 7 / Java 25
@Component("fastCache") // bean 이름 지정 (기본: 클래스명 첫 글자 소문자)
@Scope("prototype") // singleton이 아닌 prototype
public class FastCache { ... }
@Conditional — 조건부 bean 등록
@Conditional은 특정 조건이 참일 때만 bean을 등록한다. (Spring Framework Reference - Conditions)
// Spring 7 / Java 25 — @Conditional 예
@Configuration
public class CacheConfig {
@Bean
@Conditional(RedisAvailableCondition.class)
public CacheService redisCache() {
return new RedisCacheService(); // Redis 사용 가능할 때만
}
@Bean
@Conditional(OnMissingBeanCondition.class)
public CacheService localCache() {
return new LocalCacheService(); // 다른 CacheService가 없을 때만
}
}
Spring Boot가 제공하는 사전 정의된 조건 어노테이션:
| 어노테이션 | 조건 |
|---|---|
@ConditionalOnClass(name="...") |
클래스패스에 특정 클래스가 있음 |
@ConditionalOnMissingBean |
특정 타입의 bean이 아직 등록되지 않음 |
@ConditionalOnProperty(name="cache.type", havingValue="redis") |
프로퍼티 값 |
@ConditionalOnWebApplication |
웹 애플리케이션 환경 |
이 조건 어노테이션들이 Spring Boot 자동 구성(auto-configuration)의 기반이다 — classpath에 있는 라이브러리에 따라 bean을 자동 등록한다. (Spring Boot 장 01 상세)
@Import — 설정 클래스 조합
// Spring 7 / Java 25 — @Import로 여러 설정 클래스 결합
@Configuration
@Import({ DataSourceConfig.class, WebConfig.class, SecurityConfig.class })
public class AppConfig { }
@Profile — 환경별 bean
// Spring 7 / Java 25 — @Profile
@Configuration
public class DataSourceConfig {
@Bean
@Profile("dev")
public DataSource devDataSource() {
return new H2DataSource(); // 개발 환경: H2 인메모리
}
@Bean
@Profile("prod")
public DataSource prodDataSource() {
return new PostgreSQLDataSource(); // 운영 환경: PostgreSQL
}
}
활성 프로파일은 spring.profiles.active=dev로 설정한다. (장 05 상세)
실습 — @Configuration 싱글톤 확인
// Spring 7 / Java 25 — ConfigSingletonDemo.java
// CGLIB 프록시 없이 수동 확인 (개념 시뮬레이션)
public class ConfigSingletonDemo {
static class SingletonRegistry {
private static final Map<String, Object> beans = new HashMap<>();
@SuppressWarnings("unchecked")
static <T> T get(String name, Supplier<T> supplier) {
return (T) beans.computeIfAbsent(name, k -> supplier.get());
}
}
static class B { int id; B() { id = (int)(Math.random() * 10000); } }
static class ConfigSim {
B b() {
// @Configuration처럼 싱글톤 보장
return SingletonRegistry.get("b", () -> new B());
}
void demo() {
B b1 = b();
B b2 = b();
System.out.println("b1.id=" + b1.id);
System.out.println("b2.id=" + b2.id);
System.out.println("같은 객체? " + (b1 == b2));
}
}
public static void main(String[] args) {
new ConfigSim().demo();
}
}
java ConfigSingletonDemo.java
b1.id=4823
b2.id=4823
같은 객체? true
확인할 것: b()를 두 번 호출해도 같은 객체가 반환된다. Spring의 @Configuration이 CGLIB 프록시로 이 동작을 자동화한다.
excludeFilters — 스캔에서 제외
// Spring 7 / Java 25 — 컴포넌트 스캔 필터링
@Configuration
@ComponentScan(
basePackages = "com.example",
excludeFilters = @ComponentScan.Filter(
type = FilterType.REGEX,
pattern = "com\\.example\\.legacy\\..*"
)
)
public class AppConfig { }
필터 타입:
| FilterType | 의미 |
|---|---|
ANNOTATION |
특정 어노테이션이 붙은 클래스 제외/포함 |
ASSIGNABLE_TYPE |
특정 클래스/인터페이스의 하위 타입 |
REGEX |
정규식으로 패키지/클래스명 매칭 |
CUSTOM |
커스텀 TypeFilter 구현 |
@ImportResource — XML 설정 결합
// Spring 7 / Java 25 — XML 설정을 Java 설정으로 가져오기
@Configuration
@ImportResource("classpath:legacy-beans.xml")
public class AppConfig {
// XML에 정의된 bean도 이 컨텍스트에서 사용 가능
}
레거시 XML 설정이 남아 있는 프로젝트에서, 점진적으로 Java 설정으로 전환할 때 유용하다. XML bean 정의를 Java
@Bean으로 하나씩 옮기면서@ImportResource를 최종 제거한다.
@Bean의 initMethod와 destroyMethod
// Spring 7 / Java 25 — 라이프사이클 메서드 지정
@Bean(initMethod = "connect", destroyMethod = "close")
public DataSource dataSource() {
return new HikariDataSource(hikariConfig());
}
// 컨테이너 시작 시 dataSource.connect() 호출
// 컨테이너 종료 시 dataSource.close() 호출
외부 라이브러리 객체가
@PostConstruct/@PreDestroy를 가지지 않을 때,initMethod/destroyMethod로 라이프사이클 콜백 메서드를 지정한다.destroyMethod의 기본값은(inferred)— Spring이close()/shutdown()메서드를 자동으로 추론한다.
@Configuration의 full vs lite 모드
@Configuration 클래스의 두 가지 모드:
// Full 모드 (기본): CGLIB 프록시 생성, @Bean 메서드 호출이 싱글톤 보장
@Configuration
public class FullConfig {
@Bean
public A a() { return new A(b()); } // b()가 같은 bean 반환
@Bean
public B b() { return new B(); }
}
// Lite 모드: CGLIB 프록시 없음, 더 가벼움, @Bean 직접 호출이 새 객체 생성
@Configuration(proxyBeanMethods = false)
public class LiteConfig {
@Bean
public A a(B b) { return new A(b); } // 매개변수로 주입 — b() 직접 호출 X
@Bean
public B b() { return new B(); }
}
Spring Boot 3.2+ / Spring Framework 7에서는
proxyBeanMethods = false가 더 널리 권장된다 — GraalVM AOT 환경에서 CGLIB 프록시 생성이 제약되기 때문이다. lite 모드에서는 bean 간 의존성을 항상 매개변수로 전달해야 한다. (Spring Framework 7 - AOT)
요약 — 이 글의 결론
@Configuration은 CGLIB 프록시로 싱글톤을 보장한다.@Bean메서드를 직접 호출해도 컨테이너에 등록된 singleton bean을 반환한다.proxyBeanMethods = false로 끌 수 있다.- 컴포넌트 스캔으로 bean을 자동 등록한다.
@Component계열 어노테이션이 붙은 클래스를 패키지에서 자동 검색.basePackages로 검색 범위를 지정. @Conditional로 bean 등록을 조건부로 제어한다. Spring Boot의@ConditionalOnClass,@ConditionalOnMissingBean등이 이 메커니즘 위에 구축된다.@Profile로 환경별 bean을 분리한다. 개발/운영 환경에서 다른 구현체를 사용할 수 있다.@Import로 설정 클래스를 조합한다. 대규모 애플리케이션에서 설정을 모듈화할 수 있다.
생각해 볼 문제
@Configuration(proxyBeanMethods = false)를 쓰면b()를 직접 호출할 때 어떤 일이 일어나는가? 싱글톤이 깨지는가?@Component와@Bean의 차이는 무엇인가? 각각 언제 써야 하는가?@ConditionalOnMissingBean이@Bean메서드에서 어떻게 동작하는가? 사용자 정의 bean이 우선하는 원리는?@ComponentScan의basePackages없이@SpringBootApplication만으로 스캔이 되는 이유는?- Spring Boot AOT(GraalVM native image) 홼경에서 CGLIB 프록시가 제약을 받는 이유는?
참고
- Spring Framework 7 - Java-based Container Configuration - 접근 2026-07-10
- Spring Framework 7 - @Configuration - 접근 2026-07-10
- Spring Framework 7 - @ComponentScan - 접근 2026-07-10
- Spring Framework 7 - Conditions - 접근 2026-07-10
'Develop Artifacts > Spring' 카테고리의 다른 글
| Spring - 06. aop (0) | 2026.07.12 |
|---|---|
| Spring - 05. bean scope profile (0) | 2026.07.12 |
| Spring - 03. dependency-injection (0) | 2026.07.12 |
| Spring - 02. container bean lifecycle (0) | 2026.07.12 |
| Spring - 01. IoC DI (0) | 2026.07.12 |
