본문 바로가기

Dev Book Review/Effective Java

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

1. 싱글턴이란?

싱글턴(singleton) : 인스턴스를 오직 하나만 생성할 수 있는 클래스

  • 함수와 같은 무상태(stateless) 객체 - 정적 멤버클래스 이야기인가? (Enum 같은거?)
  • 설계상 유일해야하는 시스템 컴포넌트
  • 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기 어려울 수 있다. : mock 대체가 불가능

 

2. 방식 1) public static 멤버가 final 필드

Private 생성자가 public static final 필드를 초기화 할 때 딱 한번만 호출된다.

예외 ) 권한이 있는 클라이언트가 리플렉션 API인 AccessibleObject.setAccessible을 이용해 private 생성자를 호출

방어 ) 생성자를 수정해 두 번째 객체 생성때 예외 던지기

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

    public void leaveTheBuilding() {...}
}

[ 장점 ]

  • 이 클래스가 싱글턴임이 API에 명백히 들어난다.
  • 간결하다.

 

3. 방식 2) 정적 팩터리 메서드를 public static 멤버로 제공

Elvis.getInstance는 항상 같은 객체의 참조를 반환한다. (리플렉션 예외는 똑같이 적용)

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

    public void leaveTheBuilding() {...}
}

 

[ 장점 ]

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

 

4. 방식 3) Enum 타입 방식의 싱글턴

public enum Elvis {
    INSTANCE;
    public void leaveTheBuilding() {...}
}

[ 장점 ]

  • 간결하다
  • 추가 노력 없이 직렬화가 가능하다.
  • 직렬화나 리플렉션 공격에서도 제2의 인스턴스가 생기는 일을 막아준다.

조건 : 싱글턴이 부모클래스가 되어야하는 상황이 생기면 사용할 수 없다.

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

 

5. 싱글턴 클래스의 직렬화

Serializable 구현한다고 선언하는 것만으로는 부족하다. 인스턴스 필드를 transient라고 선언하고 readResolve 메서드를 제공해야한다. (readResolve에서 인스턴스를 반환한다. - 새로운 인스턴스 생성을 방지한다.)

참조 링크 : https://github.com/Java-Bom/ReadingRecord/issues/4

 

추가1. 제네릭 싱글턴 팩터리

public class GenericSingletonFactory {
    // 코드 30-4 제네릭 싱글턴 팩터리 패턴 (178쪽)
    private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;

    @SuppressWarnings("unchecked")
    public static <T> UnaryOperator<T> identityFunction() {
        return (UnaryOperator<T>) IDENTITY_FN;
    }

    // 코드 30-5 제네릭 싱글턴을 사용하는 예 (178쪽)
    public static void main(String[] args) {
        String[] strings = { "삼베", "대마", "나일론" };
        UnaryOperator<String> sameString = identityFunction();
        for (String s : strings)
            System.out.println(sameString.apply(s));

        Number[] numbers = { 1, 2.0, 3L };
        UnaryOperator<Number> sameNumber = identityFunction();
        for (Number n : numbers)
            System.out.println(sameNumber.apply(n));
    }
}

코드 요구사항 : IDENTITY_FN를 UnaryOperator<T>로 형변환 하면 비검사 형변환 경고가 발생 Object는 T가 아니기 때문. 그러나 여기 요구사항에서 항등함수를 담은 클래스를 만들고 싶은 것이기 때문에, 비검사 형변환 경고를 숨겨도 안심할 수 있다.

 

추가2. 메서드 참조 & 공급자

아이템 43 참고

메서드 참조 유형 같은 기능을 하는 람다
정적 Integer::parseInt str -> Integer.parseInt(str)
한정적 (인스턴스) Instant.now()::isAfter Instant then = Instatn.now();
t->then.isAfter(t);
비한정적 (인스턴스) String::toLowerCase str -> str.toLowerCase();
클래스 생성자 TreeMap<K,V>::new () -> new TreeMap<K,V>();
배열 생성자 int[]::new len -> new int[len]

공급자 = Supplier

인터페이스 명 추상 메서드 설명
Supplier<T> T get() T 객체를 리턴한다.