[ 싱글톤(Singleton)이란 ]
싱글톤은 인스턴스가 오직 하나만 생성 될 수 있는 클래스를 말한다. 싱글톤은 무상태로 유지되야 하며 우리가 가장 많이 접할 수 있는 싱글톤으로는 스프링 컨테이너에서 빈을 싱글톤으로 관리하는게 있다.
[ 싱글톤을 만드는 방식 ]
싱글톤을 만드는 방식은 보통 두가지 중 하나이다.
- public static final로 싱글톤 객체를 만드는 방식
- public static 메서드로 싱글톤 객체를 반환하는 방식
두 방식 모두 private 생성자를 통해 외부에서 인스턴스 생성이 불가능하게 하고, static으로 싱글톤 객체가 프로그램 로딩 시 미리 만들어지게 한다.
[ public static final로 싱글톤 객체 생성 ]
public class Elvis {
public static final Elvis INSTANCE = new Elvis(); // 프로그램 로딩 시 생성
private Elvis(){} // 생성자는 private으로 막아서 외부에서 호출 불가
}
private 생성자는 처음에 INSTANCE가 만들어질때 딱 한번 호출된다.
책에는 private 생성자를 강제로 호출할 수 있는 방법이 나온다. 바로 리플렉션을 사용하는 것이다.
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
// getDeclaredConstructor 메서드를 통해 Elvis에 선언된 private 생성자를 가져올 수 있다.
Constructor<Elvis> declaredConstructor = Elvis.class.getDeclaredConstructor();
// setAccessible(true)를 설정하면 실제 private 생성자를 사용할 수 있다.
declaredConstructor.setAccessible(true);
// private 생성자임에도 외부에서 Elvis 인스턴스를 만들 수 있다.
Elvis elvis = declaredConstructor.newInstance();
}
위의 코드처럼 싱글톤을 위해서 private으로 생성자를 막았지만 리플렉션을 사용해서 인스턴스를 외부에서 생성할 수 있다. 완전한 싱글톤을 구현하고 싶다면 처음 프로그램 실행 시 Elvis 싱글톤 객체를 만들기 위해 private 생성자가 사용된 후,두번째부터는 호출 되지 않게 만들어야 한다. 아래의 코드를 참고하자.
public class Elvis{
public static final Elvis INSTANCE = new Elvis();
private static boolean created = false; // 생성자가 처음 호출되는지 확인하는 플래그
private Elvis() {
// 두번째 생성부터는 created가 true이기 때문에 예외가 발생한다.
if (created) {
throw new UnsupportedOperationException("can't be created by constructor.");
}
created = true;
}
[ public static 메서드로 private static 인스턴스 반환 ]
public class Elvis
{
// 이제 싱글톤 객체를 private으로 외부 호출 불가능하게 막아놓음
private static final Elvis INSTANCE = new Elvis();
private Elvis(){}
// INSTANCE를 반환하는 메서드
public static Elvis getInstance(){ return INSTANCE };
}
public static 메서드를 사용해서 싱글톤 객체를 반환하면 몇가지 이점을 얻을 수 있다.
1. API를 변경하지 않고 싱글톤이 아니게 변경 가능하다
싱글톤 객체를 직접 외부에서 참조하는 것이 아닌 메서드를 통해서 참조한다면 내부적으로 반환 값을 변경할 수 있다.
이 방식을 사용하면 클라이언트에 따라 싱글톤을 줄 수도 있고 일반 객체를 제공해 줄 수도 있는 유연함이 생긴다.
public class Elvis
{
private static final Elvis INSTANCE = new Elvis();
private Elvis(){}
// INSTANCE 말고 새로 객체 만들어서 반환할 수도 있다!
public static Elvis getInstance(){ return new Elvis() };
}
2. 정적 팩토리를 제네릭 싱글톤 팩토리로 만들 수 있다
public static 메서드를 사용하면 제네릭 타입으로 유연한 싱글톤 객체를 만들 수 있다.
public class MetaElvis<T> {
// 싱글톤 객체로 미리 생성, 타입은 Object
private static final MetaElvis<Object> INSTANCE = new MetaElvis<>();
private MetaElvis() { }
// MetaElvis가 어떤 타입이 될지는 아직 모른다.
public static <E> MetaElvis<E> getInstance() { return (MetaElvis<E>) INSTANCE; }
public void say(T t) {
System.out.println(t);
}
public static void main(String[] args) {
// MetaElvis의 타입 T가 String으로 결정나면서 MetaElvis<E> 타입의 INSTANCE로 String으로 결정
MetaElvis<String> elvis1 = MetaElvis.getInstance();
MetaElvis<Integer> elvis2 = MetaElvis.getInstance();
System.out.println(elvis1);
System.out.println(elvis2);
elvis1.say("hello");
elvis2.say(100);
}
}
3. 정적 팩토리의 메서드 참조를 공급자로 사용할 수 있다
getInstance()메서드는 static이기 때문에 메서드 참조를 사용해서 표현할 수 있다.
public static void main(String[] args) {
// getInstance는 static 메서드이기 때문에 메서드 참조 가능
Supplier<Elvis> supplier = Elvis::getInstance;
Elvis elvis = supplier.get();
elvis.sing();
}
[ 싱글톤 클래스 직렬화(Serialization) ]
싱글톤 클래스를 역직렬화 하는 과정에서 새로운 객체가 하나 만들어진다. 이를 막기 위해서는 readResolve라는 메서드를 싱글톤 클래스 내에 선언한 후, 미리 만들어놓은 싱글톤 객체를 반환하도록 오버라이딩 비슷한걸 해줘야 한다.(비슷하다고 한 이유는 실제로 상위 클래스의 함수를 오버라이딩 한 건 아니기 때문이다.)
[ 열거형(enum) 사용한 싱글톤 구현 ]
위에서 제시되었던 싱글톤을 깨트리는 문제 2가지(리플랙션, 역직렬화)를 모두 해결해주는 방법이 열거형으로 싱글톤을 만드는 것이다. 열거형은 내부적으로 리플랙션을 불가능하게 막아놨고, 역직렬화도 정상적으로 작동하도록 구현해놓았다.
원소가 하나인 열거형을 통해 완전한 싱글톤을 만들어낼 수 있다.