스프링 컨테이너는 스프링 빈을 어떻게 싱글톤으로 관리할까?
🔥 스프링 컨테이너가 이미 존재하는 객체 인스턴스를 반환하는 @Bean 을 어떻게 처리하는지 알아보도록 한다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemoryMemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public RateDiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
다음 예제는 @Configuration
, @Bean
을 통해 스프링 컨테이너에 스프링 빈을 등록을 위한 자바 설정코드이다. 스프링은 싱글톤으로 객체를 관리한다고 했지만 위의 예시를 보면 memberRepository()
가 3번 호출되어 new 키워드를 통해 객체가 3번 생성되게 된다. 이것을 스프링은 어떻게 해결하여 싱글톤으로 관리하게 되는지 알아보도록 한다.
검증을 위한 코드 추가
public MemberRepository getMemberRepository() {
return memberRepository;
}
- 실제 같은 객체인지 확인을 위해
MemberServiceImpl
과OrderServiceImpl
에MemberRepository
를 반환하는 getter 메소드를 작성한다.
테스트 코드
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("memberService = " + memberRepository);
Assertions.assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
Assertions.assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
}
}
위의 예시 테스트코드는 통과하고 출력의 값을 확인해보면 같은 객체 인스턴스인 것을 알 수 있다. 스프링 빈을 등록할 때 AppConfig
안의 메서드들이 모두 한번씩 실행될텐데 어떻게 같은 인스턴스를 반환할 수 있는 것일까?
검증을 위한 코드 추가2
이번에는 AppConfig
에 호출 로그를 남겨 확인해보도록 하자.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
System.out.println("AppConfig.memberService 호출");
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemoryMemberRepository memberRepository() {
System.out.println("AppConfig.memberRepository 호출");
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
System.out.println("AppConfig.orderService 호출");
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public RateDiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
다음과 같이 AppConfig
에 호출 시 콘솔에 메소드가 출력되도록 코드를 추가하였다.
스프링 컨테이너는 @Bean
어노테이션이 붙은 메소드를 호출하여 반환하는 객체를 컨테이너에 등록한다. 그리고 컨테이너는 싱글톤으로 관리되기 때문에 같은 타입의 객체가 존재할 수 없다. 하지만 작성한 자바 설정코드에는 4개의 @Bean
이 있고 모두 실행된다면 new 키워드로 인해 각각의 객체가 생성될 것이라고 타당한 추측을 할 수 있다.
이런 의문을 품고 다시 테스트 코드를 실행해본 결과는 아래와 같다.
한번씩 메소드가 실행된다면 memberRepository()
가 3번 호출되어 3개의 객체 인스턴스가 생성되어야 하는데 1번만 호출되었다.
바이트코드 조작
@Bean
을 가진 메소드 뿐 아니라 AnnotationConfigApplicationContext
에 파라미터로 넘긴 AppConfig
도 스프링 빈으로 등록이 된다. 따라서 getBean()
을 통해 객체를 받아와 정보를 출력할 수 있다.
객체 정보를 출력해보면 위와 같은 형식으로 된 것을 알 수 있다. 우리가 정의하지 않은 xxxCGLIB와 같은 내부 클래스가 정의되어 있는 것을 볼 수 있는데 이것은 스프링이 CGLIB
이라는 바이트코드를 조작하는 라이브러리를 사용해서 AppConfig
를 상속받은 클래스를 작성하고, 상속한 클래스를 스프링 빈으로 등록한 것이다. 이것때문에 AppConfig
는 상속한 클래스의 부모 타입이기 때문에 AppConfig
타입으로도 조회가 가능하기도 하다.
내부의 코드가 어떻게 정의되어 있는지는 상당히 복잡해 간단히 설명을 한다면,
@Bean
이 붙은 메소드를 조회할 때 이미 컨테이너에 존재하는 스프링 빈이 있다면 존재하는 빈을 등록하고, 존재하지 않다면 기존의 메소드를 호출해서 반환되는 빈을 등록하는 로직으로 이루어졌을 가능성이 높다.
이런 로직을 적용하기 때문에 싱글톤으로 관리가 되는 것이다.
@Configuration을 사용하지 않는다면?
결과적으론 @Bean
만을 사용해도 스프링 빈으로 등록을 할 수 있다. 하지만 @Configuration
을 사용하지 않을 경우 싱글톤으로 관리되는 것을 보장하지 않는다.
순수한 자바 코드로 작동하기 때문에 이 글의 처음 부분에 의문을 가졌던 new 키워드를 통한 객체 인스턴스 생성이 메소드마다 호출될 것이다. 따라서 특별한 경우가 아니라면 스프링 빈을 등록할 때는 무조건 @Configuration
을 사용하는 것이 좋다.
> reference
[스프링 핵심 원리 - 기본편] - 김영한