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

뚝딱이·2024년 1월 7일
0

이펙티브 자바

목록 보기
4/55
post-thumbnail

싱글턴이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다.

클래스를 싱글턴으로 만들면 싱글턴 인스턴스를 mock 구현으로 개체할 수 없기 때문에 이를 사용하는 클라이언트를 테스트하기 어려워질 수 있다.

싱글턴을 만드는 방식에 대해 알아보자.

싱글턴

두 방식 모두 생성자는 private으로 감춰둔다.

public static final 필드

public class Elvis {
    public static final Elvis INSTANCE = new Elvis();

    private Elvis() { }

    public void leaveTheBuilding() {
        System.out.println("Whoa baby, I'm outta here!");
    }

    // This code would normally appear outside the class!
    public static void main(String[] args) {
        Elvis elvis = Elvis.INSTANCE;
        elvis.leaveTheBuilding();
    }
}

Elvis 클래스의 생성자는 private으로 선언 되었으므로 클래스 내에서만 사용할 수 있다. 따라서 처음에 public static final 필드인 INSTANCE가 초기화될 때 한번만 호출된다. 따라서 Elvis 인스턴스는 시스템 전체에서 INSTANCE 단하나 뿐이다.

예외는 단 한 가지, 권한이 있는 클라이언트는 리플렉션 API인 AccessibleObject.setAccessible을 사용해 private 생성자를 호출할 수 있다. 이를 대비하기 위해 생성자에서 두 번째 객체가 생성될 때 예외를 던지면 된다.

리플렉션 API : 타입, 변수명, 파라미터 타입, 수 등등 아주 상세한 클래스 명세정보를 다룰 수 있는 기술

장점

  • 해당 클래스가 싱글턴임이 API에 명백히드러난다.
    public static 필드가 final 이므로 절대로 다른 객체를 참조할 수 없다.
  • 간결함

public static 메서드

private 필드로 인스턴스를 생성해놓고 public static 메서드를 통해 인스턴스를 가져다 쓰는 방식이다.

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

    public void leaveTheBuilding() {
        System.out.println("Whoa baby, I'm outta here!");
    }

    // This code would normally appear outside the class!
    public static void main(String[] args) {
        Elvis elvis = Elvis.getInstance();
        elvis.leaveTheBuilding();
    }
}

getInstance를 사용해 인스턴스를 가져오고 있는데, getInstance를 호출하더라도 새로운 인스턴스가 생성되는 것이 아닌, 기존에 생성되어있는 인스턴스를 가져오는 것이다. 이것이 바로 ITEM1에서 살펴본 Boolean의 valueOf에서 사용된 기법이다.

장점

  • API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다.
  • 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다.
  • 정적 팩터리의 메서드 참조를 공급자(supplier)로 사용할 수 있다.

이러한 장점들이 굳이 필요하지 않다면 public 필드의 방식이 좋다.

둘 중 하나의 방식으로 만든 싱글턴 클래스를 직렬화하려면 단순히 Serializable을 구현한다고 선언하는 것만으로는 부족하다.

모든 인스턴스 필드를 일시적(transient)이라고 선언하고 readResolve 메서드를 제공해야한다. 이렇게 하지 않으면 직렬화된 인스턴스를 역직렬화할 때 마다 새로운 인스턴스가 만들어진다.

아래와 같이 추가를 해야한다.

private Object readResolve() {
	return INSTANCE;
}

이를 통해 진짜 Elvis를 반환하고, 가짜 Elvis는 가비지 컬렉터에 맡긴다.

열거 타입

가장 바람직한 방법이다.

public enum Elvis {
    INSTANCE;

    public void leaveTheBuilding() {
        System.out.println("Whoa baby, I'm outta here!");
    }

    // This code would normally appear outside the class!
    public static void main(String[] args) {
        Elvis elvis = Elvis.INSTANCE;
        elvis.leaveTheBuilding();
    }
}

public 필드 방법과 비슷하나, 더 간결하고 추가적인 작업없이 직렬화가 가능하다.

대부분의 상황에서 원소가 하나뿐인 열거타입이 싱글턴을 만드는 가장 좋은 방법이다.

출처

[Java] 리플렉션 API : 클래스, 필드, 메서드 정보 조회 - 코딩하는 흑구
이펙티브 자바 3/E

profile
백엔드 개발자 지망생

0개의 댓글

관련 채용 정보