라이브러리&프레임워크/Spring

싱글톤패턴, 멀티쓰레드 stateful 문제

youngble 2024. 6. 30. 04:11

Singleton Pattern을 사용하여 중복하여 new 인스턴스를 생성하지않고 단일 인스턴스를 활용하여, 서비스 이용시 새로운 인스턴스를 생성하는 비용보다 인스턴스 하나를 공유하도록 설계해야 효율적이고 낭비없는 서비스를 제공하게된다.

 

하지만 싱글톤 패턴을 적용하려다보면 DIP, OCP를 위반하게되고 별도의 세팅을 위한 public 사용못하도록 Private 생성자함수 생성, new 인스턴스 생성 등을 하는 번거로운 코드작업이 추가되는 등 안티패턴이 생겨 이점보단 단점이 생길수있다고 한다.

 

하지만 스프링의 경우 이러한 싱글톤패턴의 단점을 제거하고 좋은점을 이끌어내도록 하는데 스프링컨테이너, 스프링의 빈이 이것에 속한다.

 

먼저 3가지의 경우의 코드를 예시로 보여주겠다. 첫번째는 싱글톤적용없이 사용할때, 두번째는 순수 자바 싱글톤을 적용할때, 세번째는 스프링 컨테이너, 빈을 사용한 싱글톤 작성예시이다.

 

1. 싱글톤 적용없이 적용한 코드 

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

    // memberService != mermberService2
    assertThat(memberService1).isNotSameAs(memberService2);
}

 

위처럼 직접 config작성한 AppConfig를 new 인스턴스로 생성하고, memberService1, 2 로 각각 다른 객체가 생성한다.

이렇게 됐을시 같지않을뿐도로 같은 기능을위해 별도의 코드, 생성이 추가되는 비용의 단점이 생긴다.

 

2. 싱글톤 적용한 순수 자바코드

void singletonServiceTest(){
    SingletonService singletonService1 = SingletonService.getInstance();
    SingletonService singletonService2 = SingletonService.getInstance();

    System.out.println("singletonService1 = " + singletonService1);
    System.out.println("singletonService2 = " + singletonService2);

    assertThat(singletonService2).isSameAs(singletonService1);
}

 

// 싱글톤 서비스 패턴
public class SingletonService {

    private static final SingletonService instance = new SingletonService();

    public static SingletonService getInstance(){ // 단일 instance 인스턴스 호출
        return instance;
    }

    private SingletonService(){ // 생성자를 Private으로 만들어서 외부 main에서 생성못하도록 막음
    }

    public void logic(){
        System.out.println("싱글톤 객체 로직 호출");
    }


}

 

중복된 객체 생성 비용을 제거하기위해 순수하게 자바 싱글톤 패턴을 사용한 예시이다. 다음과같이 private static final 로 SingletonService 참조 인스턴스를 생성하고 이를 getInstance 메서드를 통해 해당 클래스 인스턴스를 return해준다.

 

이렇게 instance하나를 공유함으로써 부득이한 객체생성을 막고 단일 클래스 인스턴스를 공유한다.

 

 

3. 싱글톤 적용한 스프링 컨테이너, 빈 코드

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

        // memberService != mermberService2
        assertThat(memberService1).isSameAs(memberService2);
    }

 

하지만 특정 config 에 의존하게되면서 DIP, OCP 을 꺠버리는 경우가 생기기때문에 이러한 사항을 제거하기위해 AnnotationConfigApplicationContext를 사용하여 DIP, OCP를 지키게 스프링컨테이너 사용 및 그안의 getBean을 사용한다.

 

 

이를 이용했을때 쓰레드 사용시에 문제가 있다.

 

다음과같이 싱글톤 패턴 서비스를 만들고 price를 공유하는 필드를 생성하여 getPrice메서드를 사용하여 불러올수있다고 생각해보자

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

이렇게 만들었을때 price는 stateful 상태값이기때문에 바뀐 값도 공유하게되는 문제가있다.

 

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

    // ThreadA : A사용자 1만원 주문
    int userA = statefulService1.order("userA", 10000);
    // ThreadB : B사용자 2만원 주문
    statefulService2.order("userB", 20000);

    //ThreadA: A사용가 주문 금액을 조회
    int pirce = statefulService1.getPrice();
    System.out.println("pirce = " + pirce);

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

 

위와같이 userA, userB가 각각 1만원, 2만원을 주문했다고했을때 price 는 각각 1만원 2 만원으로 호출되야하지만 최종 사용된 2만원이 출력되게 된다. 멀티쓰레드 사용시 이러한 현상이 나타난다면 잘못된 정보 뿐만아니라 잘못된 구조설계이기때문에 치명적인 오류이다.

 

 

따라서 다음과같이 수정하여 price를 공유하지않고 지역변수로써 사용하도록 한다.

 

public class StatefulService {

    public int order(String name, int price){ // 지역변수 사용
        System.out.println("name = " + name+ " price = " + price);
        return price;
    }

}

 

 

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

        // ThreadA : A사용자 1만원 주문
        int userAPrice = statefulService1.order("userA", 10000);
        // ThreadB : B사용자 2만원 주문
        int userBPrice = statefulService2.order("userB", 20000);

        //ThreadA: A사용가 주문 금액을 조회
        System.out.println("pirce = " + userAPrice);

    }

 

이렇게하면 각각 1만원 2만원이 출력된다.