2022. 10. 11. 14:22ㆍDesignPattern
저번 게시글에서는 싱글톤 패턴에 대한 정의와 코드 구현법에 대한 이야기를 해보았다. 그런데, Java에서는 싱글톤으로 생성한 객체를 어떻게 활용하느냐에 따라, 싱글톤을 보장하지 못하는 경우가 있다고 한다.
이번 게시글에서는 싱글톤을 보장하지 못하는 경우와, 그 경우에 대한 대처법에 대해 정리해보고자 한다.
싱글톤 적용이 안되는 케이스 1 Reflection
우선 싱글톤 예제 코드를 하나 정리보도록 하자
LazyHolderSingleton
public class LazyHolderSingleton implements Serializable {
priate LazyHolderSingleton() {}
public static class LazyHolder {
private static final LazyHolderSingleton INSTANCE = new LazyHolderSingleton();
}
public static LazyHolderSingleton getInstance() {
return LazyHolder.INSTANCE;
}
}
싱글톤을 구현하는 방법은 여러가지가 있지만, LazyHolder 방식이 구현 난이도도 적고, Lazy initialization을 통해, 불필요한 리소스 소모를 방지하는 방식을 선호하는 개인적인 성향도 있어, 이번 예제 코드는 이 방식을 채택하였다.
그런데, 이 싱글톤 패턴은 소제목에서도 이야기 했듯이, 싱글톤으로 생성한 인스턴스를 reflection으로 복사할 경우, 기존 싱글톤 객체와 전혀 다른 인스턴스가 생성된다. 코드를 먼저 보자
PossibleError
public class PossibleError {
public static LazyHolderSingleton getInstanceWithReflection() {
Constructor<LazyHolderSingleton> constructor = null;
try {
constructor = LazyHolderSingleton.class.getDeclaredConstructor();
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
constructor.setAccessible(true);
try {
return constructor.newInstance();
} catch (InstantiationException | InvocationTargetException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
reflection api 메소드 명을 보고, 추측해볼 수 있겠지만, reflection은 싱글톤 패턴에서 기본 생성자를 private로 막아도, `setAccessible`함수 하나로 접근 허용을 할 수 있다.
따라서, reflection이 개입하면, 아무리 싱글톤 패턴 코드를 잘 설계해도, 일반 클래스들과도 마찬가지로 새로운 인스턴스를 계속 생성할 수 있고, 결과적으로는 싱글톤을 보장하지 못하게 된다. 이 부분을 테스트 코드로 증명하자면 다음과 같다.
@Test
@DisplayName("싱글톤 패턴으로 생성한 인스턴스와 리플렉션으로 생성한 객체는 싱글톤을 보장하지 못한다.")
void notSingletonWithReflection() {
LazyHolderSingleton singletonInstance = LazyHolderSingleton.getInstance();
LazyHolderSingleton reflectedInstance = PossibleError.getInstanceWithReflection();
assertThat(singletonInstance).isNotSameAs(reflectedInstance);
}
싱글톤 적용이 안되는 케이스 2 직렬화
사실 이 기능은 거의 100% 사용하지 않는 기능이지만, 싱글톤 객체를 직렬화하고, 나중에 역직렬화해서 읽을 경우에도 싱글톤을 보장하지 못한다. 코드로 보여주자면 다음과 같다.
object serializtion&deserialization
public static LazyHolderSingleton getDeserializedInstance() {
LazyHolderSingleton result;
LazyHolderSingleton source = LazyHolderSingleton.getInstance();
try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("lazyHolderSingleton.obj"))) {
out.writeObject(source);
} catch (IOException e) {
throw new RuntimeException(e);
}
try (ObjectInput in = new ObjectInputStream(new FileInputStream("lazyHolderSingleton.obj"))) {
result = (LazyHolderSingleton)in.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
return result;
}
test
@Test
@DisplayName("싱글톤 패턴으로 생성된 객체를 직렬화할 경우, 싱글톤을 보장하지 못한다.")
void notSingletonWithSerialized() {
LazyHolderSingleton singletonInstance = LazyHolderSingleton.getInstance();
LazyHolderSingleton deserializedInstance = PossibleError.getDeserializedInstance();
assertThat(singletonInstance).isNotSameAs(deserializedInstance);
}
직렬화 문제와 관련한 부분에 대한 해결책은 https://stackoverflow.com/questions/3930181/how-to-deal-with-singleton-along-with-serialization 이 부분을 참고해보면 알겠지만, `readResolve()` 메소드를 정의해주면 해결된다고 한다. 그러나, 자바 객체를 직접 직렬화해서 사용하는 경우는 거의 없기도 하거니와 `Serializable` 인터페이스에서 제공해주지 않는 api 이기 때문에 직관적인 방식은 아닌 듯 하다.
결론, 싱글톤을 직접 사용해야 한다면, enum을 고려해보자
직렬화 문제의 경우, `Object readResolve()`를 재정의하면 해결된다. 그렇다면, 리플렉션의 경우는 어떻게 해결할까?
제목에서도 이야기 했듯이, enum을 활용하면 된다.
enum은 jvm 단위에서 클래스 로딩 시점에서, 단 하나의 인스턴스를 생성하도록 설계되어 있어서, 별도의 코드를 추가하지 않고도, 싱글톤을 보장할 수 있으며, 일반 클래스 처럼, 필드를 선언하고, 메소드를 정의할 수 있다. 그리고, enum의 경우 reflection을 막아놓았기 때문에, 리플렉션으로 인해 발생하는 문제를 아예 미연에 방지할 수 있다. 또한 기본적으로 직렬화/역직렬화된 객체와도 싱글톤을 보장해준다. 코드로 표현하자면 다음과 같다.
SingletonEnum
public enum SingletonEnum {
INSTANCE;
private int value;
public void setValue(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
@Test
@DisplayName("Enum은 리플렉션 생성자체가 불가능하기 때문에, 리플렉션으로 인해 싱글톤이 깨지는 것을 컴파일 타임에서 막을 수 있다.")
void cannotReflectWithEnum() {
assertThrows(RuntimeException.class, PossibleError::getEnumInstanceWithReflection);
}
@Test
@DisplayName("Enum은 역직렬화한 객체와도 싱글톤을 보장한다.")
void isSingletonWithEnum() {
SingletonEnum singletonEnum = SingletonEnum.INSTANCE;
SingletonEnum deserializedEnumInstance = PossibleError.getDeserializedEnumInstance();
assertThat(singletonEnum).isSameAs(deserializedEnumInstance);
}
만약, 하나의 클래스를 상황에 따라 다르게 쓰고 싶다면?
좀 더 구체적으로 이야기 하자면, 하나의 클래스를 경우에 따라 싱글톤으로 쓰고 싶기도 하고, 그냥 별개의 인스턴스로 사용하고 싶을 수 있다. 이럴 땐 어떻게 하면 될까? 정답이 될지는 모르겠지만, 싱글톤으로 쓰고 싶은 객체는 컴파일 타임 직후에 한번 리플렉션으로 객체를 복사해서 그 객체만 사용하도록 하고, 아닌 경우에는 별도로 새로 인스턴스화 하면 될 것 같다. 그런데, 그렇게 할바에야 차라리, 스프링의 생명주기 콜백을 활용하는게 더 낫지 않을까 싶다.
'DesignPattern' 카테고리의 다른 글
싱글톤 패턴 -0- 정의와 구현법 (0) | 2022.01.28 |
---|