직렬화 (Serialization)
한눈에 보기
직렬화는 객체를 바이트 스트림으로 변환해 저장과 전송을 가능하게 하는 기술이다. serialVersionUID와 transient, 역직렬화 보안 취약점, JSON 등 대안 포맷과의 비교를 정리한다.
최종 업데이트 2026.09.17
직렬화(Serialization)는 메모리에 있는 객체를 바이트 스트림으로 변환해 파일 저장이나 네트워크 전송이 가능하게 만드는 기술이고, 역직렬화(Deserialization)는 그 바이트 스트림을 다시 객체로 복원하는 과정이다. 자바는 Serializable 인터페이스와 ObjectOutputStream, ObjectInputStream으로 언어 차원의 직렬화를 제공하지만, 버전 관리의 어려움과 심각한 보안 취약점 때문에 현대 실무에서는 JSON 같은 명시적 포맷이 우선 선택된다. 그래도 세션 클러스터링, 캐시, RMI 등에서 여전히 쓰이므로 동작 원리와 위험성을 정확히 알아야 한다.
기본 사용법과 동작 원리
자바 직렬화의 대상이 되려면 클래스가 마커 인터페이스(marker interface)인 Serializable을 구현해야 한다. 메서드가 하나도 없는 이 인터페이스는 직렬화를 허용한다는 표시일 뿐이고, 실제 변환은 JVM이 리플렉션(reflection)으로 객체의 필드를 순회하며 수행한다. 참조로 연결된 객체들도 함께 직렬화되므로, 객체 그래프 전체의 모든 구성 요소가 Serializable이어야 하며 아니면 NotSerializableException이 발생한다.
public class Member implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private transient String password; // 직렬화에서 제외
private int age;
}
// 직렬화
try (ObjectOutputStream out =
new ObjectOutputStream(new FileOutputStream("member.ser"))) {
out.writeObject(new Member("kim", "secret", 20));
}
// 역직렬화, 생성자를 거치지 않고 객체가 복원된다
try (ObjectInputStream in =
new ObjectInputStream(new FileInputStream("member.ser"))) {
Member m = (Member) in.readObject();
}
중요한 특징은 역직렬화가 생성자를 호출하지 않는다는 점이다. JVM이 메모리를 할당하고 스트림의 값으로 필드를 직접 채우므로, 생성자에 넣어 둔 검증 로직이 전부 우회된다. 이것이 불변식(invariant) 훼손과 보안 문제의 근원이다.
serialVersionUID와 transient
serialVersionUID는 클래스 버전을 식별하는 값으로, 역직렬화 시 스트림에 기록된 값과 현재 클래스의 값이 다르면 InvalidClassException이 발생한다. 명시하지 않으면 컴파일러가 클래스 구조를 해싱해 자동 생성하는데, 필드나 메서드가 조금만 바뀌어도 값이 달라져 예전에 저장한 데이터를 읽지 못하게 된다. 그래서 직렬화 가능 클래스에는 serialVersionUID를 직접 선언하고, 호환이 깨지는 변경을 할 때만 의도적으로 값을 올리는 것이 원칙이다.
transient 키워드가 붙은 필드는 직렬화에서 제외되며 역직렬화 후 타입 기본값(참조형은 null)으로 채워진다. 비밀번호 같은 민감 정보, 소켓이나 스레드처럼 직렬화가 무의미한 자원, 다시 계산 가능한 캐시 필드에 사용한다. static 필드는 인스턴스 상태가 아니므로 애초에 직렬화되지 않는다. 더 세밀한 제어가 필요하면 private 메서드 writeObject와 readObject를 정의해 기본 직렬화를 보강하거나, readObject 안에서 검증과 방어적 복사를 수행할 수 있다.
역직렬화 보안 문제
자바 직렬화의 가장 큰 문제는 신뢰할 수 없는 데이터의 역직렬화가 원격 코드 실행(remote code execution)으로 이어질 수 있다는 것이다. ObjectInputStream은 스트림에 적힌 클래스라면 클래스패스에 있는 무엇이든 인스턴스화하며, 공격자는 readObject 실행 중 연쇄적으로 호출되는 클래스들을 조합한 가젯 체인(gadget chain)으로 임의 코드를 실행시킬 수 있다. 널리 쓰이던 라이브러리를 매개로 한 대형 취약점 사례가 실제로 여러 번 있었다.
방어책으로 Java 9부터 역직렬화 필터(ObjectInputFilter)가 도입되어 허용 클래스 화이트리스트, 그래프 깊이와 크기 제한을 걸 수 있다. 그러나 근본 원칙은 신뢰 경계를 넘어온 데이터에 자바 직렬화를 쓰지 않는 것이다. 자바 아키텍트들 역시 기본 직렬화를 설계 실수로 인정하고 장기적으로 대체하는 방향을 밝혀 왔다.
대안 포맷과 비교
| 구분 | 자바 직렬화 | JSON (Jackson 등) | 바이너리 스키마 (Protobuf 등) |
|---|---|---|---|
| 가독성 | 없음, 자바 전용 바이너리 | 사람이 읽을 수 있음 | 없음, 스키마 필요 |
| 언어 호환 | 자바끼리만 | 언어 중립 | 언어 중립 |
| 버전 관리 | serialVersionUID, 취약함 | 필드 기반, 유연함 | 필드 번호 기반, 강력함 |
| 보안 | 가젯 체인 위험 큼 | 상대적으로 안전 | 상대적으로 안전 |
| 주 용도 | 세션 복제, 캐시, RMI | REST API | 고성능 내부 통신 |
실무 지침을 정리하면 이렇다. 외부와의 데이터 교환에는 JSON이나 스키마 기반 바이너리 포맷을 쓴다. 자바 직렬화를 불가피하게 쓴다면 serialVersionUID를 명시하고, 역직렬화 필터를 적용하고, readObject에서 불변식을 재검증한다. 참고로 레코드(Record)는 역직렬화 시에도 표준 생성자를 통과하도록 설계되어 검증 우회 문제가 없으므로, 직렬화 가능한 값 객체가 필요하다면 레코드가 더 안전한 선택이다.
면접에서 이렇게 물어본다
- 직렬화와 역직렬화가 무엇이고 자바에서 어떤 클래스와 인터페이스로 수행하는지 설명해 보라.
- serialVersionUID를 명시하지 않으면 어떤 문제가 생기는가.
- transient 키워드의 역할과 사용 예를 들어 보라.
- 역직렬화가 보안 취약점이 되는 원리와 방어 방법을 설명해 보라.
- 실무에서 자바 기본 직렬화 대신 JSON을 쓰는 이유를 비교 관점에서 설명해 보라.
Java에서 같이 읽기
- 오토 박싱과 오토 언박싱 (Auto Boxing and Unboxing)오토 박싱은 기본형 값을 래퍼 클래스 객체로, 오토 언박싱은 그 반대로 자동 변환하는 기능이다. Integer 캐시, == 비교 함정, NullPointerException 위험과 성능 비용을 JVM 메모리 관점에서 정리한다.
- Call by Value와 Call by Reference자바는 기본형이든 참조형이든 언제나 값에 의한 호출만 사용하며, 참조형은 객체가 아니라 참조값이 복사되어 전달된다. swap 실패 예제와 스택 프레임 관점으로 그 근거를 설명한다.
- 캐스팅 (Casting), 업캐스팅과 다운캐스팅캐스팅은 값이나 참조를 다른 타입으로 취급하는 변환이다. 기본형 확대와 축소 변환, 업캐스팅과 다형성, 다운캐스팅과 instanceof 패턴 매칭, ClassCastException 예방법을 다룬다.
- 자바 컴파일 과정 (Java Compile Process)javac가 소스를 바이트코드로 바꾸고 JVM이 로딩, 링킹, 초기화 후 인터프리터와 JIT로 실행하는 전체 흐름을 정리한다. 클래스 파일 구조와 부모 위임 모델, 계층형 컴파일까지 다룬다.
이 개념, 내 코드에서는 어떻게 물어볼까요
데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.
공고 넣고 시작하기