Blog

[SpringCore] 싱글톤 패턴 - @Configuration의 싱글톤 테스트

Category
Author
citeFred
citeFred
Tags
PinOnMain
1 more property
AppConfig는 일반적인 자바 코드 처럼 작동되지 않는 모습을 보여준다. 스프링에서 어떤 방식으로 작동되는 것인가?
Table of Content

AppConfig의 이상한 점

분명 AppConfig 또한 자바 코드기 때문에
1.
memberService가 호출 될 때 memberRepository를 new로 생성하고
2.
orderService가 호출 될 때도 memberRepository를 new로 생성한다
그럼 각각 new 인스턴스를 통해 다른 주소의 객체가 생성될 것으로 보여진다.
package hello.core; import hello.core.discount.DiscountPolicy; import hello.core.discount.RateDiscountPolicy; import hello.core.member.MemberRepository; import hello.core.member.MemberService; import hello.core.member.MemberServiceImpl; import hello.core.member.MemoryMemberRepository; import hello.core.order.OrderService; import hello.core.order.OrderServiceImpl; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @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 RateDiscountPolicy(); } }
Java
복사

동일한 인스턴스(싱글톤)인 테스트

하지만 테스트 코드를 통해서 각 서비스의 memberRepository를 조회해보면 동일한 인스턴스임을 확인 할 수 있다. 스프링은 어떻게 싱글톤을 유지할 수 있는 것일까?
package hello.core.singleton; import hello.core.AppConfig; import hello.core.member.MemberRepository; import hello.core.member.MemberServiceImpl; import hello.core.order.OrderServiceImpl; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class ConfigurationSingletonTest { @Test void configurationTest() { //given 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); //when MemberRepository memberRepository1 = memberService.getMemberRepository(); MemberRepository memberRepository2 = orderService.getMemberRepository(); //then System.out.println("memberService -> memberRepository = " + memberRepository1); System.out.println("orderService -> memberRepository = " + memberRepository2); System.out.println("memberRepository = " + memberRepository); Assertions.assertThat(memberService.getMemberRepository()).isSameAs(memberRepository); Assertions.assertThat(orderService.getMemberRepository()).isSameAs(memberRepository); } }
Java
복사

호출의 문제인지 테스트

다시 궁금증으로 돌아가서 정상적인 메서드 호출을 예측하기 위해 코드를 추가한다.
package hello.core; import hello.core.discount.DiscountPolicy; import hello.core.discount.RateDiscountPolicy; import hello.core.member.MemberRepository; import hello.core.member.MemberService; import hello.core.member.MemberServiceImpl; import hello.core.member.MemoryMemberRepository; import hello.core.order.OrderService; import hello.core.order.OrderServiceImpl; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration 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() { return new RateDiscountPolicy(); } }
Java
복사
예측에서는
1.
처음 memberService가 출력, memberRepository() 호출
2.
memberRepository 출력
3.
memberRepository 자체가 호출하여 출력
4.
orderService가 출력, , memberRepository() 호출
5.
memberRepository 출력
이처럼 5개의 로그가 남을 것으로 예상되지만 실제로는 3번의 호출만 기록되는 것을 볼 수 있다.

@Configuration이 하는 일

위 처럼 일반적인 자바 코드의 작동이 안되도록(여러 인스턴스가 생성되지 않는≠싱글톤을 유지 할 수 있는) 스프링은 어떻게 제어하고 있을까?
이전 모든 빈 조회를 통해서 AppConfig도 빈으로 등록되어 있던것을 확인했었다. 그런데 해당 클래스 정보를 다시 확인하면 예상과 다른부분이 있다.
@Test void configurationDeep() { ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class); AppConfig bean = ac.getBean(AppConfig.class); System.out.println("bean = "+ bean.getClass()); }
Java
복사
예상대로는
bean = class hello.core.AppConfig
같은 클래스 정보가 출력 될 것으로 보여지지만 실제로는
bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$713455cd
처럼 복잡한 모습이 나타난다. 이것은 직접 만든 클래스가 아니라 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고 그 클래스를 빈으로 등록한 것이라는 것
이 실행을 @Configuration 어노테이션이 명시된 클래스에 적용되는 것
이 스프링이 임의로 만든 다른 클래스가 싱글톤을 보장되도록 해준다.(실제 CGLIB 내부 기술은 매우 복잡)
예상되는 코드로는 검증(조건문 등으로 호출 대상이 기존 스프링 컨테이너에 등록되어 있으면 찾아서 반환 하는 방식)이 사용 되었을 것으로 예측된다.
@Bean이 작성된 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환, 없으면 스프링 빈으로 등록을 반복
이런 방식에서 싱글톤이 유지되는 것

@Configuration을 제거하여 상태 확인

어노테이션을 제거하여 최초 예측처럼 작동되는지 확인해본다.
package hello.core; import hello.core.discount.DiscountPolicy; import hello.core.discount.RateDiscountPolicy; import hello.core.member.MemberRepository; import hello.core.member.MemberService; import hello.core.member.MemberServiceImpl; import hello.core.member.MemoryMemberRepository; import hello.core.order.OrderService; import hello.core.order.OrderServiceImpl; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; //@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(); } }
Java
복사
테스트 실행 결과 처음 예상했었던대로 memberRepository가 3번 호출 되고 있는 모습이다.
더하여 각 호출부분이 생성한 Repository 인스턴스가 모두 다른 주소로 나타나고 있다.
이것은 싱글톤이 깨졌다는 것을 말하며 @Configuration 어노테이션이 싱글톤을 관리하고 있음을 알 수 있다.
Search
 | Main Page | Category |  Tags | About Me | Contact | Portfolio