@Configuration과 싱글톤 (with. CGLIB)
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
해당 코드는 OrderService, MemberService, MemberRepository 빈(Bean)을 정의하는 설정 파일의 코드이다.
이렇게 코드를 작성해둔다면 @Bean 애노테이션에 의해 스프링 빈으로 등록되고 싱글톤으로 관리된다.
그런데 이상한 부분이 있다.
MemberRepository 를 빈으로 등록하기 위해 return new로 구현체를 생성해주는 것은 알겠지만,
memberRepository() 메서드를 orderService(), memberService() 에서도 호출해주고 있으니
총 MemoryMemberRepository를 3번 생성해주는 것이 아닐까?
정말 싱글톤으로 관리가 되고 있는 것일까?
싱글톤 테스트
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
@SpringBootTest
public class AppConfigTest {
@Test
@DisplayName("싱글톤 테스트")
void singleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);
OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
MemberRepository memberRepository1 = orderService.getMemberRepository();
MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
MemberRepository memberRepository2 = memberService.getMemberRepository();
Assertions.assertThat(memberRepository).isSameAs(memberRepository1);
Assertions.assertThat(memberRepository).isSameAs(memberRepository2);
}
}
위 코드는 memberRepository가 싱글톤이 맞는 지 검증하는 테스트이다.
스프링 컨테이너에 존재하는 MemberRepository 빈과 OrderService, MemberService에 주입된 MemberRepository 가 같은 지 검사하는 테스트 코드로 구성되었다.
이 테스트 코드의 결과는 어떨까?
테스트 결과
테스트의 결과는 성공이다.
스프링이 MemberRepository를 싱글톤으로 관리할 수 있도록 처리해주었다는 얘기인데 과연 어떻게 그럴 수 있는 걸까?
@Configuration
그 이유는 바로 @Configuration에 있다.
스프링이 스프링 빈을 싱글톤으로 관리하도록 보장해준다고 하는데, 사실 스프링이 자바 코드까지 변경하기는 어렵다.
그래서 스프링은 바이트코드를 조작하는 CGLIB 라이브러리를 사용하는데
@Configuration 내에서 @Bean 을 통해 스프링 빈을 등록 시,
바이트코드 조작 라이브러리가 Config 객체의 코드를 조작하여 싱글톤을 보장하도록 코드를 변경시킨 CBLIB 객체를 생성하고
그 객체를 스프링 컨테이너에 빈으로 등록한다.
실제로 AppConfig의 class를 출력시켜보면 아래와 같이 출력되는 것을 확인할 수 있었다.
AppConfig appConfig = ac.getBean(AppConfig.class);
System.out.println(appConfig.getClass());
// 출력 값: class com.example.demo.AppConfig$$SpringCGLIB$$0
// 일반 객체의 경우, 이렇게 출력된다(class com.example.demo.AppConfig)
AppConfig가 CGLIB에 의해 컨테이너에 등록된 AppConfig@CGLIB 예상 코드를 보자면 아래와 같지 않을까 싶다. (실제로는 훨씬 복잡하게 구현되어있을 것이며, 이 코드는 간략한 예상 코드이다.)
@Bean
public MemberRepository memberRepository() {
if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
return 스프링 컨테이너에서 찾아서 반환;
} else { //스프링 컨테이너에 없으면
기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
return 반환
}
}
@Bean 만 사용한다면?
그렇다면 스프링 빈을 등록을 위해 사용되는 @Bean 애노테이션만 사용한다면, 스프링 빈을 싱글톤으로 관리하기 어려울까?
한번 테스트해보자
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
이전 AppConfig 코드에서 @Configuration 애노테이션을 제거했다.
테스트
그리고 아까와 똑같은 테스트를 다시 진행해보자
결과는 실패이다.
그럼 @Configuration 애노테이션이 없다면 어떻게 동작하는 것일까?
내부 동작
- 우선 Configuration 애노테이션이 없으니 CGLIB 라이브러리가 바이트코드를 조작하지 않고 싱글톤을 보장하지 않는다.
- 위와 같은 AppConfig 코드라면 memberRepository가 총 3개 생성되고 각각의 스프링 빈으로 등록된다.
@Bean
public MemberRepository memberRepository() {
System.out.println("memberRepository 생성");
return new MemoryMemberRepository();
}
memberRepository() 메서드에 위와 같이 로그를 추가하는 경우
총 3번의 memberRepository 생성이 발생하는 것을 알 수 있다.
@Configuration 애노테이션이 존재하는 경우에는 단 1번만 호출되는 것을 확인할 수 있었다.
결론
- 컴포넌트 스캔 방식이 아닌 직접 Config 객체 선언하여 IoC 컨테이너를 구성하는 경우에는 @Configuration, @Bean 애노테이션을 통해 스프링 빈들의 싱글톤을 보장할 수 있다.
- 스프링에서 @Configuration 내부에 @Bean 애노테이션을 통해 선언한 빈들이 싱글톤을 보장할 수 있는 것은 @Configuration 애노테이션에 의해 동작하는 CGLIB이라는 바이트 코드 조작 라이브러리에 의해서이다.
- @Configuration 애노테이션 없이 @Bean 만 붙이더라도, 스프링 빈 등록은 되지만 싱글톤 보장은 되지 않는다.