스프링 컨테이너와 싱글톤(feat. @Configuration)
AppConfig 설정
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
(Configration의 설정값)
@Configration을 사용하여 의존관계의 설정을 하고 ApplicationContext 에 해당 설정값을 넣으면 스프링 컨테이너 안에 있 스프링 컨테이너에서는 아래 그림과 같이 빈 저장소 @Bean 어노테이션으로 등록된 메서드와 클래스를 관리한다.
단순한 자바코드의 설정값과 다를게 없어 보인다. 하지만 @Configuration은 싱글톤을 보장하는데 큰 역할을 한다.
@Bean 혹은 @Component로 설정하면 Sping이 싱글톤을 보장한다고 알고 써 왔었다. 싱글톤을 보장하긴 한다.
하지만 위와 같은 AppConfig의 코드처럼 서로의 생성자로 관계가 얽혀있는 상황에서는 이야기가 다르다.
아래 테스트 코드는 AppConfig 클래스에서 @ Configuration 어노테이션을 빼고 돌려본 테스트이다.
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("Configuration 어노테이션 없이 테스트")
void configTest() {
MemberRepository findBean1 = ac.getBean("memberRepository", MemberRepository.class);
MemberRepository findBean2 = ac.getBean("memberRepository", MemberRepository.class);
System.out.println("findBean1 = " + findBean1);
System.out.println("findBean2 = " + findBean2);
Assertions.assertThat(findBean1).isEqualTo(findBean2);
}
같은 주소값을 반환하는데 싱글톤을 보장하는 것이 아닌가?? 라는 생각을 할 수 있다. 하지만 아래와 같이 테스트 했을 때는 두 객체의 주소값이 다른 걸 확인 할 수있다.
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("Configuration 어노테이션 없이 테스트2")
void configTest2() {
MemberServiceImpl findMemberService = ac.getBean("memberService", MemberServiceImpl.class);
MemberRepository findBean1 = findMemberService.getMemberRepository();
MemberRepository findBean2 = ac.getBean("memberRepository", MemberRepository.class);
System.out.println("findBean1 = " + findBean1);
System.out.println("findBean2 = " + findBean2);
Assertions.assertThat(findBean1).isEqualTo(findBean2);
}
@Bean으로 등록된 MemberRepository가 싱글톤으로 생성되지 않은 것을 확인 할 수있다.
이유는 MemberService의 생성자는 memberRepository() 메서드를 통해 MemoryMemberRepository 객체를 생성해 주입한다. 때문에 위 테스트 케이스에서는 memberRepository() 메서드가 2번 실행된 것이다. 이는 객체를 생성하는 각 메서드에서만 싱글톤을 보장한다는 뜻이다.
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
/*MemberServiceImpl 객체를 생성할 때 memberRepository() 생성자로
MemoryMemberRepository 객체를 만들어 주입 받음*/
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
AppConfig에서 호출될 때마다 print를 찍어 확인 해보면 객체를 생성하는 메서드가 한번만 호출되는것이 아니라 해당 객체를 생성하는 메서드 내부에서 계속 호출하는 것을 볼 수있다.
public class AppConfig {
@Bean
public MemberService memberService() {
System.out.println("AppConfig.memberService");
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository 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 DiscountPolicy discountPolicy() {
System.out.println("AppConfig.discountPolicy");
return new RateDiscountPolicy();
}
}
@Configuration 어노테이션을 적용하고 다시 테스트를 돌려보았다.
@Configuration
public class AppConfig {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("Configuration 어노테이션 넣어 테스트")
void configTest3() {
MemberServiceImpl findMemberService = ac.getBean("memberService", MemberServiceImpl.class);
MemberRepository findBean1 = findMemberService.getMemberRepository();
MemberRepository findBean2 = ac.getBean("memberRepository", MemberRepository.class);
System.out.println("findBean1 = " + findBean1);
System.out.println("findBean2 = " + findBean2);
Assertions.assertThat(findBean1).isEqualTo(findBean2);
}
두개의 MemberRepository 객체가 같은 주소를 참조하고 있고(싱글톤 유지) 각 객체들이 한번씩 생성 메서드를 통해 생성됨을 볼 수 있다.
스프링은 객체의 싱글톤을 보장하기 위해 CGLIB 라는 Proxy 객체를 앞에 두고 AppConfig를 상속 받아 스프링 컨테이너에 등록해준다. 때문에 생성 메서드간에 중복 없이 싱글톤을 보장해 준다.