프로젝트를 만들 때 반복적으로 일어나는 문제들의 구조를 어떻게 풀어나갈 것인지에 대한 방법을 패턴화 한 것으로 설계자들이 올바른 설계를 빨리 만들 수 있도록 도와준다. 디자인 패턴의 카테고리는 그 안에서 또 생성, 구조, 행동 3가지로 구분하고 있다.
생성 패턴에 해당하는 싱글톤 패턴은 공유 리소스에 대한 액세스를 제어해야 되는 상황(데이터베이스, 파일)과 같이 클래스에 있는 인스턴스 수를 제어해야 되는 문제, global한 접근을 허용하는 인스턴스에 대한 안정성(다른 곳에서 인스턴스가 수정되는 문제)을 보장해야 될 때 사용하는 디자인 패턴으로 이 두 문제 중 하나만 해결해야 되는 상황에도 사용되는 유명한 디자인 패턴이다. 싱글턴 객체는 여러가지 기능을 수행하기 때문에 SRP를 위반하는 대신 앞서 말한 2가지 문제를 해결해 줄 수 있는 디자인 패턴이다.
싱글톤의 구현에서의 핵심은 두 가지다.
이 구현은 클래스가 로드될 때만 인스턴스가 생성되기 때문에 1번의 인스턴스 생성이 보장되고 getInstance()에서는 바로 그 인스턴스만을 return하기 때문에 구현은 간단하다. 하지만 이 클래스 내부에 다른 static 메소드가 존재하고 그 메소드가 호출된다면 해당 클래스는 로드가 되면서 인스턴스 역시 생성되고 프로그램이 종료되기 전까지 메모리에 남아있다는 단점이 있다.(static 영역의 메모리는 heap 영역의 메모리와 다르게 GC에서 자동 해제를 해주지 않는다.)
public class EagerSingleton {
private static EagerSingleton instance = new EagerSingleton();
private EagerSingleton() {
}
public static EagerSingleton getInstance() {
return instance;
}
}
다음 싱글톤 방식은 인스턴스의 생성이 getInstance()를 호출할 때 인스턴스가 생성되었는지 확인 후 없다면 새로운 인스턴스 1개를 생성해서 사용자가 인스턴스를 요청한 경우에만 생성을 하기 시작해서 앞서 본 싱클톤의 메모리 낭비가 될 수 있는 early 방식을 개선했지만 두 개 이상의 쓰레드가 if문을 통과한 경우 2개 이상의 인스턴스를 생성하게 되기 때문에 멀티 쓰레드 환경에서는 사용할 수가 없는 구현방식이다.
public class LazyInitializedSingleton {
private static LazyInitializedSingleton instance;
private LazyInitializedSingleton() {}
public static LazyInitializedSingleton getInstance(){
if(Objects.isNull(instance)) {
instance = new LazyInitializedSingleton();
}
return instance;
}
}
다음 방식은 앞서 본 lazy 방식의 싱글톤 구현을 멀티 쓰레드에서 사용할 수 있는 형태로 개선한 형태이다. 주요 특징을 보면 첫번째 if문에서 인스턴스가 이미 있는 경우에는 바로 해당 인스턴스를 return해주고 아닌 경우에는 synchronized를 통해 여러 개의 쓰레드를 차례대로 접근시키게 한 다음 각 쓰레드에서는 instance가 이미 있는지 확인을 거치는 두번째 if문까지 통과한 경우에만 인스턴스를 생성할 수 있도록 구현되었다.
public class DoubleCheckedSingleton {
private static volatile DoubleCheckedSingleton instance = null;
private DoubleCheckedSingleton() {}
public static DoubleCheckedSingleton getInstance() {
if (Objects.isNull(instance)) {
synchronized (DoubleCheckedSingleton.class) {
if (Objects.isNull(instance)) {
instance = new DoubleCheckedSingleton();
}
}
}
return instance;
}
}
Double Checked Locking 방식에서 volatile이라는 키워드가 없어도 제대로 thread safe하게 작동될 거 같지만 자바의 쓰레드가 사용하는 메모리 형태에서는 volatile이 없이 구현한 위의 싱글톤은 여전히 여러 개의 인스턴스를 생성할 위험성이 존재한다.
스레드는 main memory에서 가져온 값을 각 스레드가 가지고 있는 working memory에 옮긴 후 저장된 값을 execution engine에 보내는 과정으로 read하고 이 역순의 과정으로 write하기 때문에 다른 쓰레드의 working memory에서 변경된 값이 main memory에 write하기까지의 지연 시간 사이에 다른 쓰레드가 main memory에 있는 값을 read를 하게 된다면 이 쓰레드는 main memory에 최근에 업데이트 된 값을 가져올 수 없게 되는 문제를 가지고 있다.
싱글톤 구현의 경우 한 스레드에서 인스턴스가 생성되고 해당 스레드의 work memory에는 그 값이 업데이트 되었지만 main memory까지 업데이트 되기 전에 또 다른 쓰레드가 main memory에서 값을 read하게 된다면 인스턴스 참조 변수가 NULL로 read가 되면서 인스턴스가 2개 이상이 생성될 수 있는 문제를 갖게 된다.
그래서 이러한 main memory와의 동기화 작업을 보장할 수 있는 volatile 키워드를 가진 참조 변수를 사용해서 각 스레드가 메인 메모리에서 직접 read하고 write을 할 수 있도록 싱글톤을 구현해야만 한다.
앞에서 본 3가지의 싱글톤 구현 방식의 장점을 모두 살린 구조이다. 구현이 간단하고 lazy loading이 되서 메모리 낭비가 되지 않고 멀티 스레드에서 안전하게 사용이 가능한 형태로 특징은 클래스 내부에 static 클래스 변수를 가진 클래스 구조이다. getInstance()를 사용할 때만 static 클래스의 생성자가 1번의 인스턴스 생성을 하게 되는 구조이다.
public class BillPughSingleton {
private BillPughSingleton(){}
private static class SingletonHelper{
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance(){
return SingletonHelper.INSTANCE;
}
}
앞서 살펴본 Bill Pugh Singleton을 비롯한 Singleton 구현 방식은 Java의 Reflection API에서 제공되는 setAccessible()를 이용해 클래스의 private 생성자 호출이 가능하다는 점에서 취약성을 가지게 되면서 Enum을 사용한 구현 방식이 등장하게 되었다.
import java.lang.reflect.Constructor;
public class PrivateInvoker{
public static void main(String[] args) throws Exception{
Constructor<?> con = Private.class.getDeclaredConstructors()[0];
//private한 생성자에 접근이 가능한 문제점 발생
con.setAccessible(true);
Private p = (Private) con.newInstance();
}
}
class Private{
private Private(){
System.out.println("Hello!");
}
}
Enum은 상수들만 모아놓은 클래스이고 클래스처럼 메소드, 생성자를 모두 가질 수 있지만 고정된 상수들의 집합으로써 컴파일 타임에 모든 값을 알고 있어야 하기 때문에 생성자가 private으로 제한된다. 따라서 인스턴스 생성이 불가능한 싱글톤의 성질을 갖고 있다. Enum Singleton의 구현과 갖고 있는 장점은 다음과 같다.
public enum EnumSingleton {
INSTANCE;
}
구현이 간단하다.
thread safe
Reflection X
setAccessible()이 적용이 안되기 때문에 생성자에 접근할 수가 없어서 중복 생성에 대한 문제가 해결된다.
JVM이 Enum의 Serialization 보장
Enum을 제외한 나머지 싱글톤 객체의 경우 파일에 저장하거나 다른 서버로 보낼 때 사용되는 Serialization(직렬화)를 사용한 후 Deserialization(역직렬화)를 할 때 클래스의 생성자가 private임에도 불구하고 새로운 인스턴스가 생성이 되는 문제가 있다.