Book/Effective Java

[Book] ITEM 3) private 생성자 또는 enum 타입을 사용해서 싱글톤으로 만들것

kkkkkkkkkkkk 2022. 3. 1. 16:06

오직 하나의 인스턴스만 만드는 클래스를 싱글톤이라 말합니다.

싱글톤 패턴을 생성하는 2가지 방법이 있는데 두 방법 모두 생성자를 private 으로 만들고 public static 멤버를 사용해서 유일한 인스턴스를 제공한다.

🟠  public static final 필드

첫번째 방법은 static final 필드로 인스턴스를 생성합니다.

static 필드는 메모리에 올라갈 때 인스턴스를 단 한번만 생성해주는 장점이 있고 인스턴스가 여러번 생성이 되도 인스턴스는 동일한 객체입니다.

public class Singleton {

    public static final Singleton singleton = new Singleton();

    private Singleton() {}

}

테스트 코드를 이용하여 동일한 객체인지 검증하는 코드를 작성해보겠습니다.

@Test
@DisplayName("싱글톤 비교")
void SingletonTest() {
    // given
    Singleton singleton1 = Singleton.singleton;
    Singleton singleton2 = Singleton.singleton;

    int singletonHashCode1 = singleton1.hashCode();
    int singletonHashCode2 = singleton2.hashCode();

    String singletonName1 = singleton1.toString();
    String singletonName2 = singleton2.toString();

    // when
    boolean hashCodeIsTrue = singletonHashCode1 == singletonHashCode2;
    boolean nameIsTrue = singletonName1.equals(singletonName2);

    // then
    assertThat(hashCodeIsTrue).isTrue();
    assertThat(nameIsTrue).isTrue();
}

private 생성자를 통해 단 하나의 인스턴스를 보장할 수 있는 장점을 reflection 기능으로 무산 시켜버릴 수 있습니다.

테스트 코드를 보면서 이해해 봅시다. 우선 singleton 이라는 인스턴스를 생성합니다. 다음 reflection 기능으로 private 생성자에 접근하여 newSingleton 인스턴스를 만듭니다.

그리고 singleton 과 newSingleton 이 다른지 검증을 합니다.

테스트 결과는 객체가 서로 달라서 성공입니다.

@Test
@DisplayName("싱글톤 깨짐")
void SingletonTest1()throws 예외 생략 {
    // given
    Singleton singleton = Singleton.singleton;

    // when
    // 클래스 풀패키지명으로 reflection Singleton.Class 로도 가능함 어떤걸로 해도 무방하다.
    Class<?> aClass = Class.forName("com.example.book_system.singleton.Singleton");
    Constructor<?> declaredConstructor = aClass.getDeclaredConstructor();
    declaredConstructor.setAccessible(true);
    Singleton newSingleton = (Singleton) declaredConstructor.newInstance();

    // then
    assertThat(singleton.equals(newSingleton)).isFalse();
}

reflection 에 대한 방어 라고 할까요?? 아무튼 단 하나의 인스턴스 보장 해줄수 있는 코드가 있습니다.

생성자를 한번 호출할 때 카운트를 하여 카운트가 1과 같거나 클 때 IllegalStateException 예외를 던지면 됩니다. 코드가 영 더럽습니다. ㅎㅎ

public class Singleton {

public static final Singletonsingleton= new Singleton();
    int count;

    private Singleton() {
        count++;
        if(count >= 1) {
        throw new IllegalStateException("Singleton Class 는 여러 인스턴스를 생성할 수 없습니다,");
        }
    }
}

하지만 단점도 있으면 장점도 존재하겠죠?

이러한 방법은 static factory method를 사용하는 방법보다 명확하고 간단합니다.

static factory method 방법을 알아보고 차이점을 생각해봅시다.

🟠  static factory method

static final 필드를 private 으로 인스턴스를 생성하고 생성자는 똑같이 private 생성자로 생성합니다.

그리고 나서 static factory method 를 사용하여 해당 필드의 인스턴스를 반환합니다.

public class Person {

    private static final PersonPERSON= new Person();

    private Person() {
    }
		
    // static factory method 사용!!
    public static Person getInstance() {
    return PERSON;
}

이 방법의 장점은 싱글톤을 사용할지 아니면 스레드 당 새로운 인스턴스를 사용할지 클라이언트 코드를 건들지 않고 반환 타입만 변경 할 수 있는 유연성을 제공해줍니다.

나는 하나의 스레드 당 새로운 인스턴스를 생성할꺼야!

public static Person getInstance() {
    return new Person();
}

이 코드는 supplier의 인터페이스의 구현체로 사용이 가능합니다.

Supplier<Person>supplier = Person::getInstance;

실제로 달라지는지 검증을 해봅시다.

@Test
@DisplayName("factory method singleton test")
void PersonTest() {
    // given & when
    Person instance1 = Person.getInstance();
    Person instance2 = Person.getInstance();

    // then
    assertThat(instance1.equals(instance2)).isFalse();

}

자 그럼 여기서 static final 필드를 사용한 것과 static factory 의 클라이언트 사용 코드를 보겠습니다. 어떤 것이 더 명확할 까요?? 

 

// static final 필드
Person instance = Person.singleton;

// static factory
Person instance1 = Person.getInstance();

🟠  직렬화 (Serialization)

위의 2가지 방법을 적용하고 직렬화에 사용한다면 역직렬화 할 때 같은 인스턴스가 여러개 생길 수 있습니다.

문제를 해결하려면 transient 를 추가하고 readResolve() 메서드의 반환을 해당 필드의 인스턴스로 정의합니다.

먼저 처음 Person 클래스를 직렬화 / 역직렬화 과정을 거쳐 인스턴스 값을 비교하는 코드를 작성 해보겠습니다.

직렬화 클래스를 구현하였습니다.

public class ObjectSerialization {

    private static ByteArrayOutputStream byteArrayOutputStream;
    private static ObjectOutputStream objectOutputStream;

    private ObjectSerialization() {}

    public static byte[] makeByteEnCodeArray(Object o) throws 예외생략 {
        byteArrayOutputStream = new ByteArrayOutputStream();
        objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(o);
        return byteArrayOutputStream.toByteArray();
    }

    public static String makeEncodeObject(byte[] bytes) {
        return Base64.getEncoder().encodeToString(bytes);
    }
}

역직렬화 클래스를 구현하였습니다.

public class ObjectDeSerialization {

    private static ByteArrayInputStream byteArrayInputStream;
    private static ObjectInputStream objectInputStream;

    public static byte[] makeByteDecodeArray(String encodeObject) {
        return Base64.getDecoder().decode(encodeObject);
    }

    public static Object makeDecodeObject(byte[] bytes) throws 예외생략 {
        byteArrayInputStream = new ByteArrayInputStream(bytes);
        objectInputStream = new ObjectInputStream(byteArrayInputStream);
        return objectInputStream.readObject();
    }
}

직렬화 / 역직렬화 과정을 거치면 인스턴스는 새로 생성 된다는 가정을 하고 테스트를 진행 해보겠습니다.

    @Test
    @DisplayName("serialization 사용")
    void serialization() throws IOException, ClassNotFoundException {

        // given
        Person instance = Person.getInstance();

        // when
        // 직렬화
        byte[] bytes = ObjectSerialization.makeByteEnCodeArray(instance);
        String personType = ObjectSerialization.makeEncodeObject(bytes);

        // 역직렬화
        byte[] bytes1 = ObjectDeSerialization.makeByteDecodeArray(personType);
        Person person = (Person) ObjectDeSerialization.makeDecodeObject(bytes1);

        // then
        assertThat(instance.equals(person)).isFalse();
        assertThat(instance == person).isFalse();
    }

인스턴스가 다르다는 결과가 나왔습니다.

하나의 인스턴스가 나오게 설정을 해보겠습니다.

public class Person implements Serializable{
    
    // transient 추가
    private static final transient Person PERSON = new Person();

    private Person() {}

    public static Person getInstance() {
        return PERSON;
    }
    
    // readResolve() 메서드 구현 -> 반환 PERSON
    private Object readResolve() {
        return PERSON;
    }
}

동일 인스턴스가 생성 되었다는 가정을 하고 테스트 코드를 작성 해보겠습니다.

    @Test
    @DisplayName("serialization 사용")
    void serialization() throws IOException, ClassNotFoundException {

        // given
        Person instance = Person.getInstance();

        // when
        // 직렬화
        byte[] bytes = ObjectSerialization.makeByteEnCodeArray(instance);
        String personType = ObjectSerialization.makeEncodeObject(bytes);

        // 역직렬화
        byte[] bytes1 = ObjectDeSerialization.makeByteDecodeArray(personType);
        Person person = (Person) ObjectDeSerialization.makeDecodeObject(bytes1);

        // then
        assertThat(instance.equals(person)).isTrue();
        assertThat(instance == person).isTrue();

    }

동일한 인스턴스가 생성되는 것을 알 수 있습니다.

🟠  enum

싱글톤의 최선의 방법은 enum 클래스를 사용하는 것이 최선이 방법이라고 소개 하는데요.

저는 이 타입이 객체 지향 세계에서 과연 유연하게 사용될 지 잘 모르겠습니다.

상속도 안되어 코드의 유연성이 떨어지고 이 객체 혼자 고립되어 있어 협력이 안될거 같다는 생각이 듭니다.