스터디/이펙티브 자바 스터디

[이펙티브 자바] 아이템 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라

제이온 (Jayon) 2022. 4. 17.

effective-java에서 스터디를 진행하고 있습니다.

 

싱글턴

싱글턴의 개념

싱글턴이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다. 싱글턴의 전형적인 예시로는 무상태 객체나 유일한 시스템 컴포넌트를 들 수 있다. 하지만 싱글턴 클래스는 타입을 인터페이스로 정의하고 그것의 구현체로 정의한 것이 아니라면 테스트하기 어렵다는 문제가 있다.

 

싱글턴을 만드는 방법

public static 멤버가 final 필드인 방식

public class Elvis {

    public static final Elvis INSTANCE = new Elvis();

    private Elvis() {
    }

    public void speak() {
        System.out.println("elvis");
    }
}

 

private 생성자는 Elvis 인스턴스를 초기화할 때 딱 한 번만 호출되며, 전체 시스템에서 유일한 인스턴스임을 보장한다. 단, AccessibleObject.setAccessible() 를 사용하여 private 생성자를 호출할 수 있는데, 이러한 리플렉션으로 변조하는 방법은 2번째 객체가 생성될 때 예외를 던져서 막을 수 있다.

 

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

 

정적 팩터리 메서드를 public static로 제공하는 방식

public class Elvis {

    private static final Elvis INSTANCE = new Elvis();

    private Elvis() {
    }

    public static Elvis getInstance() {
        return INSTANCE;
    }

    public void speak() {
        System.out.println("elvis");
    }
}

 

리플렉션을 통한 변조 외에는 해당 방법도 전체 시스템에서 유일한 인스턴스임을 보장한다. 단지 필드를 private으로 바꾸고, 객체 반환을 정적 팩터리 메서드를 사용해 주고 있다.

 

  • 장점
    • API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다.
      • 가령, 정적 팩터리 메서드가 호출하는 스레드 별로 다른 인스턴스를 넘겨주도록 할 수 있다.
    • 원한다면 제네릭 싱글턴 팩터리 메서드로 변경할 수 있다.
    • 정적 팩터리의 메서드 참조를 공급자로 사용할 수 있다.
      • 가령, Elvis::getInstance 대신에 Supplier<Elvis> 로 사용할 수 있다.

 

위의 장점을 활용할 일이 없다면, 첫 번째 방식을 사용하는 편이 좋다.

 

열거 타입을 사용하는 방식

public enum Elvis {

    INSTANCE;

    public void speak() {
        System.out.println("elvis");
    }
}

 

가장 바람직한 방식은 열거 타입을 사용하는 것이다. 위 두 방식에 비해 리플렉션 공격에도 안전하고 코드도 깔끔하다. 또한, 아래에도 후술하겠지만 위 두 방식은 직렬화할 때 추가 코드를 넣어주어야 한다는 단점이 있다.

 

단, 생성하려는 싱글턴이 인터페이스를 상속 받는 것은 가능하지만 클래스를 상속 받을 수 없다는 것은 주의해야 한다.

 

싱글턴 클래스를 직렬화할 때 주의할 점

위 방식 중 첫 번째 또는 두 번째로 만든 싱글턴 클래스를 직렬화하려면 단순히 Serializable을 구현하는 것 외에, 모든 인스턴스 필드를 transient로 선언하고 readResolve() 메서드를 재정의하여 제공해야 한다.

 

private Object readResolve throws ObjectStreamException {
    return INSTANCE;
}

 

출처

댓글

추천 글