스프링 싱글톤: 왜 @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으로 선언된 메서드이므로 호출될 때마다 새로운 객체를 반환할 것이라고 예상할 수 있다.
실제로 예상한 호출 흐름은 아래와 같다.
- memberService가 호출되며 memberRepository를 호출 → 첫 번째 호출
- orderService가 호출되며 memberRepository를 호출 → 두 번째 호출
- 기타 다른 메서드에서도 memberRepository 호출 → 세 번째 호출
따라서, call AppConfig.memberRepository가 3번 출력될 것으로 보인다. 하지만, 실제 실행 결과는 다르다.
실행 결과
코드를 실행하면 다음과 같은 결과를 출력한다.
call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService
memberRepository는 단 한 번만 호출된다. 스프링은 내부적으로 싱글톤 컨테이너를 사용해 동일한 객체를 공유하기 때문이다.
스프링 싱글톤과 @Configuration
스프링의 @Configuration이 붙은 클래스는 단순한 클래스가 아니다. 스프링 컨테이너는 CGLIB라는 바이트코드 조작 라이브러리를 이용해 AppConfig 클래스를 상속받아 동적으로 프록시 클래스를 생성한다.
이를 통해 @Bean으로 등록된 메서드들이 호출될 때마다 새로운 객체를 생성하지 않고, 스프링 컨테이너에 저장된 기존 객체를 반환하도록 동작을 변경한다.
CGLIB 프록시 동작 예시
스프링은 @Configuration을 통해 설정 클래스를 프록시 객체로 변환한다. memberRepository() 메서드의 호출 흐름은 아래와 같이 변경된다.
- AppConfig.memberService() 호출
- 내부적으로 AppConfig.memberRepository() 호출
- 프록시 객체가 memberRepository() 호출을 가로채고, 이미 생성된 객체가 있는지 확인
- 기존에 생성된 객체가 있다면 반환, 없다면 생성 후 반환
결과적으로, 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을 활용해 싱글톤 컨테이너를 자동으로 관리한다.
- @Configuration을 통해 설정 클래스를 프록시 객체로 생성한다.
- @Bean 메서드 호출 시 이미 생성된 객체가 있다면 이를 반환한다.
- 이를 통해 동일한 객체를 반복 생성하지 않고 효율적으로 관리한다.
이 방식은 객체 생성 비용을 줄이고 메모리를 효율적으로 사용할 수 있도록 돕는다. 스프링의 핵심 개념인 싱글톤 패턴은 애플리케이션 전반의 성능 최적화와 객체 관리 효율성을 극대화하는 중요한 설계 방식이다.
추가 공부
'JAVA > Spring' 카테고리의 다른 글
DI(의존관계 주입, Dependency Injection)란 무엇인가? (1) | 2024.12.20 |
---|---|
Spring에서 생성자에 @Autowired 어노테이션을 사용하는 이유와 장점 (0) | 2024.12.20 |
[스프링 빈과 싱글톤 패턴: 무상태로 설계해야 하는 이유?] (0) | 2024.12.17 |
AssertThat의 isEqualTo와 isSameAs의 차이점 알아보기 (0) | 2024.12.17 |
스프링 BeanDefinition 완벽 정리: 빈 설정 메타정보 탐구 (0) | 2024.12.16 |
댓글