@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를 최종 제거한다.

@BeaninitMethoddestroyMethod

// 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() 메서드를 자동으로 추론한다.

@Configurationfull 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로 설정 클래스를 조합한다. 대규모 애플리케이션에서 설정을 모듈화할 수 있다.

생각해 볼 문제

  1. @Configuration(proxyBeanMethods = false)를 쓰면 b()를 직접 호출할 때 어떤 일이 일어나는가? 싱글톤이 깨지는가?
  2. @Component@Bean의 차이는 무엇인가? 각각 언제 써야 하는가?
  3. @ConditionalOnMissingBean@Bean 메서드에서 어떻게 동작하는가? 사용자 정의 bean이 우선하는 원리는?
  4. @ComponentScanbasePackages 없이 @SpringBootApplication만으로 스캔이 되는 이유는?
  5. Spring Boot AOT(GraalVM native image) 홼경에서 CGLIB 프록시가 제약을 받는 이유는?

참고

'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