베하~!
BTC 블랙아웃입니다!!
지난 포스팅에서는 싱글톤 패턴의 구현 방법에 대해 알아봤습니다.
지난 포스팅
오늘은 이 패턴의 취약점을 알아보고, 싱글톤 패턴을 어떻게 깨트릴 수 있는지 그리고 대응 방법에 대해 살펴보겠습니다.
리플렉션(Reflection)
리플렉션이란 Java에서 제공하는 기능 중 하나로, 런타임에서 프로그램의 내부 구조, 즉 클래스, 메서드, 필드, 생성자 등의 메타데이터에 접근하거나 조작할 수 있게 해주는 API입니다.
이를 이용하면 ‘private’ 생성자에도 접근이 가능하므로 싱글톤 객체를 또 생성할 수 있습니다.
Singleton singleton = Singleton.getInstance();
Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Singleton singleton1 = declaredConstructor.newInstance();
System.out.println(singleton == singleton1);
위의 코드는 싱글톤 인스턴스를 생성한 후 리플렉션을 통해 private 생성자에 접근하고 새로운 인스턴스를 생성하여 객체가 동일한지 비교하는 코드입니다.
이때 setAccessible(true) 메서드가 ‘private’ 생성자에 접근할 수 있게 해주는 역할을 합니다.
직렬화 & 역직렬화
Singleton singleton = Singleton.getInstance();
Singleton singleton1 = null;
try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("singleton.obj"))) {
out.writeObject(singleton);
}
try (ObjectInput in = new ObjectInputStream(new FileInputStream("singleton.obj"))) {
singleton1 = (Singleton) in.readObject();
}
System.out.println(singleton == singleton1);
위 코드는 ‘singleton’이라는 싱글톤 인스턴스를 생성하고, 다른 싱글톤 인스턴스를 저장할 ‘singleton1’이라는 변수를 선언했습니다.
그 후에 FileOutputStream을 사용하여 ‘singleton’ 객체를 직렬화하여 ‘singleton.obj’에 저장하고, ObjectInputStream을 사용하여 역직렬화 한 다음 ‘singleton1’ 객체에 저장했습니다.
그리고 ‘singleton’ 객체와 ‘singleton1’객체를 비교하는 코드입니다.
결과는 ‘false’로 서로 다른 객체로 인식하게 됩니다.
그 이유는 직렬화 & 역직렬화의 과정 때문입니다.
자바의 직렬화(Serialize)는 JVM의 힙 메모리에 있는 객체 데이터를 바이트 스트림(byte stream) 형태로 바꿔 외부 파일로 내보낼 수 있게 하는 기술을 말합니다. 반대로 외부로 내보낸 직렬화 데이터를 다시 읽어 들여 자바 객체로 재변환하는 것을 역직렬화(Deserialize)라 합니다.
그런데 JVM은 바이트 스트림에서 객체를 만들 때 새로운 객체를 생성하기 때문에 역직렬화의 과정에서 싱글톤이 깨지게 되는 것입니다.
💡 직렬화하기 위해서는 클래스에 Serializable 인터페이스를 구현해야 합니다.
- 이는 마커 인터페이스(marker interface)로서, 별도의 메서드를 포함하고 있지 않지만, 클래스에 이 인터페이스를 구현한다는 것은 해당 클래스의 인스턴스가 직렬화될 수 있음을 JVM에 알려주는 것입니다.
- 이를 쓰지 않으면 java.io.NotSerializableException 가 발생하게 됩니다.
readResolve()
이를 예방하기 위해서는 ‘readResolve()’ 라는 메서드를 이용하면 됩니다.
동일한 객체 인스턴스를 유지하기 위해 ‘readResolve()’ 라는 메서드를 사용하게 되는데 이 메서드의 반환 값이 역직렬화의 최종 결과가 됩니다.
protected Object readResolve() {
return getInstance();
}
때문에 이처럼 구현하게 된다면 역직렬화를 하더라도 항상 동일한 인스턴스를 반환할 수 있게 되어 싱글톤 패턴이 깨지는 것을 대응할 수 있습니다.
그렇다면 리플렉션으로 인해 싱글톤이 깨지는 일도 방지할 수 있을까요?
enum
해당 방법으로는 ‘enum’을 사용하면 됩니다.
public enum Singleton {
INSTANCE;
}
이 방법은 아주 간단하지만 리플렉션 뿐만 아니라 직렬화 & 역직렬화의 과정에서도 인스턴스의 생성을 방지할 수 있습니다.
JVM이 열거형(enum) 상수의 유일성을 보장하기 때문입니다.
- 장점 : 싱글톤 이탈 방지에 좋음
- 단점 : 미리 생성됨
Enum 클래스는 Serializable 인터페이스를 구현하기 때문에 이를 상속받는 enum 타입은 자동으로 직렬화할 수 있습니다.
이렇게, 싱글톤 패턴이 갖는 몇몇 취약점과 이를 대응하는 방법에 대해 살펴보았습니다. 싱글톤 패턴을 사용할 때에는 이러한 취약점을 항상 염두에 두고, 안전한 코드를 작성하는 것이 중요합니다.
특히, 실제로 서비스나 애플리케이션에서 싱글톤을 구현할 때에는 반드시 리플렉션과 직렬화 문제에 대비하여야 합니다.
다음 포스팅에서는 또 다른 디자인 패턴에 대해 알아볼 예정이니, 기대해 주세요!
이상으로 포스팅을 마치겠습니다.
감사합니다!
'Programming' 카테고리의 다른 글
CORS (0) | 2023.08.18 |
---|---|
React Native란? (0) | 2023.08.08 |
[Rust] Rust에 대하여 (0) | 2023.08.03 |
[Java] 싱글톤 패턴 구현하는 방법 (0) | 2023.08.03 |
React란? (0) | 2023.07.24 |
댓글