본문으로 바로가기

[Spring] 싱글톤 방식의 주의점 | 무상태(stateless)

category Java/Spring 2022. 1. 21. 13:09
반응형

 

 

 

싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든,

 

객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은

여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에

싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안 된다.

 

 

 

 

무상태(stateless)로 설계해야 한다.

 

 

 

 

특정 클라이언트에 의존적인 필드가 있으면 안 된다.

(특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안 된다)

 

가급적 읽기만 가능해야 한다.

 

필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.

 

 

 

 

스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다.

 

 

 

 

 

 

 

상태를 유지할 경우 발생하는 문제점을 테스트 코드를 통해 예시로 살펴보자.

 

 

 

 

 

 


● 상태를 유지할 경우 발생하는 문제점 예시

 

 

 

 

StatefulService.java

package hello.core.singleton;

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)을 필드로 삼는 클래스를 생성했다.

 

 

이제 테스트 코드를 통해 무슨 문제점이 있는지 확인해보자.

 

 

 

 

 

 

 

StatefulServiceTest.java

package hello.core.singleton;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;

import javax.swing.plaf.nimbus.State;

import static org.junit.jupiter.api.Assertions.*;

class StatefulServiceTest {

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


	statefulService1.order("userA", 10000);
	statefulService2.order("userB", 20000);

        
        int price= statefulService1.getPrice();
        System.out.println("price = " + price);

        // Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
        Assertions.assertThat(userAPrice).isNotEqualTo(userBPrice);
    }


    static class TestConfig {

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

 

 

임시 스프링 컨테이너(TestConfig)를 생성하여 

 

하나의 statefulService 빈을 공유하는 객체를 두 개 생성하고

각각 다른 price를 넣어 order 메서드를 실행했다.

 

 

 

 

 

 

테스트 코드를 실행해보면..

 

 

 

 

 

 

userA의 주문금액을 출력했더니 10000 이 아닌 20000이 출력되었다.

 

 

왜냐하면 생성된 2개의 statefulService는 같은 빈 객체를 공유하기에

한 곳에서 price를 수정하면 다른 곳에도 영향을 끼치기 때문이다.

 

 

 

 


 

 

이러한 문제를 코드로 간단히 해결할 수 있다.

 

작성했던 코드들을 아래와 같이 수정한다.

 

 

 

 

 

 

StatefulService.java

package hello.core.singleton;

public class StatefulService {

    /*
        service에서 사용자가 공유하는 변수등을 수정하면
        실무에서 엄청난 문제가 생길 수 있다.(ex. 다른 사용자의 정보를 볼 수 있다.)

        해결책으로는 사용자가 공유하는 변수는 건드리지 못하게 한다.
        (이 클래스에선 들어오는 값을 바로 return해서 해결)
     */

    //private int price; // 상태를 유지하는 필드

    public int order(String name, int price){
        //this.price = price; // 여기가 문제
        System.out.println("name = "+ name + " price = " + price);
        return price;
    }

//    public int getPrice(){
//        return price;
//    }
}

 

공유되는 price 필드를 생성하지 않고,

 

order 메서드가 실행 시 바로 그 가격을 반환해주도록 한다.

 

 

 

 

 

 

 

 

StatefulServiceTest.java

package hello.core.singleton;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;

import javax.swing.plaf.nimbus.State;

import static org.junit.jupiter.api.Assertions.*;

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("price = " + userAPrice);

        // Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
        Assertions.assertThat(userAPrice).isNotEqualTo(userBPrice);
    }

    static class TestConfig {

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

 

StatefulSerivce 객체를 두개 생성하고

 

각각 order 메서드를 실행하면서 반환되는 값을 변수에 저장한다.

 

 

 

 

 

 

 

그리고 출력을 해보면,,

 

 

 

 

 

 

각각의 가격이 따로 저장됨을 알 수 있다.

 

 

 

 

 

 

 

스프링 빈은 항상 무상태(stateless)로 설계하자.

 

 

 

반응형