minOS

싱글톤 컨테이너(2) -주의점 본문

TIL/김영한의 스프링 핵심 원리

싱글톤 컨테이너(2) -주의점

minOE 2024. 3. 7. 17:02
728x90

싱글톤 컨테이너

스프링 컨테이너는 싱글톤 패턴의 문제점을 해결해서 , 객체 인스턴스를 싱글톤(1개만 생성)으로 관리한다. 
스프링 빈이 바로 싱글톤으로 관리되는 빈이다.

싱글톤 컨테이너
- 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.
  ㄴ 이전에 설명한 컨테이너 생성 과정을 자세히 보면(https://onepunchprogram.tistory.com/41) 컨테이너는 객체를                 하나만 생성하여 관리한다.
- 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.
- 스프링 컨테이너의 이런 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.
   ㄴ 싱글톤 패턴을 위해 지저분한 코드가 들어가지 않아도 된다.
   ㄴ DIP,OCP,테스트,private 생성자로부터 자유롭게 싱글톤을 사용할 수 있다.
    @Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    void springContainer(){
        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);

        assertThat(memberService1).isSameAs(memberService2);


참조값 같음


테스트 통과


스프링 컨테이너 덕분에 객체를 생성하는 것이 아니라 이미 만들어진 객체를 공유하여 효율적으로 재사용할 수 있다.

참고) 스프링의 기본 빈 등록 방식은 싱글톤이지만, 요청할때마다 새로운 객체를 생성해서 반환하는 기능도 제공한다. 빈 스코프에서 설명하겠다.

 

싱글톤 방식의 주의점

- 싱글톤 패턴 또는 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다.
- 무상태(stateless)로 설계해야한다.
    1) 특정 클라이언트에 의존적인 필드가 있으면 안된다.
    2) 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
    3) 가급적 읽기만 해야한다.
    4)필드 대신에 자바에서 공유되지 않는 , 지역변수, 파라미터,Threadlocal등을 사용해야 한다.
- 스프링 빈의 필드에 공유 값을설정하면 정말 큰 장애가 발생할 수 있다.

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;
    }

}

테스트 코드
class StatefulServiceTest {
    @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);

        assertThat(statefulService1.getPrice()).isEqualTo(20000);

    }


    static class TestConfig{
        @Bean
        public StatefulService statefulService(){
            return new StatefulService();
        }

    }


출력 결과, 테스트 결과


단순한 설명을 위해 실제 쓰레드는 사용하지 않았다.
ThreadA가 사용자A 코드를 호출하고 ThreadB가 사용자B 코드를 호출한다 가정하자
StatefulService의 price 필드는 공유 필드인데 , 특정 클라이언트가 값을 변경하였다.
따라서 사용자 A의 price 가 10000원이 아니라 20000원이 나왔다.
스프링 빈은 무상태(stateless)로 설계해야한다.

수정 코드

public class StatefulService {
    //private int price ; // 상태를 유지하는 필드

    public int order (String name,int price){
        System.out.println("name = " + name + " price = " + price);
        return price;
    }
//    public int getPrice(){
//        return price;
//    }

}


테스트 코드

class StatefulServiceTest {
    @Test
    void statefulServiceSingleton() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        //ThreadA: 사용자A가 10000원 주문
        int  userAPrice = statefulService1.order("UserA", 10000);
        //ThreadB: 사용자B가 20000원 주문
        int userBPrice =statefulService2.order("UserB", 20000);

        //ThreadA: 사용자A 주문 금액 조회
        //int price = statefulService1.getPrice();
        System.out.println(userAPrice);

        //assertThat(statefulService1.getPrice()).isEqualTo(20000);

    }

    static class TestConfig{
        @Bean
        public StatefulService statefulService(){
            return new StatefulService();
        }

    }

}


출력결과

UserA의 금액이 제대로 나옴

728x90