private 생성자나 열거 타입으로 싱글턴임을 보증하라!
이 글을 읽는 사람이라면 싱글턴 클래스에 대해서 알거라고 믿...지 말고 그래도 한번 짚고 넘어가자.
싱글턴 (Singleton)이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다.
싱글턴을 만드는 방식은 일반적으로 두가지이다. 각각의 예시를 보자.
public class LeaveMeAlone {
public static final LeaveMeAlone PLEASE = new LeaveMeAlone();
private LeaveMeAlone() {}
public void dontBortherMe() {
System.out.println("Hew...");
}
}
일반적인 방법이다. private 생성자는 초기화 과정에 딱 한번 호출되고, public이나 protected 생성자가 없으니까 자연스럽게 LeaveMeAlone 클래스는 하나만 있는 것이 보장된다.
그러나 클라이언트에게 권한을 부여하면, 리플렉션 API를 통해 private 생성자를 호출할 수 있다.
import java.lang.reflect.Constructor;
public class ILikeYou {
public static void main(String[] args) {
try {
Class hello = LeaveMeAlone.class;
Constructor iAlwaysWatchingYou = hello.getDeclaredConstructor();
iAlwaysWatchingYou.setAccessible(true);
Object comeToMe = iAlwaysWatchingYou.newInstance();
LeaveMeAlone hahaha = (LeaveMeAlone)comeToMe;
hahaha.dontBortherMe();
} catch (Exception e) {
e.printStackTrace();
}
}
}
클래스와 인자들의 이름이 무섭다... 우리 불쌍한 싱글톤 친구를 저 스토커로 부터 어떻게든 분리시켜야 한다... 그래서 이러한 공격을 방어하기 위해 두번째 객체가 생성될때 예외를 던지도록 하면 된다.
public class LeaveMeAlone {
private static final LeaveMeAlone PLEASE = new LeaveMeAlone();
private LeaveMeAlone() {
if(PLEASE != null) {
throw new IllegalStateException("What Are You Doing???");
}
}
public static LeaveMeAlone getInstance() { return PLEASE; }
public void dontBortherMe() {
System.out.println("Hew...");
}
}
어차피 처음 생성 이후에는 PLEASE라는 싱글톤 객체는 자연스럽게 NULL이 아닐테니, 널 체크만 해주면 쉽게 해결할 수 있다. 휴... 이것으로 우리 싱글톤 친구를 무사히 리플렉션 스토커로 부터 분리했다.
생성자를 통한 방법이 있으면, 자연스럽게 정적 팩토리 메소드를 활용한 방법도 있을 것이다. 그러니 살포시 만들어보자.
public class LeaveMeAlone {
private static final LeaveMeAlone PLEASE = new LeaveMeAlone();
private LeaveMeAlone() {}
public static LeaveMeAlone getInstance() { return PLEASE; }
public void dontBortherMe() {}
}
미리 생성자를 호출하는 것은 동일하지만, 이번엔 PLEASE 객체를 private로 돌려버리고, 정적 팩토리 메소드인 getInstance() (어떠한 의미를 갖고 있는지는 item1을 참고하자!)을 사용해 리턴한다.
이 친구도 리플렉션이 엉겨붙으면 답이 없긴 하다.
각각의 방법엔 장점과 단점이 있다. 처음 설명한 public 필드 방식은 API를 보기만 해도 싱글턴임을 확인할 수 있고, 코드가 짧다는 장점이 있다.
다음으로 설명한 정적 팩토리 방식은 싱글턴이 아니게 수정해야 할 필요가 있을때 큰 문제 없이 수정할 수 있다는 장점이 있고, 희망할 경우 정적 팩토리를 제네릭 싱글턴 팩토리로 만들 수 있다는 장점과, 정적 팩토리의 메소드 참조를 공급자로 사용할 수 있다는 점이다.
오... 무슨 말인지 대충 보면 감이 안 올 것이다. 하나씩 뜯어먹어보자.
"정적 팩토리를 제네릭 싱글턴 팩토리로 만들 수 있다" 라는 말부터 보자.
싱글턴은 싱글턴인데, 제네릭이 적용되어 요청 타입 변수에 맞게 객체의 타입을 바꿔준다. 도저히 내 머리에선 예시를 뽑아낼 수 없으니... (아직 자바가 어렵다 ㅠ) JDK를 뜯어 예시를 찾아보았다.
Collections.reverseOrder()
를 보자.
public static <T> Comparator<T> reverseOrder() {
return (Comparator<T>) ReverseComparator.REVERSE_ORDER;
}
private static class ReverseComparator
implements Comparator<Comparable<Object>>, Serializable {
private static final long serialVersionUID = 7207038068494060240L;
static final ReverseComparator REVERSE_ORDER
= new ReverseComparator();
public int compare(Comparable<Object> c1, Comparable<Object> c2) {
return c2.compareTo(c1);
}
private Object readResolve() { return Collections.reverseOrder(); }
@Override
public Comparator<Comparable<Object>> reversed() {
return Comparator.naturalOrder();
}
}
보면 싱글톤 객체인 ReverseComparator가 있고, 이것을 reverseOrder()가 반환하는 역할을 한다. 그런데 ReverseComparator는 Comparable한 Object라면 모두 받고 있기 때문에 제네릭의 이점을 활용할 수 있다. 즉, 싱글톤 객체를 제네릭스럽게 사용할 수 있다는 말이다.
"정적 팩토리의 메소드 참조를 공급자로 사용할 수 있다" 라는 말은 무슨 소리일까?
결론만 말하면 함수형 프로그래밍의 내용이다. 어지간하면 공부해서 적고 싶었지만, 아직은 내용을 봐도 잘 모르겠으니 다음을 기약하자...
일단 장점은 다 본 것 같고,만약 직렬화 시킬일이 있다면 어떨까? 단순히 Serializable을 선언하는데서 그치기엔 문제가 조금 복잡하다.
역직렬화 하는 과정에서 받아오는 객체는 싱글톤으로 정의한 그 객체가 아닌, readObject를 통해 변경한 새로운 객체 (해시코드 값은 같겠지만, 같은 객체는 아니다.)이기 때문에, 싱글톤이 깨지는 문제가 있다. 이것을 해결하기 위해,
private Object readResolve() { return PLEASE; }
요런식의 readResolve()를 생성해 무조건 해당 객체를 반환하도록 변경해줘야 한다.
일반적으로 싱글턴을 만드는 방식은 두가지라고 했지만, 하나의 방법이 더 있다. 바로 원소가 하나인 열거 타입을 선언하는 것이다.
public enum LeaveMeAlone { PLEASE; public void dontBortherMe() {} }
public과 비슷하지만, 더 짧고 추가 노력 없이 직렬화할 수 있고, 리플렉션 상황에서도 인스턴스가 더 생성되지 않는다.
물론 부자연스럽게 보일 수 있지만 대부분의 상황에서는 가장 좋은 방법이다. 그러나 만들려는 싱글턴이 Enum 이외의 클래스를 상속해야 하는 상황이라면 불가능하다.