만약, 스프링 없는 순수 DI 컨테이너를 통해 여러 고객이 조회 요청한다면 어떻게 될까?
@Test
@DisplayName("스프링 없는 순수한 DI 컨테이너")
void pureContainer() {
AppConfig appConfig = new AppConfig();
//1. 조회: 호출할 때 마다 객체를 생성
MemberService memberService1 = appConfig.memberService();
//2. 조회: 호출할 때 마다 객체를 생성
MemberService memberService2 = appConfig.memberService();
//참조값이 다른 것을 확인
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
//memberService1 != memberService2
assertThat(memberService1).isNotSameAs(memberService2);
}
Test결과를 확인하니 두 객체 참조값이 달랐고, AppConfig
요청을 할 때마다 객체를 새로 생성한다는 사실을 확인했다. 트래픽이 초당 100 아니 1,000번이 발생한다면 1,000개의 객체가 생성되고 소멸된다. 즉, 메모리 낭비가 심하다.
위 문제를 해결할 수 있는 방안은 객체를 딱 1개만 생성하고, 이를 공유하도록 설계하면 된다.
그래서 만들어진 디자인 패턴이 바로 싱글톤 패턴이다.
private
생성자를 사용하여 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야 한다!싱글톤 패턴을 구현하는 방법은 여러가지가 있다. 본 글에서는 생략한다
스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤으로 관리한다.
전 글에서 학습한 스프링 빈
이 바로 싱글톤
으로 관리되는 빈이다.
싱글톤 레지스트리
라 한다.@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
// AppConfig appConfig = new AppConfig();
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
//참조값이 같은 것을 확인
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
//memberService1 != memberService2
assertThat(memberService1).isSameAs(memberService2);
}
스프링 컨테이너 ApplicationContext
를 사용해 AppCofig.class
를 빈으로 등록시키고, 두 서비스를 호출하여 참조값을 비교해보니 같은 인스턴스임을 확인할 수 있다.
문제를 확인해보기 위해 order()
호출 시 필드 price
를 공유하는 다음과 같은 코드가 존재한다.
public class StatefulService {
private int price;
public void order(String name, int price) {
System.out.println("name = " + name + " price = " + price);
this.price = price; //여기가 문제!!
}
public int getPrice() {
return price;
}
}
@Test
void statefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
//ThreadA: A사용자 10000원 주문
statefulService1.order("userA", 10000);
//ThreadB: B사용자 20000원 주문
statefulService2.order("userB", 20000);
//ThreadA: 사용자A 주문 금액 조회
int price = statefulService1.getPrice();
System.out.println("price = " + price);
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
order()
호출 시 price 필드
를 공유하기 때문에 B사용자의 주문 금액이 저장되어 사용자A 주문 금액 조회 시 20,000이 조회되는 문제가 발생하기 때문이다.
문제를 해결하기 위해선 원초적으로 상태를 유지하지 않는 무상태(stateless)로 설계해야 한다.
@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 FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
위 코드를 보고 있으면 뭔가 이상하다. 스프링 컨테이너가 실행될 때 각 빈들을 호출하면 각 빈들이 몇번 호출 되는지 세어보자.
memberService()
: 1번memerRepository()
: 3번orderService()
: 1번discountPolicy()
: 1번어.. 이상하다. 그렇다면 memerRepository()
는 2개 저장되어 싱글톤 패턴이 깨지는게 아닐까?
AppCofig.class
의 각 빈을 호출할 때 call AppCofig.~~
를 출력시키고, memberService()
와 orderService()
의 MemberRepository
가 다른지 비교해보자.
싱글톤 패턴이 깨진다면 memberService()
와 orderService()
호출 시 생성되는 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);
MemberRepository memberRepository1 = memberService.getMemberRepository();
MemberRepository memberRepository2 = orderService.getMemberRepository();
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);
}
테스트 결과는 memeberService()
와 orderService()
그리고 memberRepository()
까지 같은 MemberRepository
가 생성되었음을 확인할 수 있었고, 각 메서드들은 한번 씩 출력됨을 출력문을 통해 확인할 수 있다.
위 테스트 결과가 나온 이유는 바로 스프링이 클래스의 바이트코드를 조작하는 라이브러리를 사용하기 때문이다.
@Configuration
을 사용하면 내가 만든 클래스를 스프링 빈으로 등록하는게 아니라, CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들어 이를 스프링 빈으로 등록한다.
@Test
void configurationDeep() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
}
AppConfig
의 className을 예상해보면 hello.core.AppCofig
가 나와야되지만, 실제로는 뒤에 ~~CGLIB가 붙어있는 className을 볼 수 있다.
즉 스프링 컨테이너에는 실제로 다음 그림과 같이 CGLIB를 통해 만들어진 인스턴스가 들어가 있다.
@Bean
public MemberRepository memberRepository() {
if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
return 스프링 컨테이너에서 찾아서 반환;
} else { //스프링 컨테이너에 없으면
기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
return 반환
}
}
이 코드는 김영한 강사님께서 예시로 든 AppConfig@CGLIB 예상 코드다.
스프링 컨테이너는 AppCofig.class
를 복제한 AppConfig@CGLIB.class
를 만들고 위 코드와 같은 로직을 통해 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없다면 생성해서 등록하고 반환하는 코드가 만들어진다.
위 같은 방법을 사용하여 싱글톤을 보장시킨다.
@Bean
만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.@Configuration
을 사용하자!지난 여름 미니 프로젝트를 진행하다, 어디에서 '싱글톤이 좋다', '싱글톤 개발을 해야한다.'라는 말들을 듣고 '스프링에서 싱글톤은 어떻게 적용 시키는거지?' 하며 구글링을 열심히 찾아봤던 기억이 있다.
개념만 잠깐 공부하다 어려워보여서 창을 닫긴했지만..
이번 싱글톤 컨테이너를 공부하면서, 스프링 컨테이너와 빈 그리고 싱글톤 컨테이너까지 연결고리가 딱 딱 맞춰지는 느낌이 들어서 너무 좋고, 개념을 터득하니 나중에 프로젝트를 진행할 때 좀 더 좋은 코드들을 작성할 수 있다는 자신감이 든다.