Post

Item 3 - private 생성자나 열거타입으로 싱글턴임을 보증하라

싱글턴(Singleton)이란?

  • 인스턴스를 오직 하나만 생성할 수 있는 클래스
  • 무상태 객체, 설계상 유일해야 하는 시스템 컴포넌트, DBCP(DataBase Connection Pool) 등
  • 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워진다.
    • 싱글턴 인스턴스를 가짜(mock) 구현으로 대체할 수 없기 때문이다.

싱글턴을 만드는 방식

(1) private 생성자 + public static final 필드

1
2
3
4
public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
}
  • private 생성자는 public static final 필드인 Elvis.INSTANCE가 초기화될 때 한번만 호출된다.
  • public이나 protected 생성자가 없으므로 Elvis 클래스가 초기화될 때 만들어진 인스턴스가 전체 시스템에서 하나 뿐임을 보장한다.

장점 👍

  • 해당 클래스가 싱글턴임이 API에 명백히 드러난다.
  • 간결하다.

단, 리플렉션 API를 사용하면 private 생성자를 호출할 수 있어 새로운 인스턴스를 생성할 수 있다.

이러한 공격을 방어하려면 생성자를 수정하여 두 번째 객체를 생성하려 할 때 예외를 던지면 된다.

1
2
3
4
5
6
7
8
public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() {
        if (INSANCE != null) {
            throw new RunTimeException("생성자 호출 불가!");
        }
    }
}

(2) private 생성자 + 정적 팩터리 메서드

1
2
3
4
5
public class Elvis {
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    public static Elvis getInstance() { return INSTANCE; }
}
  • Elvis.getInstance는 항상 같은 객체 참조를 반환하므로 제 2의 인스턴스를 만들지 않는다.
  • 이 방식도 리플렉션 API에 의해 예외적인 상황이 발생할 수는 있다.

장점 👍

  • 정적 팩터리 메서드만 수정하면 언제든 싱글턴이 아니게 변경할 수 있다.
  • 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다. (타입에 대한 유연한 대처)
  • 정적 팩터리의 메서드 참조를 공급자(Supplier)로 만들 수 있다.
    • ex. Elvis::getInstance → Supplier

두 방식의 유의점 🤔

1
2
3
4
5
// 싱글턴임을 보장해주는 readResolve 메서드
private Object readResolve() {
    // '진짜' Elvis를 반환하고, 가짜 Elvis는 가비지 컬렉터에 맡긴다.
    return INSTANCE;
}
  • (1)번과 (2)번 방식으로 만들어진 싱글턴 클래스를 직렬화하려면 Serializable을 구현한다고 선언하는 것만으로는 부족하다.
  • 모든 인스턴스 필드에 transient를 선언하고 readResolve 메서드를 제공해야만, 역직렬화시에 새로운 인스턴스가 만들어지는 것을 방지할 수 있다.

(3) 열거 타입 방식의 싱글턴

1
2
3
public enum Elvis {
    INSTANCE;
}
  • 가장 간결한 방법이며 직렬화와 리플렉션에도 안전하다.
  • 대부분의 상황에선 원소가 하나뿐인 열거타입이 싱글턴을 만드는 가장 좋은 방법이다. 👍

참고 🕶️

함수형 인터페이스

https://blogs.oracle.com/javamagazine/post/understanding-java-method-invocation-with-invokedynamic

https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/LambdaMetafactory.html

객체 직렬화 스펙

https://docs.oracle.com/javase/8/docs/platform/serialization/spec/serialTOC.html

This post is licensed under CC BY 4.0 by the author.