## 목표
- 인스턴스 수를 통제해야 할 때,
readResolve보다 열거 타입을 사용하는 것이 왜 더 좋은지 알아보자.
## 핵심 요약
직렬화된 싱글턴의 문제점
Serializable을 구현한 싱글턴 클래스는 역직렬화 시 새로운 인스턴스가 생성될 위험이 있다.
readResolve()를 사용하면 기존 인스턴스를 반환할 수 있지만, 완전한 해결책은 아니다.
열거 타입(Enum) 기반 싱글턴의 장점
- 자바가 제공하는
Enum은 기본적으로 직렬화가 안전하다.
- 리플렉션 공격에도 강하며, 추가적인 코드 없이 싱글턴을 보장할 수 있다.
- 따라서 싱글턴을 구현할 때는
Enum을 사용하는 것이 가장 안전한 방법이다.
1. 직렬화 가능한 싱글턴 클래스의 문제점
기본적인 싱글턴 구현 (문제 발생 가능)
public class Elvis implements Serializable {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {}
private String[] favoriteSongs = {"Hound Dog", "Heartbreak Hotel"};
public void printFavorites() {
System.out.println(Arrays.toString(favoriteSongs));
}
private Object readResolve() {
return INSTANCE;
}
}
readResolve()를 사용했지만 역직렬화 중 새로운 객체가 생성되는 문제는 여전히 존재한다.
☘️ 객체가 새로 생성되는 과정
직렬화된 Elvis 객체를 역직렬화하면, readObject()를 통해 새로운 객체가 생성됨.
이 과정에서 생성자는 호출되지 않지만, 자바의 역직렬화 메커니즘이 바이트 스트림을 기반으로 새로운 인스턴스를 만든다.
그 후 readResolve()가 실행되어 기존 INSTANCE를 반환함.
새로운 객체는 사용되지 않고, 기존의 INSTANCE가 반환됨.
하지만 이전에 생성된 새로운 객체는 메모리에 존재했다가 가비지 컬렉터에 의해 수거됨.
공격자가 이 과정에서 새로운 객체를 가로챌 수 있음.
readResolve()가 실행되기 전에, 악의적인 코드(ElvisStealer 같은 클래스)를 이용하면 새로운 객체의 참조를 가져올 수 있음.
이러면 원래 하나여야 하는 Elvis 인스턴스가 두 개 이상으로 늘어나게 됨.
2. ElvisStealer 공격 기법
싱글턴을 훔치는 도둑 클래스
public class ElvisStealer implements Serializable {
static Elvis impersonator;
private Elvis payload;
private Object readResolve() {
impersonator = payload;
return new String[]{"A Fool Such as I"};
}
}
ElvisStealer를 이용하면, readResolve() 실행 이전의 인스턴스 참조를 가로챌 수 있다.
3. 문제해결책
1️⃣ Enum 싱글턴 (가장 안전한 방법)
public enum Elvis {
INSTANCE;
private String[] favoriteSongs = {"Hound Dog", "Heartbreak Hotel"};
public void printFavorites() {
System.out.println(Arrays.toString(favoriteSongs));
}
}
Enum을 사용하면 역직렬화 공격과 리플렉션 공격을 모두 차단할 수 있다.
- 자바가
Enum을 직렬화할 때 인스턴스가 하나만 유지되도록 보장한다.
2️⃣ transient 키워드 사용
public class Elvis implements Serializable {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {}
// 필드를 transient 로 선언하여 직렬화되지 않도록 함
private transient String[] favoriteSongs = {"Hound Dog", "Heartbreak Hotel"};
}
- transient 필드는 직렬화되지 않으므로, readResolve() 실행 전에 참조를 탈취할 방법이 없어진다.
Topics
transient vs static
1️⃣ transient vs static 차이점
| 키워드 |
직렬화 여부 |
주요 특징 |
언제 사용하면 좋을까? |
transient |
❌ |
객체별 필드를 직렬화에서 제외 |
객체 내부의 보안 필드, 민감한 데이터 보호 |
static |
❌ |
클래스 레벨 변수이므로 직렬화 대상이 아님 |
공유해야 할 상수, 설정 정보, 싱글턴 유지 |
2️⃣ transient vs static 예제 비교
(1) transient 사용 예제
public class Elvis implements Serializable {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {}
// transient 필드: 직렬화되지 않음
private transient String[] favoriteSongs = {"Hound Dog", "Heartbreak Hotel"};
public void printFavorites() {
System.out.println(Arrays.toString(favoriteSongs));
}
private Object readResolve() {
return INSTANCE;
}
// 역직렬화 후 필드를 다시 초기화하는 메서드
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
favoriteSongs = new String[]{"Hound Dog", "Heartbreak Hotel"}; // 필드 복구
}
public static void main(String[] args) throws Exception {
Elvis elvis1 = Elvis.INSTANCE;
// 직렬화 수행
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(elvis1);
oos.close();
// 역직렬화 수행
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
Elvis elvis2 = (Elvis) ois.readObject();
ois.close();
// 두 인스턴스가 같은지 확인
System.out.println("elvis1 == elvis2: " + (elvis1 == elvis2)); // true
// favoriteSongs가 정상적으로 복구되었는지 확인
elvis2.printFavorites(); // [Hound Dog, Heartbreak Hotel]
}
}
결과 정리
favoriteSongs 필드는 transient이므로 직렬화되지 않고, 역직렬화 후 null 이 된다.
readObject()를 통해 필드를 다시 초기화했기 때문에 안전하게 값을 복구할 수 있다.
(2) static 사용 예제
public class Elvis implements Serializable {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {}
// static 필드: 직렬화되지 않음 (클래스 변수이므로 원래 직렬화 대상이 아님)
private static String[] favoriteSongs = {"Hound Dog", "Heartbreak Hotel"};
public void printFavorites() {
System.out.println(Arrays.toString(favoriteSongs));
}
private Object readResolve() {
return INSTANCE;
}
public static void main(String[] args) throws Exception {
Elvis elvis1 = Elvis.INSTANCE;
favoriteSongs[0] = "Blue Suede Shoes"; // 직렬화 전에 값 변경
// 직렬화 수행
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(elvis1);
oos.close();
// favoriteSongs 변경
favoriteSongs[0] = "Jailhouse Rock"; // 직렬화 후 값 변경
// 역직렬화 수행
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
Elvis elvis2 = (Elvis) ois.readObject();
ois.close();
// 두 인스턴스가 같은지 확인
System.out.println("elvis1 == elvis2: " + (elvis1 == elvis2)); // true
// favoriteSongs가 변경되었는지 확인
elvis2.printFavorites(); // [Jailhouse Rock, Heartbreak Hotel]
}
}
결과 정리
static 필드는 애초에 직렬화되지 않는다.
- 따라서, 직렬화 후 값을 변경해도 역직렬화 후 변경된 값이 유지된다.
readObject()를 사용하지 않아도 값이 그대로 유지되므로 객체의 상태와 무관하게 클래스의 공유 데이터로 동작한다.
총 정리
- 객체 내부의 특정 필드만 직렬화되지 않도록 하려면
transient을 사용해야 한다.
- 클래스 레벨에서 공유하는 필드는
static을 사용하면 된다.
- 보안이 중요한 정보(비밀번호, 키 값)는
transient를 사용해서 직렬화되지 않도록 하는 것이 좋다.
💡 핵심 정리
- 싱글턴을 구현할 때
readResolve()를 사용하는 것은 완벽한 해결책이 아니다.
- 직렬화된 객체가
readResolve() 이전에 임시로 생성될 수 있어, 공격 가능성이 존재한다.
- 가능하면 열거 타입(
Enum)을 사용하여 싱글턴을 구현하는 것이 가장 안전하다. (또는 transient 키워드 사용)
## 목표
readResolve보다 열거 타입을 사용하는 것이 왜 더 좋은지 알아보자.## 핵심 요약
직렬화된 싱글턴의 문제점
Serializable을 구현한 싱글턴 클래스는 역직렬화 시 새로운 인스턴스가 생성될 위험이 있다.readResolve()를 사용하면 기존 인스턴스를 반환할 수 있지만, 완전한 해결책은 아니다.열거 타입(Enum) 기반 싱글턴의 장점
Enum은 기본적으로 직렬화가 안전하다.Enum을 사용하는 것이 가장 안전한 방법이다.1. 직렬화 가능한 싱글턴 클래스의 문제점
기본적인 싱글턴 구현 (문제 발생 가능)
readResolve()를 사용했지만 역직렬화 중 새로운 객체가 생성되는 문제는 여전히 존재한다.2. ElvisStealer 공격 기법
싱글턴을 훔치는 도둑 클래스
ElvisStealer를 이용하면,readResolve()실행 이전의 인스턴스 참조를 가로챌 수 있다.3. 문제해결책
1️⃣
Enum싱글턴 (가장 안전한 방법)Enum을 사용하면 역직렬화 공격과 리플렉션 공격을 모두 차단할 수 있다.Enum을 직렬화할 때 인스턴스가 하나만 유지되도록 보장한다.2️⃣
transient키워드 사용Topics
transientvsstatic1️⃣
transientvsstatic차이점transientstatic2️⃣
transientvsstatic예제 비교(1)
transient사용 예제결과 정리
favoriteSongs필드는transient이므로 직렬화되지 않고, 역직렬화 후null이 된다.readObject()를 통해 필드를 다시 초기화했기 때문에 안전하게 값을 복구할 수 있다.(2)
static사용 예제결과 정리
static필드는 애초에 직렬화되지 않는다.readObject()를 사용하지 않아도 값이 그대로 유지되므로 객체의 상태와 무관하게 클래스의 공유 데이터로 동작한다.총 정리
transient을 사용해야 한다.static을 사용하면 된다.transient를 사용해서 직렬화되지 않도록 하는 것이 좋다.💡 핵심 정리
readResolve()를 사용하는 것은 완벽한 해결책이 아니다.readResolve()이전에 임시로 생성될 수 있어, 공격 가능성이 존재한다.Enum)을 사용하여 싱글턴을 구현하는 것이 가장 안전하다. (또는transient키워드 사용)