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

ds_chanin

·

2020. 1. 17. 19:21


아이템 3 private 생성자나 열거타입으로 싱글턴임을 보증하라

p.23 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워질 수 있다.
타입을 인터페이스로 정의한 다음 그 인터페이스를 구현해서 만든 싱글턴이 아니라면 mock 구현으로 대체할 수 없기 때문이다.

인터페이스 없이 구현한 싱글턴 코드

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

    public static final Validator getInstance() {
        return INSTANCE;
    }

    private int bound;

    private Validator() {
        this.bound = 10;
    }

    public void validate(int target) {
        if (target > bound) {
            throw new IllegalArgumentException();
        }
    }

}

class Service {

    private final Validator validator = Validator.getInstance();

    public void validate(int target) {
        validator.validate(target); // validator 가 제대로 작동하는지 테스트를 하려는데 bound 값을 바꿔서 테스트하고 싶다면?
    }
}

인터페이스로 정의한 다음 구현한 싱글턴 코드

public interface Validator {
    void validate(int target);
}

class RealValidator implements Validator {
    private static final Validator INSTANCE = new RealValidator();

    private int bound = 10;

    private RealValidator() {
    }

    @Override
    public void validate(int target) {
        if (target > bound) {
            throw new IllegalArgumentException();
        }
    }

}

class MockValidator implements Validator {
    private static final Validator INSTANCE = new MockValidator();

    private int bound;

    private MockValidator() {
    }

    // 테스트를 위한 코드, 이러한 코드는 production 코드에 노출되어선 안되기 때문에 mock 클래스에만 존재한다.
    public void changeMockBound(int mockBound) {
        this.bound = mockBound;
    }

    @Override
    public void validate(int target) {
        if (target > bound) {
            throw new IllegalArgumentException();
        }
    }
}

테스트를 유연하게 할 수 있는 구조가 된다.

주의해야할 점

싱글턴 클래스를 직렬화하려면 단순히 Serializable을 구현할 것이 아니라, 모든 인스턴스 필드를 transient라고 선언해줘야지 역직렬화 할때 새로운 인스턴스가 생성이 안된다.
혹은 readResolve() 메서드를 추가하여 싱글턴임을 보장하자.

public class Singleton implements Serializable {
    private static final Singleton INSTANCE = new Singleton();

    public static Singleton getInstance() {
        return INSTANCE;
    }

    // 역직렬화시 새로운 인스턴스 생성 요인 차단방법
    // transient 예약어를 인스턴스 변수에 추가하여 직렬화를 차단하여 역직렬화시 새로운 인스턴스가 생성되지 못하도록 한다.
    transient private int value;

    private Singleton() {
        value = 1;
    }

    // 역직렬화시 새로운 인스턴스 생성 요인 차단방법
    // 역직렬화시 readResolve 를 재정의하여 기존 인스턴스를 반환하여 싱글턴을 보장하도록 한다.
    private Object readResolve() {
        return INSTANCE;
    }

    @Override
    public String toString() {
        return "Singleton{" +
                "value=" + value +
                '}';
    }
}

테스트 코드(Junit5 + assertJ)

class SingletonTest {

    @DisplayName("자바 직렬화 테스트")
    @Test
    void singletonTest1() throws IOException, ClassNotFoundException {
        Singleton singleton = Singleton.getInstance();
        byte[] serializedSingletonByte;
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
                oos.writeObject(singleton);
                // serializedSingletonByte -> 직렬화된 객체
                serializedSingletonByte = baos.toByteArray();
            }
        }
        // 바이트 배열로 생성된 직렬화 데이터를 base64로 변환
        String serializedSingleton = Base64.getEncoder().encodeToString(serializedSingletonByte);

        byte[] deSerializedSingletonByte = Base64.getDecoder().decode(serializedSingleton);

        ByteArrayInputStream bais = new ByteArrayInputStream(deSerializedSingletonByte);
        ObjectInputStream ois = new ObjectInputStream(bais);

        // 역직렬화된 Singleton 객체를 읽어온다.
        Object objectMember = ois.readObject();
        Singleton deSerializedSingleton = (Singleton) objectMember;

        assertAll(
                () -> assertThat(singleton).isEqualTo(deSerializedSingleton),
                () -> assertThat(singleton == deSerializedSingleton).isTrue(),
                () -> assertThat(singleton.toString().equals(deSerializedSingleton.toString())).isTrue()
        );

    }

}

테스트 결과

image

대부분의 상황에서 가장 바람직한 방법은 논리적으로 원소가 하나 뿐인 Enum과 같은 열거형을 이용하여 싱글턴을 구현하는 것이다.