Spring

[Spring 완전 정복 시리즈] 14편 - @Configuration과 바이트코드 조작의 마법

dev-nadan 2025. 7. 28. 12:04

지난 편에서는 스프링이 어떻게 싱글톤 객체를 자동으로 관리해주는지, 그리고 싱글톤 설계 시 주의할 점들을 다뤘다.

이번 편에서는 @Configuration이 어떻게 실제로 싱글톤을 보장하는지, 그리고 그 뒤에 숨어 있는 바이트코드 조작의 비밀에 대해 살펴보겠습니다람쥐~


1. AppConfig의 의도와 실제 호출

 

다음은 우리가 일반적으로 작성하는 AppConfig 코드입니다.

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        System.out.println("call AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public OrderService orderService() {
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    @Bean
    public MemberRepository memberRepository() {
        System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }

    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
}

이 코드를 보면 memberRepository()가 3번 호출될 것처럼 보입니다. 하지만 실제로는 딱 한 번만 호출됩니다. 이유가 뭘까요?


2. 스프링의 마법: @Configuration + CGLIB

 

스프링은 @Configuration이 붙은 클래스를 처리할 때, 해당 클래스를 상속받아 프록시 객체를 생성합니다. 이 프록시 객체는 CGLIB이라는 바이트코드 조작 라이브러리를 이용해 만들어집니다.

 

AppConfig$$EnhancerBySpringCGLIB$$... 

 

클래스 이름 뒤에 이상한 이름이 붙는다면 CGLIB 프록시가 생성된 것이라는 뜻입니다.

3. 프록시 객체의 역할

 

프록시로 생성된 AppConfig@Bean 메서드를 호출할 때 다음과 같은 동작을 합니다.

 

  • 해당 빈이 이미 스프링 컨테이너에 존재하면 → 기존 객체 반환
  • 존재하지 않으면 → 생성하고 등록한 뒤 반환

 

이러한 로직이 동적으로 프록시 내부에 삽입되어 있어, memberRepository()가 여러 번 호출되더라도 항상 같은 인스턴스를 반환하게 됩니다.


4. 테스트로 확인해보기

 

아래는 실제로 싱글톤이 보장되는지 확인하는 테스트입니다.

@Test
void configurationTest() {
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
    OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
    MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

    MemberRepository repo1 = memberService.getMemberRepository();
    MemberRepository repo2 = orderService.getMemberRepository();

    assertThat(repo1).isSameAs(memberRepository);
    assertThat(repo2).isSameAs(memberRepository);
}

 

실행 결과

call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService
memberRepository()는 한 번만 호출되며, 모든 빈이 같은 객체를 공유하고 있습니다.

5. @Configuration을 생략하면?

 

만약 @Configuration을 제거하고 @Bean만 붙인 클래스라면 어떻게 될까요?

public class AppConfig {
    ...
}

이 경우, CGLIB 프록시가 적용되지 않기 때문에 각 메서드가 호출될 때마다 새로운 객체가 생성됩니다. 즉, 싱글톤이 깨지게 되는 것이죠.

 

설정 클래스에는 항상 @Configuration을 붙여야 싱글톤이 제대로 보장됩니다.

정리

  • @Configuration은 단순한 어노테이션이 아니라, CGLIB 프록시를 통해 싱글톤을 보장하는 핵심 메커니즘입니다.
  • @Bean 메서드는 스프링이 자동으로 프록시로 감싸기 때문에, 개발자가 신경 쓰지 않아도 하나의 객체만 생성됩니다.
  • @Configuration이 없다면 이러한 보장이 깨지므로, 설정 클래스에는 반드시 붙여야 합니다.