📖 ✏️
- TIL 시리즈에 작성된 글은 '매일 매일 학습한 지식 조각을 메모해 놓은 포스팅'입니다. 공유가 아닌 개인적인 학습 내용 기록을 목적으로 작성되었음을 알려드립니다.
- 그 외 시리즈에 작성된 공유 목적의 포스팅은 시간이 날 때마다 별도로 작성하고 있습니다. 주로, TIL 시리즈에 작성된 내용에서 특정 주제를 선정하고, 더 깊이 공부한 후 정리하여 작성합니다.
스프링 컨테이너는 싱글톤을 보장한다. 아래와 같이 @Configuration
애노테이션이 지정되어 있는 설정 클래스는 의존 관계 주입을 위해 객체가 여러번 생성되어도 싱글톤이 보장된다.
@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());
}
// ...이하 생략
위 코드를 살펴보면 memberService
Bean을 등록하는 과정에서 memberRepository()
를 호출하여 MemoryMemberRepository
객체를 생성한다. 이어서 memberRepository
Bean을 등록하기 위해 MemoryMemberRepository
객체가 또 한 번 생성된다. 마지막으로 orderService
Bean을 등록하는 과정에서 memberRepository()
가 호출되고 MemoryMemberRepository
객체가 또 다시 생성된다.
각각 서로 다른 3개의 MemoryMemberRepository
가 생성되는 코드라는 것은 자명하다. 이로써, 하나의 객체를 공유하여 사용하는 싱글톤 패턴이 성립하지 않는 듯 보인다. 스프링 컨테이너는 정말 싱글톤 패턴을 유지하고 있을까? 만약 싱글톤을 보장한다면 어떤 원리로 이 문제를 해결하고 있을까?
먼저, 스프링 컨테이너가 싱글톤을 보장하고 있는지 테스트를 진행해 보자. 서로 다른 3개의 MemoryMemberRepository
객체가 동일한 객체가 맞는지 확인하는 테스트다.
객체 간 참조값을 비교해보기 위해 사전 준비가 필요하다. MemberServiceImpl
이 생성하는 MemberRepository
의 구현체를 확인할 수 있도록 getMemberRepository()
를 만들었다.
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
// AppConfig에서 2번 생성된 MemberRepository 객체의 동일 여부 확인을 위해 임시로 만든 메서드
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
마찬가지로 OrderServiceImpl
객체가 생성하는 MemberRepository
구현체도 조회할 수 있도록 getMemberRepository()
를 만들었다.
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
// 설계가 잘된 이유는 discountPolisy가 알아서 해주기 때문이다.
// 단일 책임 원칙을 잘 지킨 경우라고 할 수 있다.
return new Order(memberId, itemName, itemPrice, discountPrice);
}
// AppConfig에서 2번 생성된 MemberRepository 객체의 동일 여부 확인을 위해 임시로 만든 메서드
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
다음으로, 스프링 컨테이너를 생성하고 getMemberRepository()
를 이용해 두 객체를 조회할 수 있도록 클래스를 만들었다. 클래스 이름은 ConfigurationSingletonTest
다.
getBean()
를 이용해 MemberServiceImpl
구현체와 OrderServiceImpl
구현체를 꺼내 온다. 테스트를 위해 각 구현체에 임시로 만들어 놓은 getMemberRepository()
를 이용해 객체를 각각 조회한다. 추가로 AppConfig.class
의 memberRepository()
를 호출하여 실제 반환 객체인 MemoryMemberRepository()
도 꺼내 온다.
세 가지 방법으로 각기 다른 곳에서 생성된 MemberRepository
구현체의 참조값을 비교하는 테스트를 진행한다.
public class ConfigurationSingletonTest {
@Test
void configurationTest() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// MemberRepository를 생성하는 두 Service 구현체를 꺼내온다.
MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
// AppConfig에서 생성되는 MemberRepository 구현체도 꺼내온다.
MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);
// 각각의 구현체가 필드 변수에 가지고 있는 memberRepository 객체를 조회한다.
MemberRepository memberRepository1 = memberService.getMemberRepository();
MemberRepository memberRepository2 = orderService.getMemberRepository();
// 세 가지 방식으로 생성된 MemberRepository 구현체의 참조값을 비교한다.
System.out.println("memberServiceImpl -> memberRepository1 = " + memberRepository1);
System.out.println("orderServiceImpl -> memberRepository2 = " + memberRepository2);
System.out.println("memberRepository = " + memberRepository);
// 세 객체의 참조값이 동일한지 검증한다.
assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
}
}
테스트를 실행해보면 모두 통과한다. 아래와 같이 로그를 통해 세 객체의 참조값이 일치하다는 사실도 확인할 수 있다.
MemberRepository
구현체는 MemberServiceImpl
와 OrderServiceImpl
에서 각각 생성되는 듯 보이지만, 스프링 컨테이너는 여전히 싱글톤을 보장하고 있다. 어떤 원리로 싱글톤이 보장되는 것일까?
AppConfig
의 자바 코드를 보면 아래와 같이 new MemoryMemberRepository()
가 분명 호출되어야 한다.
- Bean에 등록하기 위해 @Bean이 붙어있는 memberRepository() 호출
- memberService() 로직에서 memberRepository() 호출
- orderService() 로직에서 memberRepository() 호출
실제로 그러할까? 메서드 실행 단위마다 출력 로그를 남겨 확인해 보았다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
System.out.println("call AppConfig.memberService");
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemoryMemberRepository 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 FixDiscountPoliocy();
return new RateDiscountPolicy(); // 기획자가 배우를 변경하듯, AppConfig에서 할인 정책을 변경한다.
}
}
출력 결과를 보면, call AppConfig.memberRepository
는 단 한 번만 호출되었다.
스프링 컨테이너는 여러번 객체를 생성하는 코드를 그대로 따르지 않는다. 언제나 하나의 객체를 사용하는 싱글톤을 보장한다. 그 원리는 @Configuration
에 있다.
스프링 컨테이너가 싱글톤을 보장할 수 있는 이유는 @Configuration
를 이용한 CGLIB 바이트코드 조작 라이브러리를 사용하기 때문이다.
직접 확인해 보자. 아래와 같이 스프링 컨테이너 객체를 생성하고, getBean()
을 이용하여 Bean에 등록된 자바 설정 파일 객체를 조회했다.
@Test
void configuration_깊이_알아보기() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean.getClass() = " + bean.getClass());
}
아래와 같은 출력 결과를 확인할 수 있다.
getClass()
로 조회하면 클래스명인 class hello.core.AppConfig
까지만 나오는 것이 정상이다. 뒤에 이어서 붙어있는 EnhancerBySpringCGLIB
는 무엇일까?
스프링은 CGLIB
라는 바이트코드 조작 라이브러리를 사용하여, AppConfig
클래스를 상속하는 임의의 클래스를 생성한다. 그렇게 생성된 클래스가 스프링 Bean으로 등록된다. 스프링은 컨테이너의 설정 파일로 파라미터의 추가했던 AppConfig
를 사용하지 않는다.
AppConfig
를 상속받은 새로운 인스턴스(CGLIB)
를 사용한다. 그리고 새로운 인스턴스가 싱글톤을 보장한다. AppConfig@CGLIB
의 내부에서는 Bean에 등록된 메서드를 오버라이드 한다. 오버라이드 된 메서드는 중복 객체를 생성하지 않고, 기존 객체를 공유한다. 싱글톤을 보장하도록 메서드 기능을 오버라이드하는 것이다.
아래는 오버라이드된 memberRepository()
의 예상 로직이다. 해당 객체의 생성 여부에 따라 새로운 객체를 생성하거나, 기존에 생성되어 컨테이너에 보관중인 객체가 있다면 찾아서 반환하는 방식으로 싱글톤을 보장한다.
@Bean
public MemberRepository memberRepository() {
if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
return 스프링 컨테이너에서 찾아서 반환;
} else { //스프링 컨테이너에 없으면 기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
return 반환
}
}
참고
AppConfig@CGLIB는 AppConfig의 자식 타입이므로, AppConfig 타입으로 조회가 가능하다.
@Configuration
을 적용하지 않으면, 스프링은 싱글톤을 보장하지 않는다. 바이트코드를 조작하는 CGLIB 기술
을 사용하는 key가 @Configuration
에 있기 때문이다.
@Configuration
를 제거하면 AppConfig
가 CGLIB 기술
없이 순수한 AppConfig
클래스로 스프링 Bean에 등록된다. 앞서 진행했던 객체 생성 테스트를 동일하게 실행해보면, 아래와 같이 MemberRepository
가 총 3번 생성된다.
call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService
call AppConfig.memberRepository
call AppConfig.memberRepository
당연히 각각의 인스턴스 참조값도 모두 다르다. 엄연히 다른 객체이기 때문이다.
@Configuration
이 적용된 스프링 컨테이너에서 CGLIB 기술이 적용된다.@Bean
만 사용하면 스프링 Bean 등록은 가능하다. 하지만 싱글톤은 보장되지 않는다. @Configuration
을 사용해야 한다.