본문으로 바로가기

싱글턴 패턴의 다양한 구현 방법을 알아보자.

category Java 2020. 12. 21. 00:25

싱글턴 패턴이란?

싱글턴 패턴은 인스턴스를 오직 1개만 생성하는 디자인 패턴이다.

인스턴스 1개만 생성되는 싱글턴 패턴을 이용하면, 하나의 인스턴스만을 메모리에 등록해서 여러 스레드가 동시에 해당 인스턴스를 공유하여 사용하게끔 할 수 있으므로, 요청이 많은 곳에서 사용하면 성능상 유리한 이점을 가져올 수 있다.

예를들어  만약 스프링이 싱글톤을 지원하지 않는다면,스프링에서 하나의 HTTP를 요청할 때 마다 새로운 스레드가 새로운 객체를 생성하게 되면  웹 애플리케이션의 엄청난 메모리 부하를 가져올 수 있다.

 

단, 싱글턴 패턴을 만들때는 동시성 문제성능을 고려해서 싱글턴 패턴을 설계해야 한다. 아래 6가지 방법을 익혀두자.

1. 가장 기본적인 싱글톤 (Lazy Initialization : 늦은 초기화)

public class BasicSingleton {
    private static BasicSingleton uniqueInstance;
    public static BasicSingleton getInstance() {
        if(uniqueInstance == null) {
            uniqueInstance = new BasicSingleton();
        }
        return uniqueInstance;
    }
}

동시성 문제 : 멀티 스레드 환경에서, if문에 두 개이상의 스레드가 동시에 들어가서 실행한다면 인스턴스를 두 개이상 만들 수 있음.

 

 

2. 간단한 Synchronized 적용 (Lazy Initialization : 늦은 초기화)

public class SynchronizedSingleton {
    private static SynchronizedSingleton uniqueInstance;

    public static synchronized SynchronizedSingleton getInstance() {
        if(uniqueInstance == null){
            uniqueInstance = new SynchronizedSingleton();
        }
        return uniqueInstance;
    }
}

성능 문제 : 싱글톤을 최초로 생성하는 경우(==null 일때)에만 Lock을 걸면 되는데, 이 코드는 싱글톤 인스턴스를 가져올때 마다 Lock을 건다.

Thread-safe 보장됨.

 

 

3. 클래스 로드 시점에 생성 ( Eager-Initialization : 이른 초기화)

public class EagerInitializationSingleton {
    private static EagerInitializationSingleton uniqueInstance = new EagerInitializationSingleton();

    private EagerInitializationSingleton(){

    }

    public static EagerInitializationSingleton getInstance() {
        return uniqueInstance;
    }

}

성능 문제 : Eager-Initialization 로 인한 문제.

-> 클라이언트에서 이 싱글톤을 사용하지 않더라도 이 인스턴스가 로드됨.

Thread-safe 보장됨.

 

 

4. Double - Checking - Locking : 동시성 문제와 성능 문제(일부)를 해결(Lazy Initialization : 늦은 초기화)

public  class DoubleCheckingLockingSingleton {
    private volatile static DoubleCheckingLockingSingleton uniqueInstance;

    private DoubleCheckingLockingSingleton(){

    }

    public static DoubleCheckingLockingSingleton getInstance() {
        if(uniqueInstance == null){
            synchronized (DoubleCheckingLockingSingleton.class) {
                if(uniqueInstance == null) {
                    uniqueInstance = new DoubleCheckingLockingSingleton();
                }
            }
        }
        return uniqueInstance;
    }
}

인스턴스를 얻어오는 메소드 안에 두 개의 if문으로 더블 체킹을 하는 것.

 

또다른 성능문제: 이 과정에서 하나의 if문 안에 들어갔을 때, synchronized로 locking 하는 방식.

여기서 uniqueInstance 변수를 자세히 보면 volatile 키워드가 붙은것을 확인할 수 있다. 이는 캐시 관련 문제를 해결하기 위해 사용하는 키워드인데, volatile을 사용함으로써 또 다른 성능 문제를 가져온다.

volatile : volatile가 붙은 변수는 CPU의 캐시를 거치지 않고 바로 메인 메모리로 read/write됨.

 

 

5. Enum을 사용한 singleton (Eager-Initialization : 이른 초기화)

public enum EnumSingleton {
    uniqueInstance;
}

 

  • 리플렉션을 통해 싱글톤을 깨트리는 공격에 안전
  • 직렬화 보장

 

성능 문제 : Eager-Initialization로 인한 문제와 싱글톤이 Enum 외의 클래스를 상속해야 하는 경우에는 사용할 수 없다.

또한 안드로이드 같이 Context 의존성이 존재하는 환경일 경우 싱글턴 초기화시 Context라는 의존성이 끼어들 가능성이 있다.

 

 

6. LazyHolder 기법 (Lazy Initialization : 늦은 초기화)

최근 가장 많이 사용되는 싱글톤 기법으로, 3.클래스 로드 시점에 생성 방법을 개선한 패턴이다.**

volatilesynchronized 키워드 없이도 동시성 문제를 해결할 수 있음.

public class LazyHolderSingleton {

    private LazyHolderSingleton() {
    }

    public static LazyHolderSingleton getInstance() {
        return LazyHolder.uniqueInstance;
    }

    private static class LazyHolder {
        private static final LazyHolderSingleton uniqueInstance = new LazyHolderSingleton();
    }
}
  • Singleton 클래스에는 LazyHolder 클래스의 변수가 없기 때문에 Singleton 클래스 로딩 시 LazyHolder 클래스를 초기화하지 않음
  • Class를 로딩하고 초기화하는 시점은 thread-safe를 보장
  • holder 안에 선언된 instance가 static이기 때문에 클래스 로딩 시점에 한번만 호출

또한 여러 쓰레드에서 동시에 LazyHodler를 호출해도, JVM이 알아서 하나의 객체만 올려주기 때문에 volatilesynchronized 키워드 없이도 동시성 문제를 해결

 

단점 : 역직렬화 수행시 새로운 객체 생성 , 리플렉션을 이용해 내부 생성자 호출 가능

 


결론

  • LaszHolder : 성능이 중요시 되는 환경
  • Enum : 직렬화, 안정성 중요시 되는 환경