클래스의 instance가 1개만 생성되는 것을 보장하는 디자인 패턴이다.
public class SingletonService {
private static final SingletonService instance = new SingletonService();
public static SingletonService getInstance() {
return instance;
}
private SingletonService() {
}
public void logic() {
System.out.println("싱글톤 객체 로직 호출");
}
}
다음과 같은 방식으로 정적 필드에 instance 1개를 넣어놓고 instance를 사용하기 위해서는 클라이언트에서 getInstance()를 호출하도록 규명하고있다. 또한, 생성자의 제어자를 private으로 두어 생성자로 instance를 생성하는 것을 막는다.
싱글톤 패턴을 사용하지 않는 경우 client가 instance를 요청할 때마다 새로운 instance를 생성하고 삭제한다. 이러면 자원이 너무 많이 든다.
이렇게 instance를 공유자원으로 두는 싱글톤 패턴에도 문제점이 있다.
객체를 하나만 생성해서 공유하는 싱글톤 방식은 여러 client가 하나의 instance를 공유하기 때문에 stateful하게 설계해서는 안 된다.
즉, 무상태로 설계해야한다. 무상태로 설계한다는 것의 의미는 다음과 같다.
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를 저장하는 service이다.
class StatefulServiceTest {
@Test
void statefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
//ThreadA: 10000원 주문
statefulService1.order("userA", 10000);
//ThreadB: 20000원 주문
statefulService2.order("userA", 20000);
//ThreadA: 10000원 주문
int price = statefulService1.getPrice();
System.out.println("price = " + price);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
이를 Bean으로 이용하는 컨테이너를 사용해서 A사용자가 10000원 B사용자가 20000원을 주문한 상황이다. B사용자가 주문을 하면은 등록된 bean의 price 필드가 갱신되므로 뒤늦게 A사용자가 가격을 조회하려고 해도 이미 이 값이 사라져있으므로 race condition이 발생한 것이다.
이와 같은 문제를 방지하기 위해서 싱글톤 패턴에는 client가 변경 가능한 필드를 두어서는 안 된다.