대부분 스프링 애플리케이션은 웹 애플리케이션이다. 웹 애플리케이션의 특성상, 보통 수많은 고객이 동시에 요청을 한다.
위와 같은 상황에서, 초당 만명의 클라이언트가 스프링 DI컨테이너에 요청을 하면, 초당 만개의 빈 객체가 생성되고 소멸되기 때문에 메모리 낭비가 심하다. 이를 해결하기 위해서 객체를 딱 1개만 생성하고 이 빈 객체를 공유하는 싱글톤 패턴이 등장하였다.
하나의 클래스에 대해 하나의 인스턴스만 생성되는 것을 보장하는 디자인 패턴으로, private 생성자를 사용하여 외부에서 임의로 new를 통해 객체를 생성하는 것을 막는다.
public class SingletonService { // //1. static 영역에 객체를 딱 1개만 생성 private static final SingletonService instance = new SingletonService(); // //2. public 으로 열어서 객체 인스턴스가 필요하면 이 static 메서드를 통해서만 조회하도록 허용한다. public static SingletonService getInstance() { return instance; } // //3. 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 하지 못하게 막는다. private SingletonService() { } }
3에서 생성자를 private로 선언하고 1에서 클래스 내부에서 생성자를 호출하여 static 영역(스택 영역)에 인스턴스를 생성한다. 외부에서 이 클래스 인스턴스를 사용하고자 하면 2를 호출하여 사용한다.
하지만, 이 방법은 의존관계상 클라이언트가 구체 클래스에 의존하여 DIP를 위반하게 된다.(코드를 짜보면 당연하다는 것을 알게됨.) DIP를 위반하니, OCP를 위반할 가능성이 높고, private 생성자 때문에 자식 클래스를 만들기 어려워 유연성이 떨어진다. 결국, 이 방법은 사용하지 않는다.
스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.
실제 스프링 컨테이너의 싱글톤 빈 관리 그림이다.
싱글톤 방식에서는 객체의 상태를 유지(stateful)하게 설계하면 안된다. 무상태(stateless)로 설계해야 한다. 따라서, 특정 클라이언트가 값을 변경하여 특정 클라이언트에 의존적인 필드가 있으면 안되고 가급적 읽기만 가능해야 한다.
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; } }
public class StatefulServiceTest { // @Test void statefulServiceSingleton(){ // ApplicationContext applicationContext = new AnnotationConfigApplicationContext(TestConfig.class); // StatefulService bean1 = applicationContext.getBean(StatefulService.class); StatefulService bean2 = applicationContext.getBean(StatefulService.class); // //쓰레드A. A사용자가 만원 주문 bean1.order("A", 10000); //쓰레드B. A사용자가 만원 주문 bean2.order("B", 20000); // int price = bean1.getPrice(); //만원을 기대했으나 2만원 나옴 // System.out.println("price = " + price); Assertions.assertThat(bean1.getPrice()).isEqualTo(10000); } // @Configuration static class TestConfig { // @Bean public StatefulService statefulService() { // return new StatefulService(); //생성자 호출 } } }
위 코드에서 사용자 A는 만원을 주문했는데, 주문 후 사용자 B가 2만원을 주문하여 StatefulService 빈 객체의 price값이 2만으로 세팅되어, 사용자 A의 getPrice()를 하면 20000이 나오는 치명적인 오류를 범한다.
@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(); } }
위 코드에서 memberService()와 orderService() 두 함수에서 memberRepository()를 호출하여 총 두번 memberRepository 빈 객체가 생성되는 것처럼 보이는데, 그렇지 않다.
AnnotationConfigApplicationContext에 파라미터로 AppConfig.class(컨테이너 설정 클래스)를 넘기면 AppConfig도 스프링 빈으로 등록되는데, 이때, 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 이용하여 AppConfig를 상속받은 임의의 클래스를 만들고 그 클래스를 스프링 빈을 등록한다.
스프링이 조작하여 만든 클래스는 위 사진과 같다.
CGLIB이 조작한 AppConfig의 내부 코드는 아래와 같다. (로직만...)@Bean public MemberRepository memberRepository() { // if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) { return 스프링 컨테이너에서 찾아서 반환; } else { //스프링 컨테이너에 없으면 기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록 return 반환 } }
CGLIB은 @Bean이 붙은 매서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고 빈이 없으면 기존 로직을 호출하여 빈을 생성하는 코드를 삽입한다.
**참고 : 스프링 핵심 원리 - 기본편 (김영한)