Singleton이 깨지는 경우와 막는 방법 in Java

Singleton이 깨지는 경우와 막는 방법 in Java

Serialization & Reflection API

Singleton 패턴을 어쩔 수 없이 깨트리고 사용하는 방법이 있다고 한다.

사실 이럴 일이 있나 싶은데, 그래도 그렇다고 하니까 그렇다고 하자.

Legacy Project에서 Singleton Instance를 어떻게 고쳐서 사용해야 하는 경우가 있다고 가정하자. DB와 연관관계가 있거나, 그 인스턴스 자체를 수정할 수 없다거나 하는 경우라고 하자. 그럴 때 Reflection API를 통해 Singleton instance의 Signature라고 해야 하나? 그걸 수정하고 싶다고 하자. 아무튼 그렇다고 하자. 내 생각에는 다른 class를 만들어서 Singleton instance를 필드로 가지게 하거나 wrapper 등의 방식으로 감싸는 게 더 나을 것 같은데 그럴 일이 어찌어찌 있다고 하자.

우선 Reflection API는 난생처음 봤다. 괴물 같이 느껴졌는데, 사실 여기저기 많이 쓰이는 도구다. 특히 Spring 내부 Architecture에서 BeanFactory라거나, JPA라거나 그런 데서 사용된다고 한다. 약간 보통의 개발자가 사용하는 API 보다는 Framework 등을 개발할 때 사용되는 Core 단에서 효용을 볼 수 있는 그런 도구다.

직렬화는 아직 새내기 개발자라 알긴 알지만 실제로 어딘가 사용되는 것을 본 적은 없다. 직렬화나 Reflection API나 그 내용 자체를 다루는 것은 주제에 벗어나니까 벗어나자.

정리하면, Reflection API는 class를 동적으로 수정해서 사용할 수 있는 무서운 친구다. 그만큼 단점이 많기 때문에 잘 사용되는 일은 없을 듯싶다. 위에서 말한 것처럼 특정 문제가 발생했을 때, 최대한 다른 방법도 많을 것이라고 생각한다.

깨트린다는 게 악의적인 느낌으로의 깨트림이 아니라 잘못 사용되었을 경우, 즉 직렬화나 Reflection API를 사용해서 Singleton Pattern이 어쩌다가 깨지는 것이 있다는 게 중요하다. 그걸 막는 예를 정리한다.

Breaking Singleton

우선 깨지는 경우부터 보자.

Serialization

// Serializable을 구현하는 Settings class public class Settings implements Serializable { private Settings {} private static class Holder { private static final Settings INSTANCE = new Settings(); } public Settings getInstance() { return Holder.INSTANCE; } }

위와 같이 구현된 Singleton instance의 경우 직렬화 해서 객체를 저장하고, 다시 불러올 경우 저장 전의 객체(A)와 불러온 객체(B)는 서로 다른 객체가 된다.

public class App { public static void main(String[] args) throws Exception { Settings settingsA = Settings.getInstance(); Settings settingsB = null; try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("settingsA.obj"))) { out.writeObject(settingsA); } try (ObjectInput in = new ObjectInputStream(new FileInputStream("settingsA.obj"))) { settingsB = (Settings) in.readObject(); } Assert.isTrue(settingsA != settingsB, "These are same instances."); } }

위와 같이 사용했을 경우 서로 다른 class임을 알 수 있다. Singleton을 사용하는 목적이 프로세스 상에서 전역 변수처럼 하나의 객체로만 존재해야 하는데, A와 B 두 객체로 된 것이다.

Reflection API

public class App { public static void main(String[] args) throws Exception { Settings settings1 = Settings.getInstance(); Constructor constructor = Settings.class.getDeclaredConstructor(); constructor.setAccessible(true); Settings settings2 = constructor.newInstance(); System.out.println(settings1 == settings2); // false } }

위와 같은 방법으로 Singleton intance를 깨부술 수 있다. 특정 class의 선언된 생성자를 가져오고, 접근 권한 주고, 그 생성자로 새로운 instance를 생성할 수 있다는 정도의 맥락으로 볼 수 있다. Reflection API의 경우 그 용도와 방법이 매우 다양하기 때문에 자세한 설명은 달지 않는다. 무엇보다 잘 모른다.

How to prevent breaking a singleton instance

그럼 이제 막는 방법을 보자. Serialization의 경우 비교적 그 방법이 단순하다. 하지만 Reflection API의 경우 매우 강력한 친구라서 막는 방법이 많지 않다. 대표적으로 Enum을 사용한 Singleton을 구현하면 된다.

Avoid Deserialization

Serializable 구현체의 경우 직렬화 시점에서 특정 메서드를 호출하여 관리한다. 그 방법을 이용해서 해당 메서드를 Overriding 해서 Singleton class를 관리할 경우 역직렬화를 하더라도 직렬화 전후의 객체가 동일한 객체임을 보장할 수 있다.

class Settings implements Serializable { // ... // 역직렬화 대응방안 protected Object readResolve() { return getInstance(); } }

직렬화 방식을 구체적으로 알고 있다면 수월하게 막을 수 있다.

Avoid Reflection API

Reflection API의 경우 거듭 말했던 것처럼 단순한 방법으로 막을 수 없다. Singleton을 정리하는데 그다지 좋은 예제는 아닌 것 같다. 그냥 그렇구나 보면 되고, 아마도 Java에서만 발생할 수 있는 예시들이다.

해결책은 Singleton instance를 Enum class로 구현하면 알아서 해결된다. 이 방법은 자연스럽게 Serialization 취약점도 동시에 막을 수 있는 방법이다. 하지만 Enum을 쓴다는 것은 Java의 class가 가지는 이점도 포기하는 것이기 때문에 원천적인 해결책으로 볼 수는 없다.

public enum EnumClass { INSTANCE; // getters & setters }

Java를 잘 다룬다고 착각하던 시절이 있었다. 알고리즘 공부한답시고 자료구조 정도 몇 개 착착 구현할 줄 알 때가 그랬다. Effective, Reactive, Reflection, Deeeeeep Enum 등에 관련된 글을 볼 때면 아찔하다.

구글링을 돌려보니까 역직렬화나 Reflection 등으로 실제 문제가 생기기도 하는 것 같다. 뚫리는 이유랑 막는 방법 정도는 알고 있으면 좋을 것 같다. 언제쯤 Back End 개발을 할 수 있을까?

from http://jihogrammer.tistory.com/103 by ccl(A) rewrite - 2021-12-29 15:26:57