싱글턴은 객체를 하나만 만들 수 있는 클래스다. 싱글턴은 보통 유일할 수 밖에 없는 시스템 컴포넌트를 나타낸다. 창 관리자나 파일 시스템 같은 것들이 그 예다. 그런데 클래스를 싱글턴으로 만들면 클라이언트를 테스트하기가 어려워질 수가 있다. 싱글턴이 어떤 인터페이스를 구현하는 것이 아니면 가짜 구현으로 대체할 수 없기 때문이다.
JDK 1.5 이전에는 싱글턴을 구현하는 방법이 두 가지였다. 두 방법 다 생성자는 private로 선언하고, 싱글턴 객체는정적 멤버를 통해 이용한다. 첫 번째 방법의 경우, 정적 멤버는 final로 선언한다.
1 2 3 4 | public class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elvis() {}; } |
Elvis 객체는 클래스가 초기화 되고 나면 하나만 존재하게 된다. 클라이언트가 이 상태를 변경할 방법은 없지만 AccessibleObject.setAccessible 메서드의 도움을 받아 권한을 획득한 클라이언트는 리플렉션 기능을 통해 private 생성자를 호출할 수 있다는 것이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class Private { private Private() { System.out.println("Hello"); } } public class Test { public static void main(String[] args) throws Exception { Constructor<?> con = Private.class.getDeclaredConstructors()[0]; con.setAccessible(true); Private p = (Private) con.newInstance(); } } |
이런 종류의 공격을 방어하고 싶다면, 두 번째 객체를 생성하라는 요청을 받으면 예외를 던지도록 생성자를 고쳐야 한다. 두 번째 방법은 public 으로 선언된 정적 팩터리 메서드를 이용하라는 것이다.
1 2 3 4 5 | class Elvis { public static final Elvis INSTANCE = new Elvis(); private Elvis() { // Exception을 throw }; public static Elvis getInstance()(return INSTANCE;) } |
Elvis.getInstance는 항상 같은 객체에 대한 참조를 반환한다. 이것 외의 Elvis 객체는 만들 수 없다. public 필드를 사용하면 클래스가 싱글턴인지 선언만 보면 금방 알 수 있어서 좋다. 하지만 이 방법의 성능이 더 좋을 것이라는 기대는 접는 것이 좋다. 요즘 JVM은 정적 팩터리 메서드 호출을 거의 항상 인라인 처리해 버리기 때문이다. 팩터리 메서드를 사용하는 방법의 첫 번째 장점은 API를 변경하지 안혹도 싱글턴 패턴을 포기할 수 있다는 것이다. 가령, 스레드마다 별도의 객체를 반환하도록 팩토리 메서드를 수정하는 것도 간단하다. 두 번째 장점은 제내릭 타입을 수용하기 쉽다는 것이다. 그런데 이런 장점이 필요 없는 경우도 많다. 그럴 때는 public 필드를 사용하는 쪽이 더 간단하다.
앞서 설명한 방법들로 구현한 싱글턴 클래스를 직렬화 가능 클래스로 만들려면 클래스 선언에 implements Serializable을 추가하는 것으로는 부족하다. 싱글턴 특성을 유지하려면 모든 필드를 transient로 선언하고 readResolve 메서드를 추가해야 한다. 그렇지 않으면 serialize된 객체가 역직렬화될 때마다 새로운 객체가 생기게 된다. 이 문제를 막으려면 Elvis 클래스에 아래의 readResolve 메서드를 추가해야 한다.
JDK 1.5 부터는 싱글턴을 구현할 때 새로운 방법을 사용할 수 있다. 원소가 하나뿐인 enum 자료형을 정의하는 것이다.
1 2 3 4 5 6 | //Enum 싱글턴 - 이렇게 하는 것이 더 좋다. public enum Elvis { INSTANCE; public void leaveTheBuilding() { .. }; } |
이 접근법은 public 필드를 사용하는 것과 비슷하다. 차이는 간결하고, 직렬화가 자동으로 처리된다는 것이다. 직렬화가 아무리 복잡하게 이루어져도, 여러 객체가 생기지 않고, 리플렉션을 통한 공격에도 안전하다. 아직 널리 사용되는 접근법은 아니지만, 원소가 하나뿐인 enum 자료형이야말로 싱글턴을 구현하는 가장 좋은 방법 이다.
'Effective Java > 1장 객체의 생성과 삭제' 카테고리의 다른 글
Effective Java #6 유효기간이 지난 객체 참조는 폐기하라 (0) | 2018.10.20 |
---|---|
Effective Java #5 불필요한 객체는 만들지 마라 (0) | 2018.10.20 |
Effective Java #4 객체 생성을 막을 때는 private 생성자를 사용하라 (0) | 2018.10.18 |
Effective Java #2 생성자 인자가 많을 때는 빌더 패턴 적용을 고려하라 (0) | 2018.10.17 |
Effective Java #1 생성자 대신 정적 팩토리 메서드를 사용할 수 없는지 생각해 보라 (0) | 2018.10.16 |