1. 이진 직렬화 보안
Java에서의 직렬화는 단순히 객체의 필드를 저장하는 것이 아닙니다. Serializable 인터페이스만 구현했다면, 어떤 내용이든 가진 객체를 “복원”할 수 있는 기능입니다. 편리해 보일 수 있습니다! 하지만 애플리케이션이 신뢰할 수 없는 소스(예: 네트워크나 공격자가 바꿔치기했을 수 있는 파일)에서 받은 데이터를 역직렬화한다면, 공격의 희생양이 될 수 있습니다.
어떻게 동작하나요?
역직렬화 동안 Java는 클래스의 생성자를 호출하지 않고 바이트 스트림을 기반으로 객체를 생성합니다. 클래스에 readObject 같은 특수 메서드가 구현되어 있으면 자동으로 호출됩니다. 공격자는 역직렬화 시 취약한 코드가 실행되도록 바이트 스트림을 “구성”할 수 있습니다.
예: “가젯 체인” 공격
역직렬화 시 외부 명령을 실행하는(파일을 읽거나 셸을 호출하는) 클래스가 있다고 가정해 봅시다. 애플리케이션이 특정 타입의 객체를 역직렬화한다는 사실을 공격자가 알고 있다면, 악성 코드 실행으로 이어지는 특수 바이트 스트림을 주입할 수 있습니다. 이러한 공격은 위험한 동작으로 끝나는 호출의 연쇄, 즉 “가젯 체인”으로 구성됩니다.
왜 이렇게 치명적일까요?
역직렬화는 데이터 스트림에서 실제 객체를 복원하는 과정이며, 이 순간 임의의 코드가 실행될 수 있습니다. 데이터가 신뢰할 수 없는 소스에서 온 것이라면 원격 명령 실행(RCE)의 문을 여는 셈입니다. 주요 기업들은 안전하지 않은 역직렬화를 지양하라는 권고를 발표했고, 현대의 엔터프라이즈 프로젝트에서는 이진 직렬화를 보안 정책으로 금지하는 경우가 많습니다.
어떻게 방어할 수 있나요?
- 신뢰할 수 없는 소스에서 온 객체는 절대 역직렬화하지 마세요.
- 허용 목록(whitelisting)을 사용하여 역직렬화 가능한 타입을 명시적으로 제한하세요.
- 외부 연동에는 텍스트 기반 포맷을 선호하세요: JSON, XML.
- 직렬화가 불가피하다면 안전한 역직렬화 설정을 제공하는 라이브러리를 사용하세요(예: 타입 제한을 적용한 Jackson).
- readObject, readResolve 등 비표준 직렬화 메서드는 안전성이 확실하지 않다면 사용을 제한하세요.
2. 클래스 버전 호환성
Java의 이진 직렬화는 클래스의 구조에 강하게 묶여 있습니다. 버전 1.0에서 객체를 직렬화한 뒤, 클래스(필드 추가/삭제)를 수정했다면, “예전” 객체를 새 버전으로 역직렬화할 때 오류가 발생하거나 데이터가 손실될 수 있습니다.
Java는 호환성을 어떻게 판단하나요?
이를 위해 serialVersionUID라는 특수 필드를 사용합니다. 이는 클래스의 버전 식별자입니다. 직렬화된 객체의 serialVersionUID와 현재 클래스의 값이 다르면 InvalidClassException이 발생하며 역직렬화가 진행되지 않습니다.
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L; // 버전을 명시적으로 지정
private String name;
private int age;
}
클래스 구조를 변경(예: email 필드 추가)하고 serialVersionUID를 바꾸지 않으면, Java는 해당 클래스가 호환된다고 판단하여 예전 객체를 역직렬화하려 시도합니다. 반대로 serialVersionUID를 명시하지 않았다면 JVM이 구조를 기반으로 자동 생성하므로, 사소한 변경도 비호환을 초래합니다.
버전이 일치하지 않을 때/일치할 때 무슨 일이 일어나나요?
식별자가 일치하지 않으면 역직렬화는 진행되지 않습니다: InvalidClassException. 일치하면 필드는 이름과 타입으로 매핑됩니다. 새 필드는 기본값(null, 0)을 받고, 제거된 필드는 무시됩니다. 필드의 타입이나 이름이 바뀌면 오류가 발생하거나 데이터가 잘못 해석될 수 있습니다.
실용 팁. 직렬화 가능한 클래스에는 항상 명시적으로 serialVersionUID를 지정하세요. 호환되지 않는 변경(중요 필드 삭제/타입 변경)에서만 값을 변경하세요. 새 필드를 추가하는 경우에는 식별자를 그대로 두어도 JVM이 예전 객체를 올바르게 처리합니다.
표: 클래스가 변경되면 어떻게 되나요
| 클래스 변경 | 역직렬화 시 결과 |
|---|---|
| 새 필드 추가 | 기본값을 받음(0, null) |
| 필드 삭제 | 예전 데이터 읽기 시 무시됨 |
| 필드 타입 변경 | 예외 발생 또는 잘못된 데이터 |
| 필드 이름 변경 | 예전 필드는 무시, 새 필드는 기본값 |
| serialVersionUID 변경 | InvalidClassException 예외 |
3. 표준 직렬화의 제약사항
모든 객체를 직렬화할 수 있는 것은 아니다
transient 및 static 한정자가 붙은 필드는 직렬화되지 않습니다. static은 객체가 아니라 클래스에 속하기 때문이고, transient는 해당 필드를 직렬화하지 않도록 명시적으로 금지했기 때문입니다.
일부 객체는 정의상 직렬화할 수 없습니다: Thread, DB 연결, 소켓, Scanner 등. 클래스에 이런 타입의 필드가 있고 그것이 transient가 아니라면 NotSerializableException이 발생합니다.
import java.io.Serializable;
import java.util.Scanner;
public class Session implements Serializable {
private transient Scanner scanner; // 직렬화되지 않음!
private String login;
}
성능 및 확장성 문제
거대한 객체 그래프의 직렬화는 느리고 메모리 요구량이 높을 수 있습니다.
이진 포맷은 다른 플랫폼과 언어와의 통합에 적합하지 않습니다 — 오직 Java만 “이해”합니다.
특히 깊은 상속 계층과 순환 참조가 있을 때 무엇이 직렬화되는지 제어하기가 어렵습니다.
기존 데이터 유지보수 문제
이진 스냅샷의 장기 보관은 리스크입니다. 1~2년이 지나 클래스 구조가 바뀌면 예전 파일을 더 이상 불러올 수 없게 됩니다.
실제 사례: “우리는 3년 전에 사용자 캐시를 직렬화해 저장했고, 애플리케이션을 업데이트했더니 이제는 불러올 수 없습니다. 데이터야, 안녕!”
4. 모범 사례: 함정에 빠지지 않는 방법
- 양쪽을 모두 통제할 수 있는 내부 용도에만 이진 직렬화를 사용하세요.
- 외부 통합이나 중요한 데이터의 장기 보관에는 이진 직렬화를 사용하지 마세요.
- 직렬화 가능한 클래스에는 항상 serialVersionUID를 명시하세요.
- 직렬화되면 안 되는 필드에는 transient 한정자를 사용하세요.
- 외부 시스템과의 교환에는 텍스트 포맷과 현대 라이브러리: JSON, XML, Jackson, Gson, JAXB.
- 호환성을 위해 버저닝을 사용하세요: 클래스 자체에 버전 정보를 저장하고 역직렬화 시 처리 로직을 조정하세요.
- 직렬화가 캐시 용도라면 — 어떤 수단으로든 호환성을 유지하려고 애쓰지 마세요. 캐시는 다시 계산하면 됩니다.
- 직렬화 가능한 객체에 민감 정보(비밀번호, 키)를 저장하지 마세요 — 직렬화는 데이터를 암호화하지 않습니다.
5. 이진 직렬화 작업 시 흔한 실수
오류 №1: 신뢰할 수 없는 소스의 데이터를 역직렬화함. 가장 위험한 실수는 외부(네트워크, 사용자 입력, 바꿔치기된 파일)에서 들어온 객체를 받아서 역직렬화하는 것입니다. 이는 RCE까지 이어질 수 있는 취약점으로 곧장 연결됩니다.
오류 №2: serialVersionUID를 업데이트하지 않은 채 클래스 구조를 암묵적으로 변경함. 식별자를 명시하지 않으면 JVM이 자동으로 생성합니다. 구조가 조금만 바뀌어도(필드 순서 변경 포함) 비호환이 발생하여 예전 객체를 불러올 수 없게 됩니다.
오류 №3: 직렬화할 수 없는 필드를 가진 객체를 직렬화하려 함. 클래스에 Serializable을 구현하지 않은 타입의 필드가 있고 그것이 transient가 아니라면, 직렬화는 예외로 종료됩니다.
오류 №4: 직렬화 가능한 객체에 임시 데이터나 민감 정보를 저장함. 토큰, 비밀번호, 자원의 임시 디스크립터 등이 파일에 실수로 포함될 수 있습니다.
오류 №5: 장기 보관과 버전 간 교환에 이진 직렬화를 사용함. 클래스를 한 번만 업데이트해도 “깨진” 데이터와 호환성 문제를 겪을 가능성이 큽니다.
오류 №6: static과 transient 필드가 역직렬화 후 “복원”될 것이라고 기대함. 이 필드들은 직렬화되지 않으며, 로드 후에는 기본값을 갖습니다.
GO TO FULL VERSION