DEV ℧ Developer Diary

[EffectiveJava] item03 - private 생성자나 열거(enum) 타입으로 싱글턴임을 보증하라.

private 생성자나 열거(enum) 타입으로 싱글턴임을 보증하라.

먼저, 싱글턴이 뭔지 간단하게 알아보자.

싱글턴(singleton)이란? 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다. 싱글턴은 클래스의 인스턴스를 단 한번만 생성하는 것으로, 메모리 측면이나, 데이터 공유가 용이하다.

보통 싱글턴의 선언에는 두가지 방식이 쓰인다. 두 방식 모두 생성자는 private로 감춘뒤 유일한 인스턴스로 접근할 수 있는 수단을 마련한다.

1. public static final 필드 방식

첫번째로, public static멤버가 final 필드인 방식을 보자

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

    private Car() {
        /* 클래스 외부에서 생성자를 호출하지 못하도록 private로 선언 */
    }
}

위의 예제는 private를 이용해 생성자를 만들어 싱글톤을 만드는 방법이다. 해당 방식은 인스턴스를 아래와 같이 호출한다.

Car car = Car.INSTANCE;

예제의 Car 클래스는 public이나 protected 생성자가 없으므로, 모든 시스템 중 Car클래스가 초기화 될때 만들어진 인스턴스가 하나뿐임을 보장된다.

그런데 여기서 문제점이 생길 수 있다. 클래스를 싱글턴으로 만들면 이를 사용하는 로직을 테스트 하기 어려워 질 수 있다. 또한, 리플렉션 API를 이용해 AccessibleObject.setAccessible을 사용해 private를 호출하면 생성자를 새롭게 생성할 수 있게된다.

AccessibleObject.setAccessible는 필드나 메서드의 접근제어 지시자에 의한 제어를 변경 할 수 있다. 해당값을 true로 준다면, private로 선언된 필드에 접근할 수 있게된다.

public static final 필드 방식의 장점으로는

  1. 해당 클래스가 싱글턴임을 API에게 명백히 드러낼 수 있다.
  2. 간결하다.

가 있다.

2. 정적 팩터리 메서드 방식

두번째로 정적 팩터리 메서드를 public static 멤버로 제공한다. item01에서 배운 그방법이다.

public class Car {
    private static Car car = new Car();

    private Car() {
        /* 클래스 외부에서 생성자를 호출하지 못하도록 private로 선언 */
    }

    /* 미리 생성한 인스턴스를 반환한다. */
    public static Car getInstance() {
        return car;
    }
}

위의 예제는 아래와 같이 인스턴스를 호출한다.

Car car = Car.getInstance();

정적 팩터리 메서드 방식의 싱글턴 생성의 장점은

  1. API를 바꾸지 않고 싱글턴에서 다른 방식으로 수정이 용이하다.
  2. 정적팩터리를 제네릭 싱글턴 팩터리로 만둘 수 있다.
  3. 정적 팰터리의 메서드 참조를 공급자(Supplier)로 사용할 수 있다는 점이다.

Supplier는 인자를 받지 않고 T객체를 반환하는 함수형 인터페이스로 Supplier supplier = Car::getInstance; 와 같이 사용할 수 있다.

만약 직렬화를 사용하기 위해 둘중 하나의 방식으로 싱글턴 클래스를 직렬화 한다면 Serializable을 구현한다고 선언하는 것 만으로 부족하다. 모든 인스턴스 필드를 일시적(transient)이라고 선언하고 readResolve 메서드를 제공한다.

/* 싱글턴임을 보장해주는 readResolve 메서드 */
private Object readResolve() {
    /* '진짜' Car를 반환하고, 가짜 Car는 가비지 컬렉터에 맡긴다. */
    return INSTANCE;
}

만약 해당 메서드를 선언하지 않으면 역직렬화(Deserializable)를 할때마다 새로운 인스턴스가 만들어진다.

Serializable을 하는 이유는 Object 객체나 Data를 byte 형태로 변환하는 것이다. 이와 반대되는 말로 Deserializable은 역직렬화로 byte로 변환된 형태를 원래대로 Object나 Data로 변환하는 기술이다.

3. Enum 타입 방식의 싱글턴

public enum Car {
    INSTANCE;
}

가장 간단한 방식의 싱글턴이다.. 하지만 상속을 사용 할 수가 없다.