✅ Serializable을 구현한 클래스는 생성자 외의 방법으로도 인스턴스가 생성될 수 있어 취약점이 발생할 수 있음
✅ 직렬화 프록시 패턴은 객체의 직렬화를 위임하여 보안 및 견고성 문제를 해결할 수 있음
✅ 불변식이 복잡한 객체나 불변 클래스에 특히 유용한 방법
✅ 성능상 비용이 있지만 보안과 견고성이 중요한 클래스에 적극 검토할 가치가 있음
📚 필수 개념 정리
🧩 직렬화와 취약점
직렬화(Serialization)는 자바 객체를 바이트 스트림으로 변환하는 과정이며, 이는 객체를 파일에 저장하거나 네트워크로 전송할 때 유용하다.
하지만 직렬화는 일반 생성자를 우회하는 객체 생성 방법을 제공하므로 여러 취약점이 발생할 수 있다.
🔄 직렬화 취약점 예시
// 불변식을 지켜야 하는 기간(Period) 클래스publicfinalclassPeriodimplementsSerializable{privatefinalDate start;privatefinalDate end;/** * @paramstart 시작 시각 * @paramend 종료 시각 * @throwsIllegalArgumentException 시작 시각이 종료 시각보다 늦을 때 발생*/publicPeriod(Datestart,Dateend){this.start=newDate(start.getTime());this.end=newDate(end.getTime());if(this.start.compareTo(this.end)>0)thrownewIllegalArgumentException("시작 시각이 종료 시각보다 늦습니다.");}publicDatestart(){returnnewDate(start.getTime());}publicDateend(){returnnewDate(end.getTime());} // 나머지 코드 생략}
👇 이 클래스를 직렬화했다가 역직렬화하면 어떻게 될까요?
👉 왜 이런 문제가 발생할까?
🚨 직렬화의 위험성
역직렬화는 생성자를 우회하는 인스턴스 생성 방법
바이트 스트림을 직접 조작하면 클래스의 불변식을 깨뜨릴 수 있음
객체의 내부 구현이 변경되면 이전 버전과의 호환성 문제 발생 가능
🔍 직렬화 프록시 패턴(Serialization Proxy Pattern)
💡 직렬화 프록시 패턴이란?
직렬화 프록시 패턴은 쉽게 말해 "진짜 객체 대신 가짜(프록시)를 직렬화하자"는 아이디어다.
직접 객체를 직렬화하면 위험하니, 그 객체의 데이터만 담은 간단한 도우미 클래스를 만들어서 그것을 대신 직렬화하는 것이다.
🎭 비유하자면: 중요한 상품(객체)을 그대로 배송(직렬화)하는 대신, 상품의 설계도(프록시)만 배송하고 도착지(역직렬화)에서 그 설계도로 상품을 새로 만드는 것과 같다.
💫 작동 원리:
원본 객체 안에 작은 내부 클래스(프록시)를 만든다
직렬화할 때는 원본 대신 이 프록시가 대신 나간다
역직렬화할 때는 프록시가 원본 객체를 정상적인 방법으로 새로 만들어 준다
✨ 직렬화 프록시 패턴의 구현 단계
대리인 만들기: 원본 클래스 안에 내부 클래스로 프록시를 만듭니다
대리 배송 준비: writeReplace 메서드를 만들어서 "직렬화할 때는 나 대신 내 프록시를 내보내라"고 지시합니다
직접 배송 차단: readObject 메서드를 만들어 "누군가 프록시 없이 직접 나를 역직렬화하려 하면 오류를 발생시켜라"고 지시합니다
복원 지시서 첨부: 프록시 안에 readResolve 메서드를 만들어 "역직렬화될 때 원본 객체를 어떻게 다시 만들어야 하는지" 알려줍니다
// 직렬화로 불변식을 깨뜨리는 공격
public static void main(String[] args) {
Period p = new Period(new Date(), new Date());
byte[] serialized = serialize(p);
// 직렬화된 바이트 스트림을 수정하여 불변식을 깨뜨림
// (바이트 스트림에서 날짜 필드 값 조작)
Period p2 = (Period) deserialize(serialized);
// p2는 시작 시각이 종료 시각보다 늦는 잘못된 상태!
}
// Period 클래스에 직렬화 프록시 패턴 적용
public final class Period implements Serializable {
private final Date start;
private final Date end;
// 생성자 및 접근자 메서드는 동일
// Period 클래스의 직렬화 프록시
private static class SerializationProxy implements Serializable {
private final Date start;
private final Date end;
// 직렬화 프록시 생성자
SerializationProxy(Period p) {
this.start = p.start;
this.end = p.end;
}
private static final long serialVersionUID = 234098243823485285L; // 임의의 번호
// 역직렬화시 호출되어 바깥 클래스 인스턴스 생성
private Object readResolve() {
return new Period(start, end); // 정상적인 public 생성자 사용
}
}
// 직렬화 시 SerializationProxy 인스턴스로 대체
private Object writeReplace() {
return new SerializationProxy(this);
}
// 불법 역직렬화 시도 방어
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
throw new InvalidObjectException("프록시가 필요합니다");
}
}