[Spring] 싱글톤 컨테이너

Seongho·2023년 1월 28일
0

Spring

목록 보기
6/13

스프링은 기업용 온라인 서비스 기술을 위해 탄생하였다.

대부분 스프링 애플리케이션은 웹 애플리케이션이다. 웹 애플리케이션의 특성상, 보통 수많은 고객이 동시에 요청을 한다.

위와 같은 상황에서, 초당 만명의 클라이언트가 스프링 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을 적용하면 스프링에서 싱글톤이 적용되도록 모든 것을 컨트롤해준다.

@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이 붙은 매서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고 빈이 없으면 기존 로직을 호출하여 빈을 생성하는 코드를 삽입한다.

**참고 : 스프링 핵심 원리 - 기본편 (김영한)

profile
Record What I Learned

0개의 댓글