INFLEARN

[스프링 핵심 원리 - 기본편] 9. @Configuration과 Singleton 보장

ch010104 2026. 3. 13. 22:27

1. @Configuration과 싱글톤의 의문점

AppConfig 코드를 보면 memberRepository()가 여러 번 호출되는 구조로 인해 싱글톤이 깨질 것처럼 보임

[코드] 의문이 발생하는 AppConfig

@Configuration
public class AppConfig {
    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository()); // memberRepository() 호출
    }

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy()); // memberRepository() 호출
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository(); // 여기서 new가 총 3번 실행될까?
    }
    // ... 생략
}
  • 추측: memberService에서 1번, orderService에서 1번, 그리고 스프링이 빈을 등록할 때 1번. 총 3개의 인스턴스가 생성되어야 음

2. 검증을 위한 코드 추가 및 테스트

정말로 인스턴스가 여러 개 생기는지 확인하기 위해 테스트 용도의 메서드를 추가하고 테스트 코드를 실행

[코드] 검증 용도 코드 추가 (Service 구현체)

public class MemberServiceImpl implements MemberService {
    private final MemberRepository memberRepository;

    // 테스트 용도
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}

public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;

    // 테스트 용도
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}

[코드] 싱글톤 확인 테스트 코드

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberServiceImpl;
import hello.core.order.OrderServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.*;

public class ConfigurationSingletonTest {
    @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);

        // 모두 같은 인스턴스를 참고하고 있다.
        System.out.println("memberService -> memberRepository = " + memberService.getMemberRepository());
        System.out.println("orderService -> memberRepository  = " + orderService.getMemberRepository());
        System.out.println("memberRepository = " + memberRepository);

        assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
    }
}
  • 결과: 모든 memberRepository 인스턴스는 동일합니다. 주소값이 모두 같음

3. 호출 로그 실험

정말로 메서드가 여러 번 호출되는지 확인하기 위해 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();
    }
    // ...
}
  • 출력 결과: 예상과 달리 모두 1번만 호출됨
call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService

4. @Configuration과 바이트코드 조작의 마법

스프링은 자바 코드의 로직을 그대로 두지 않고, CGLIB라는 바이트코드 조작 라이브러리를 사용하여 싱글톤을 보장

[코드] AppConfig 클래스 정보 확인 테스트

@Test
void configurationDeep() {
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    AppConfig bean = ac.getBean(AppConfig.class);

    System.out.println("bean = " + bean.getClass());
    // 출력: bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70
}
  • 클래스 명에 CGLIB가 붙어 있음
  • 이는 스프링이 AppConfig를 상속받은 임의의 프록시 클래스를 만들어 빈으로 등록했음을 의미

CGLIB 예상 로직 (AppConfig@CGLIB)

@Bean
public MemberRepository memberRepository() {
    if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
        return 스프링 컨테이너에서 찾아서 반환;
    } else { // 스프링 컨테이너에 없으면
        기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
        return 반환;
    }
}

5. @Configuration을 적용하지 않는다면?

만약 @Configuration을 빼고 @Bean만 사용하면 어떻게 될까요?

[코드] @Configuration 삭제 실험

// @Configuration 삭제
public class AppConfig {
    @Bean
    public MemberService memberService() { ... }
    @Bean
    public MemberRepository memberRepository() { ... }
    // ...
}
  • 출력 결과 (클래스): bean = class hello.core.AppConfig (순수 자바 클래스)
  • 출력 결과 (로그): memberRepository()가 총 3번 호출됩니다.
  • 테스트 결과: 각각 다른 MemoryMemberRepository 인스턴스를 가지게 되어 싱글톤이 깨집니다.
memberService -> memberRepository = @6239aba6
orderService -> memberRepository  = @3e6104fc
memberRepository = @12359a82

6. 정리

  1. @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤은 보장되지 않음
  2. memberRepository()처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 문제가 발생
  3. 스프링 설정 정보에는 항상 @Configuration을 사용하여 싱글톤을 보장받아야 함