애플리케이션이 시작될 때, 어떤 클래스가 최초 한번만 메모리를 할당하고 해당 메모리에 인스턴스를 만들어 사용하는 패턴.
싱글톤 패턴은 클래스의 인스턴스를 하나만 만들어 사용하는 패턴을 말한다. 주로 데이터 커넥션 풀이나 스레드 풀, 캐시, 로그 기록 객체와 같이 공통된 객체를 여러개 생성해야하는 상황에 사용한다.
public class SingleTone{
private static SingleTone instance;
private SingleTone(){}
public SingleTone getInstance(){
if (instance == null){
return new SingleTone();
}
return instance;
}
}
자바에서는 private static으로 인스턴스를 선언하고, getInstance()
를 정의하여 클래스의 객체가 생성되어 있다면 생성된 객체를 반환하고 그렇지 않다면 새로 생성하는 방식으로 구현할 수 있다.
이때, 외부에서 new
를 사용하여 객체를 생성할 수 없도록 생성자를 private으로 만들어야 한다.
멀티 스레드 환경에서는 위와 같은 방식으로 구현한다면 다음과 같은 문제가 생길 수 있다.
바로 위 예시 코드에서 보면 인스턴스를 반환하기전에 인스턴스가 생성되어 있는지의 여부를 확인하는 부분이 있다. 만약 멀티스레스 환경에서 instance가 아직 생성되지 않았을 때 getInstance()
를 실행하게 된다면 인스턴스가 없다고 판단되어 여러개의 인스턴스가 생기는 문제가 발생한다.
public class SingleTone{
private static SingleTone instance;
private SingleTone(){}
public static synchronized SingleTone getInstance(){
if (instance == null){
instance = new SingleTone();
}
return instance;
}
}
위와 같이 synchronized
동기화를 사용하여 스레드를 안전하게 만들어 해결할 수 있다. 하지만 synchronized
는 성능 저하를 일으킬 수 있어 권장 되지 않는다.
public class SingleTone{
private static SingleTone instance;
private SingleTone(){}
public static SingleTone getInstance(){
if (instance == null){
synchronized (SingleTone.class) {
if (instance == null){
instance = new SingleTone();
}
}
}
return instance;
}
}
instance
가 null인 경우에만 동기화를 하여 성능을 개선시켰다. 처음 인스턴스 생성 이후에는 동기화를 하지 않기 때문에 성능 저하를 완화시킬수 있다.
하지만 개발자가 직접 동기화를 제어하는 경우에는 프로그램 구조가 복잡해지거나 비용문제가 발생할 수 있다. 따라서 다음 3번의 방식이 일반적으로 사용된다.
public class SingleTone{
private static SingTone instance = new SingleTone();
private SingleTone(){};
public static SingleTone getInstance(){
return instance;
}
}
클래스가 로드될 때 초기에 인스턴스를 생성한다면 멀티 스레드 환경에서도 하나의 인스턴스를 공유할 수 있다.
싱글톤 패턴 구현 시에 주의해야할 점은 싱글톤 객체는 반드시 무상태(stateless)
여야 한다는 것이다. 싱글톤 객체가 stateful
할 경우 데이터 일관성이 깨질 수 있다.
public class Order{
private static Order instance = new Order();
private int price;
public void order(int price){
this.price= price;
}
public int getPrice(){
return price;
}
private Order(){}
public static Order getInstance(){return instance;}
}
public class Main{
public static void main(String args[]){
Order thread1 = Order.getInstance();
Order thread2 = Order.getInstance();
thread1.order(1000);
thread2.order(2000);
System.out.println(thread1.getPrice()); /// 2000
}
}
멀티 스레드 환경이라고 가정해서봤을 때, 여러 스레드에서 동시에 price
필드에 접근하므로써 데이터의 일관성이 깨지게 되었다. thread1의 유저는 1000원을 결제하였지만 주문 가격을 조회하니 2000원으로 나오는 것이다. (실제 서비스라고 생각하면 아찔한 상황이다.)
따라서 싱글톤 패턴을 사용할 때에는 무상태로 설계하여 이러한 문제를 예방해야한다.
위 코드는 다음과 같이 수정할 수 있다.
public class Order{
private static Order instance = new Order();
private int price;
public void order(int price){
// .. 내부로직..
return price;
}
private Order(){}
public static Order getInstance(){return instance;}
}
public class Main{
public static void main(String args[]){
Order thread1 = Order.getInstance();
Order thread2 = Order.getInstance();
int price1 = thread1.order(1000);
int price2 = thread2.order(2000);
System.out.println(price1); /// 1000
}
}