ch1.객체의 생성과 파괴

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

Singleton은 인스턴스를 오직 하나만 생성할 수 있는 클래스입니다.

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

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

Reflection을 사용하여 private 생성자를 호출할 수 있습니다.

리플렉션 API를 이용한 사용을 막으려면 두번째 생성자가 생성되려 할 때 예외를 던지면 됩니다. 코드

public class SingletonReflectionMain {
  public static void main(String[] args) {
    SingletonField singletonField = SingletonField.INSTANCE;
    
    try {
      Constructor<SingletonField> fieldConstructor = SingletonField.class.getDeclaredConstructor();
      fieldConstructor.setAccessible(true);
      SingletonField singletonField1 = fieldConstructor.newInstance();
      SingletonField singletonField2 = fieldConstructor.newInstance();
      System.out.println(singletonField);
      System.out.println(singletonField1);
      System.out.println(singletonField2);
    } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
      throw new RuntimeException(e);
    }
  }
}

결과

chapter01.item03.SingletonField@4517d9a3
chapter01.item03.SingletonField@372f7a8d
chapter01.item03.SingletonField@2f92e0f4

Reflection을 이용한 호출 시 예외를 던지도록 수정

코드

public class Singleton1 {
    public static final Singleton1 INSTANCE = new Singleton1();
    private static boolean created;

    private Singleton1() {
        if (created) {
            throw new IllegalStateException("Singleton already created");
        }
        created = true;
    }

}

결과

Caused by: java.lang.IllegalStateException: Singleton already created
	at chapter01.item03.SingletonField.<init>(SingletonField.java:12)
	... 6 more

직렬화를 사용하여 싱글턴을 깨트릴 수 있습니다.

코드

public class SingletonSerializationMain {
    public static void main(String[] args) {
        SingletonField singletonField = SingletonField.INSTANCE;
        
        try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("field.obj"))) {
            out.writeObject(SingletonField.INSTANCE);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        try(ObjectInput in = new ObjectInputStream(new FileInputStream("field.obj"))){
            SingletonField singletonField1 = (SingletonField) in.readObject();
            System.out.println(singletonField);
            System.out.println(singletonField1);
        } catch (IOException | ClassNotFoundException e) {
            throw new RuntimeException(e);
        }

    }
}

결과

chapter01.item03.SingletonField@2ef1e4fa
chapter01.item03.SingletonField@4459eb14

readResolve를 사용하여 직렬화를 사용하여 싱글턴을 깨트리지 않도록 수정

코드

public class SingletonField implements Serializable {
    public static final SingletonField INSTANCE = new SingletonField();
    private static boolean created;

    private SingletonField() {
        if (created) {
            throw new IllegalStateException("Singleton already created");
        }
        created = true;
    }

    private Object readResolve() {
        return INSTANCE;
    }
}

결과

chapter01.item03.SingletonField@2ef1e4fa
chapter01.item03.SingletonField@2ef1e4fa

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

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

3. 열거 타입 방식의 싱글턴

public enum Singleton3 {
    INSTANCE;
}

싱글턴 클래스 사용

public class SingletonMain {
  public static void main(String[] args) {
    Singleton1 singleton1 = Singleton1.INSTANCE;
    Singleton2 singleton2 = Singleton2.getInstance();
    Singleton3 singleton3 = Singleton3.INSTANCE;
  }
}

싱글턴은 테스트하기 어렵다.

클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트(Concert)를 테스트하기 어렵습니다. 아래 Concertperform()lightsOn()mainStateOpen()을 테스트하고 싶은데 진짜 Elvis가 노래를 계속 부르게 됩니다. 만약 Elvis가 외부API와 연동된 비싼 인스턴스라면 더욱 문제가 됩니다.

public class Concert {
    private boolean lightsOn;

    private boolean mainStateOpen;

    private Elvis elvis;

    public Concert(Elvis elvis) {
        this.elvis = elvis;
    }

    public void perform() {
        mainStateOpen = true;
        lightsOn = true;
        elvis.sing();
    }

    public boolean isLightsOn() {
        return lightsOn;
    }

    public boolean isMainStateOpen() {
        return mainStateOpen;
    }
}

싱글턴은 어떻게 테스트?

인터페이스를 통해 테스트하면 됩니다.

IElvis를 상속한 ElvisMockElvis를 만들고 MockElvis를 통해 테스트하면 됩니다.

public interface IElvis {
    void leaveTheBuilding();

    void sing();
}

댓글남기기