Configuration과 싱글톤
스프링은 정말 싱글 톤 패턴을 보장해줄까요?
AppConfig를 보면 memberService와 odrderService가 호출되면서 각각 memberRepository가 호출되고 이로인해 MemoryMemberRepository가 각각 두개 생성되는 것으로 보입니다. 이는 싱글톤이 깨지는 것인데요. 그래서 진짜로 싱글톤이 깨지는지 확인해보도록 하겠습니다.
@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();
}
... }
같은 객체인지 확인해보기위해 객체를 출력하는 함수 getMemberRepository를 테스트용으로 잠깐 만들겠습니다.
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
//테스트 용도
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
//테스트 용도
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
테스트 코드
@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("memberRepository = " + memberRepository); //모두 같은 인스턴스를 참고하고 있다.
assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
}
출력값
memberService -> memberRepository =hello.core.member.MemoryMemberRepository@708400f6
orderService -> memberRepository =hello.core.member.MemoryMemberRepository@708400f6
memberRepository =hello.core.member.MemoryMemberRepository@708400f6
아래와 같이 스프링을 이용해서 memberService -> memberRepository, orderService -> memberRepository, memberRepository 3개의 객체를 비교해봤습니다. 모두 같은 객체인 것을 확인해 볼 수 있습니다. 자바코드상으로는 이해할 수 없지만 싱글톤이 보장되고있습니다.
AppConfig가 그대로 수행되는 것이 아닌 뭔가 중간에 스프링 컨테이너에서 내부적으로 개입이 일어나야 가능한 일로 추측해 볼 수 있습니다.
@Configuration과 바이트코드 조작
AppConfig도 ac로 등록하기 위해 넘겨줄 때 스프링빈으로 등록이 됩니다. 따라서 AppConfig 스프링 빈을 조회해보도록 하겠습니다.
@Test
void configurationDeep() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
//AppConfig도 스프링 빈으로 등록된다.
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
}
출력값
bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$6fd0acc1
출력값을 보면 객체뒤에 이상하게 문자들이 붙으면서 순수한 AppConfig객체와 달라진 것을 볼 수 있습니다. 이는 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig를 조작한뒤 새로운 객체를 스프링 빈으로 등록해서 사용하고 있는 것입니다. 그림으로 나타내면 아래와 같은 상황입니다.
바이트 조작은 이미 등록된 객체면 그 객체를 반환하고 아니면 객체를 등록하도록 했을 것이며 그렇기 때문에 중복으로 같은 클래스의 객체가 생기지 않아 싱글톤을 보장할 수 있는 것입니다.
이는 @Configuration이라는 어노테이션 덕분에 일어나는 일입니다.
그렇다면 아래와 같이 @Configuration을 지워본뒤 AppConfig 스프링빈을 꺼내보도록 하겠습니다
//@Configuration 삭제
public class AppConfig {
}
출력값
bean = class hello.core.AppConfig
아까와는 다르게 바이트조작이 일어나지 않은 AppConfig가 조회됩니다. 따라서 스프링을 써도 싱글톤을 보장해주지 않습니다.