오직 하나의 인스턴스만 만드는 클래스를 싱글톤이라 말합니다.
싱글톤 패턴을 생성하는 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 클래스를 사용하는 것이 최선이 방법이라고 소개 하는데요.
저는 이 타입이 객체 지향 세계에서 과연 유연하게 사용될 지 잘 모르겠습니다.
상속도 안되어 코드의 유연성이 떨어지고 이 객체 혼자 고립되어 있어 협력이 안될거 같다는 생각이 듭니다.
'Book > Effective Java' 카테고리의 다른 글
| [Book] ITEM 9) try-finally 대신 try-with-resource 를 사용하라 (0) | 2022.03.19 | 
|---|---|
| [Book] ITEM 8) finalizer 와 cleaner는 피하라 (0) | 2022.03.07 | 
| [Book] ITEM 7) 더이상 쓰지 않는 객체 레퍼런스는 없애자 (0) | 2022.03.04 | 
| [Book] ITEM 6) 불필요한 객체는 만들지 말자 (0) | 2022.03.03 | 
| [Book] ITEM 5) 리소스를 엮을 때는 의존성 주입을 선호하라 (0) | 2022.03.03 | 
| [Book] ITEM 4) private 생성자로 noninstantiability를 강제할 것 (0) | 2022.03.01 | 
| [Book] ITEM 2) 생성자의 매개변수가 많다면 빌더를 고려하라 (0) | 2022.02.26 | 
| [Book] ITEM 1) 생성자 대신 정적 (static) 팩토리 메서드를 고려해 볼 것 (0) | 2022.02.26 |