📖 ✏️
- TIL 시리즈에 작성된 글은 '매일 매일 학습한 지식 조각을 메모해 놓은 포스팅'입니다. 공유가 아닌 개인적인 학습 내용 기록을 목적으로 작성되었음을 알려드립니다.
- 그 외 시리즈에 작성된 공유 목적의 포스팅은 시간이 날 때마다 별도로 작성하고 있습니다. 주로, TIL 시리즈에 작성된 내용에서 특정 주제를 선정하고, 더 깊이 공부한 후 정리하여 작성합니다.
싱글톤 패턴을 사용 할 때, 주의할 점은 객체의 상태를 유지(stateful)하는 설계를 반드시 피해야 한다는 것이다. 싱글톤 방식은 객체 인스턴스를 하나만 생성하고 공유하여 사용한다. 따라서 여러 클라이언트가 하나의 객체를 공유하게 된다. 이때 무상태(stateless)가 아니라면, 하나의 객체가 갖는 상태를 여러 클라이언트가 공유하는 결과가 생긴다.
따라서, 싱글톤 패턴을 사용하는 클래스는 반드시 무상태(stateless)를 유지해야 한다. 무상태(stateless)란 상태를 공유하는 필드 변수가 없는 것을 의미한다. 특정 클라이언트가 의존할 수 있는 필드 변수가 존재하면 안된다. 당연히 값을 변경할 수 없어야 한다. 가능한 메서드를 이용해 값을 읽기만 할 수 있도록 한다.
싱글필드 변수 대신 공유되지 않는 변수를 사용하면 좋다. 지역 변수, 파라미터, ThreadLocal이 이에 해당한다. 스프링 빈의 필드에 공유되는 변수가 존재하면 서비스에 큰 문제가 발생할 가능성이 크다.
아래 예시 코드를 통해 어떤 문제가 발생하는지 확인해보자.
상태가 있는(stateful) 클래스
클라이언트의 요청에 의해StatefulService
객체가 여러개 생성된 상황이다.StatefulService
객체는order()
를 통해 필드 변수에price
를 넣을 수 있고,getPrice()
를 통해price
을 조회할 수있다.
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;
}
}
상태가 있는(stateful) 객체를 여러 클라이언트가 조회할 때 발생하는 문제
문제는 싱글톤 패턴이 하나의 객체만을 생성하고, 공유하여 사용하기 때문에 여러 클라이언트가 price를 변경할 수 있다는 점이다. 상태를 공유하므로, 특정 클라이언트의 price를 보장하지 않는다. 아래 예시 코드와 같이 A 사용자의 price가 B 사용자의 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원 주문
statefulService1.order("userA", 20000);
// ThreadA: 사용자 A 주문 금액 조회
int price = statefulService1.getPrice();
System.out.println("price = " + price); // 20000원이 나온다. B 사용자로 인해 상태값 변경
// statefulService1의 price와 statefulService2의 price가 동일해지는 문제가 발생한다.
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(statefulService2.getPrice());
}
static class TestConfig {
@Bean
public StatefulService statefulService () {
return new StatefulService();
}
}
}