오직 하나의 인스턴스만 만드는 클래스를 싱글톤이라 말합니다.
싱글톤 패턴을 생성하는 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 |