[Effective Java] Item3. private 생성자나 열거 타입으로 싱글턴임을 보증하라

2022년 02월 28일

TOC

이 글은 Effective Java 3/E의 내용을 요약한 글입니다. 자세한 내용은 책을 참고하시기 바랍니다.

싱글턴은 하나의 프로그램 내에 하나의 인스턴스만을 가질 수 있도록 하는 전략 패턴입니다.

자세한 내용은 싱글턴 패턴이란? 을 통해 확인하실 수 있습니다

싱글턴을 사용하는 대표적인 예로는 stateless 객체나 설계적으로 프로그램 내에 하나만 존재해야하는 시스템 컴포넌트 들이 있습니다. 하지만 이러한 싱글턴 패턴은 해당 인스턴스를 사용하는 클라이언트를 테스트하기 어려워질 수 있다는 단점이 존재합니다.

싱글턴의 경우 interface를 통해 구현하여 만든 것이 아니라면 싱글턴 인스턴스를 mock 구현으로 대체할 수 없기 때문입니다.

싱글턴 패턴을 만드는 방식

1. public static final 필드 방식의 싱글턴

public class Singleton {
    public static final Singleton INSTANCE = new Singleton();
    private Singleton() {
      ...
    }
    ...
}

다음 방법은 생성자를 private 접근 제어자로 설정하고, 유일한 인스턴스에 접근할 수 있는 수단으로 public static으로 설정한 INSTANCE멤버 변수를 생성해둬야합니다. private으로 설정된 생성자는 INSTANCE를 처음 초기화할 때만 한번 호출하며 프로그램 내에 인스턴스가 하나만 존재하도록 하는 방법입니다.

해당 방법의 장점은 해당 클래스가 싱글턴임을 API에서 명백하게 드러낼 수 있고 간결하다는 점이 있습니다.

2. 정적 팩터리 방식의 싱글턴

해당 방법은 생성자를 private 접근 제어자로 설정하고, 유일한 인스턴스에 접근할 수 있는 수단으로 private static으로 설정한 INSTANCE멤버 변수를 생성해둬야합니다. 1번 방식과 다른점이라면 INSTANCE 멤버 변수의 접근제어자가 public이 아닌 private이라는 점입니다.

그러면 해당 INSTANCE에는 어떻게 접근을 할까요?? 해당 방법에서는 정적 팩터리 메서드를 생성하여 해당 메서드를 통해 접근을 합니다.

예시 코드는 다음과 같습니다.

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    private Singleton() {}
    public static final Singleton getInstance() { return INSTANCE; }
    ...
}

getInstance()메서드를 통해 사용자들은 INSTANCE에 접근을 할 수 있으며 만들어진 인스턴스를 가져오는 식으로 운영되기에 새로운 인스턴스가 생성될 일이 없습니다.

해당 방법의 장점은 개발을 하며 마음이 바뀔 경우 API를 바꾸지 않고도 싱글턴이 아니게 변경을 할 수 있으며, 원한다면 정적 팩터리 메서드를 제너릭 싱글턴 팩터리로 만들 수 있다는 점이 있습니다. 또한 정적 팩터리 메서드를 공급자(Supplier)로 활용할 수 있다는 점이 있습니다.

만약 해당 장점들이 필요하지 않다면 1번(public필드)방식이 좋습니다.

1, 2번 방식의 주의할 점

  • 두 방법 모두 권한이 있는 클라이언트가 리플렉션 API인 AccessibleObject.setAccessible을 통해 private 생성자를 호출하게 된다면 두개 이상의 인스턴스가 생성될 수 있는데 이러한 문제는 생성자 내에서 두번째 객체를 생성하려고 할 때 exception을 발생시키도록 설정함으로써 예방할 수 있습니다.
  • 1, 2번 방식으로 싱글턴 클래스를 직렬화 하려면 단순히 Serializable을 구현한다고 선언하는 것 뿐만 아니랏 새로운 작업이 필요합니다.

    • 모든 인스턴스 필드를 transient(일시적)이라고 선언을 하고 readResolve메서드를 통해 인스턴스를 반환해야합니다. (그렇지 않을 경우 새로운 인스턴스가 생성됩니다.)
    • 예시코드는 아래와 같습니다.
public class Singleton implements Serializable {
    public static final Singleton INSTANCE = new Singleton();
    private Singleton() {}

    private Singleton readResolve() {
        // 진짜 Singleton 인스턴스를 반환 후 가짜 인스턴스는 가비지 컬렉터로~
        return INSTANCE;
    }
}

transient란?
transient는 Serialize하는 과정에 제외하고 싶은 경우 선언하는 키워드입니다.

3. 열거타입(Enum)타입의 싱글턴

public enum Singleton {
    INSTANCE;

    public void method() {}
    ...
}

해당 방법은 1번 public 필드 방식과 유사하지만 더 간결하고, 추가적인 노력 없이 직렬화를 할 수 있습니다. 또한 아주 복잡한 직렬화 상황이나 리플렉션 공격에도 다른 인스턴스가 생성되는 것을 완벽하게 막아줍니다.

열거타입의 싱글턴은 원소가 하나뿐이라 부자연스러워 보일 수 있지만 대부분의 상황에서는 가장 좋은 방법입니다. 단, 만들고자하는 싱글턴이 Enum이외의 클래스를 상속해야한다면 해당 방법은 사용할 수 없습니다.

Buy me a coffeeBuy me a coffee
Written by

@Seongwon

기술공유를 통해 새로운 가치 창조을 추구하는 백엔드 개발자 오성원입니다.
©SeongwonOh