JAVA/Spring

[스프링 싱글톤] : 왜 @Bean은 한 번만 호출될까?

min민 2024. 12. 20.

 

스프링 싱글톤: 왜 @Bean은 한 번만 호출될까?

스프링 프레임워크는 객체 관리를 효율적으로 처리하기 위해 **싱글톤(Singleton)**이라는 개념을 기본적으로 채택하고 있다. 이번 글에서는 스프링에서 @Bean이 선언된 메서드들이 왜 한 번만 호출되는지와 관련된 싱글톤 컨테이너의 동작 원리를 설명한다.

 

 

 

 

문제 상황

아래는 스프링 애플리케이션에서 사용할 설정 클래스이다.

@Configuration
public class AppConfig {

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

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

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

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

 

위 코드를 보면 memberService와 orderService는 각각 memberRepository를 호출해 생성자를 통해 주입받는다. memberRepository는 @Bean으로 선언된 메서드이므로 호출될 때마다 새로운 객체를 반환할 것이라고 예상할 수 있다.

실제로 예상한 호출 흐름은 아래와 같다.

  1. memberService가 호출되며 memberRepository를 호출 → 첫 번째 호출
  2. orderService가 호출되며 memberRepository를 호출 → 두 번째 호출
  3. 기타 다른 메서드에서도 memberRepository 호출 → 세 번째 호출

따라서, call AppConfig.memberRepository가 3번 출력될 것으로 보인다. 하지만, 실제 실행 결과는 다르다.

 

 

 

 

실행 결과

코드를 실행하면 다음과 같은 결과를 출력한다.

call AppConfig.memberService  
call AppConfig.memberRepository  
call AppConfig.orderService

memberRepository는 단 한 번만 호출된다. 스프링은 내부적으로 싱글톤 컨테이너를 사용해 동일한 객체를 공유하기 때문이다.

 

 

 

 

스프링 싱글톤과 @Configuration

스프링의 @Configuration이 붙은 클래스는 단순한 클래스가 아니다. 스프링 컨테이너는 CGLIB라는 바이트코드 조작 라이브러리를 이용해 AppConfig 클래스를 상속받아 동적으로 프록시 클래스를 생성한다.

이를 통해 @Bean으로 등록된 메서드들이 호출될 때마다 새로운 객체를 생성하지 않고, 스프링 컨테이너에 저장된 기존 객체를 반환하도록 동작을 변경한다.

 

 

 

 

 

CGLIB 프록시 동작 예시

스프링은 @Configuration을 통해 설정 클래스를 프록시 객체로 변환한다. memberRepository() 메서드의 호출 흐름은 아래와 같이 변경된다.

  1. AppConfig.memberService() 호출
  2. 내부적으로 AppConfig.memberRepository() 호출
  3. 프록시 객체가 memberRepository() 호출을 가로채고, 이미 생성된 객체가 있는지 확인
  4. 기존에 생성된 객체가 있다면 반환, 없다면 생성 후 반환

결과적으로, AppConfig.memberRepository()는 항상 같은 객체를 반환한다.

 

 

 

 

 

@Bean 메서드 호출 흐름 확인하기

CGLIB 프록시 동작을 이해하기 위해 실제 프록시 클래스가 생성된 모습은 아래와 같다.

@Configuration
class AppConfig$$EnhancerBySpringCGLIB extends AppConfig {
    
    private final Map<String, Object> beans = new HashMap<>();
    
    @Override
    public MemberRepository memberRepository() {
        if (!beans.containsKey("memberRepository")) {
            beans.put("memberRepository", super.memberRepository());
        }
        return (MemberRepository) beans.get("memberRepository");
    }
}

 

프록시 클래스는 실제 memberRepository() 호출 대신 이미 생성된 객체를 캐싱해 반환한다. 이를 통해 @Bean으로 선언된 객체들은 컨테이너에서 하나의 인스턴스만 관리하게 된다.

 

 

 

 

싱글톤 적용 원리

스프링의 싱글톤 방식은 효율적인 객체 관리를 위해 설계되었다. 모든 @Bean으로 선언된 객체는 **스프링 컨테이너(ApplicationContext)**에 등록되고, 이를 통해 싱글톤으로 관리된다.

즉, @Bean 메서드가 호출될 때마다 새로운 객체를 생성하는 대신, 스프링 컨테이너에 저장된 기존 객체를 반환한다.

 

 

 

 

 

실제 코드로 싱글톤 확인하기

싱글톤 동작을 직접 확인하기 위해 아래와 같은 테스트 코드를 작성해보았다.

public class SingletonTest {

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

        MemberRepository memberRepository1 = ac.getBean("memberRepository", MemberRepository.class);
        MemberRepository memberRepository2 = ac.getBean("memberRepository", MemberRepository.class);

        // 동일한 객체인지 확인
        Assertions.assertThat(memberRepository1).isSameAs(memberRepository2);
    }
}

 

테스트 결과, memberRepository1과 memberRepository2는 동일한 객체를 참조하고 있음을 확인할 수 있다.

 

 

 

 

 

총 정리

스프링은 @Configuration과 @Bean을 활용해 싱글톤 컨테이너를 자동으로 관리한다.

  1. @Configuration을 통해 설정 클래스를 프록시 객체로 생성한다.
  2. @Bean 메서드 호출 시 이미 생성된 객체가 있다면 이를 반환한다.
  3. 이를 통해 동일한 객체를 반복 생성하지 않고 효율적으로 관리한다.

이 방식은 객체 생성 비용을 줄이고 메모리를 효율적으로 사용할 수 있도록 돕는다. 스프링의 핵심 개념인 싱글톤 패턴은 애플리케이션 전반의 성능 최적화와 객체 관리 효율성을 극대화하는 중요한 설계 방식이다.

 

추가 공부

@Configuration 을 적용하지 않고, @Bean 만 적용하면 어떻게 될까?
- @Configuration 을 붙이면 바이트코드를 조작하는 CGLIB 기술을 사용해서 싱글톤을 보장하지만, 만약 @Bean만 적용하면 어떻게 될까?
    - 싱글톤 보장하지 않음.

### 정리
- @Bean만 사용해도 스프링 빈으로 등록되지만, `싱글톤을 보장하지 않는다.`
    - memberRepository() 처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤을 보장하지 않는다.
- 크게 고민할 것이 없다. 스프링 설정 정보는 항상 @Configuration 을 사용하자.

댓글