Java
[Effective Java] 12-1. 직렬화
noahkim_
2025. 1. 3. 03:41
조슈아 블로크 님의 "Effective Java" 책을 정리한 포스팅 입니다.
1. 자바 직렬화의 대안을 찾으라
자바 직렬화
- 객체의 상태를 바이트 스트림으로 변환하여 저장하거나 전송할 수 있게 하는 기술
- 역직렬화를 통해 객체로 복원 가능
위험성
위험 유형 | 설명 |
보안 |
신뢰할 수 없는 스트림을 역직렬화하면 원격 코드 실행 등 공격에 노출됨
|
성능 |
대용량 데이터를 역직렬화할 경우 서버 리소스를 과도하게 소모하여 DDoS에 악용될 수 있음
|
대안
대안 | 특징 | 주요 용도 |
JSON | 텍스트 기반, 사람이 읽을 수 있음 | 브라우저 ↔ 서버 통신 |
Protocol Buffer | 이진 기반, 성능 우수, 스키마 기반 | 서버 ↔ 서버 통신 |
예) JSON
더보기
public class JsonExample {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
// 객체를 JSON으로 직렬화
Person person = new Person("Noah", 30);
String jsonString = mapper.writeValueAsString(person);
System.out.println(jsonString); // {"name":"Noah","age":30}
// JSON을 객체로 역직렬화
Person deserializedPerson = mapper.readValue(jsonString, Person.class);
System.out.println(deserializedPerson);
}
}
class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
예) Protocol Buffer
더보기
Person person = Person.newBuilder().setName("Noah").setAge(30).build();
// 객체를 바이트 배열로 직렬화
byte[] serializedData = person.toByteArray();
// 바이트 배열을 객체로 역직렬화
Person deserializedPerson = Person.parseFrom(serializedData);
System.out.println(deserializedPerson.getName()); // Noah
역직렬화 필터링
항목 | 설명 |
화이트리스트 |
역직렬화 시 허용된 클래스만 처리하도록 화이트리스트 기반 필터링 적용
|
SWaT |
직렬화와 관련된 위험 요소를 분석하고 문제를 탐지 및 차단하는 보안 도구 (Static Web Analysis Tool)
|
2. Serializable을 구현할지는 신중히 결정하라
Serializable
- 클래스 직렬화 시 사용하는 마커 인터페이스
- 직렬화 지원을 하는데 간단해 보이지만, 매우 값비싼 일임
고려 사항
고려사항 | 설명 |
릴리즈 후 수정 어려움 |
직렬화된 데이터는 클래스 구조에 강하게 의존
→ 구조 변경 시 기존 데이터와 호환성 문제 발생 |
보안 취약 가능성 |
신뢰할 수 없는 입력 스트림을 역직렬화할 경우 원격 코드 실행 등의 보안 구멍 발생 가능
|
테스트 복잡성 증가 |
클래스 구조가 바뀔 때마다 직렬화 호환성 테스트가 필요
→ 테스트 범위와 난이도 증가 |
주의 사항
주의사항 | 설명 |
상속용 클래스 직렬화 금지 |
상속을 위한 클래스에 직렬화 구현 시, 하위 클래스까지 직렬화 부담이 전가됨
→ 하위 클래스가 호환성을 유지해야 하므로 설계가 제한됨 |
내부 클래스 직렬화 금지 |
내부 클래스는 외부 클래스의 참조를 포함함
→ 직렬화가 어렵고 비효율적임 |
3. 커스텀 직렬화 형태를 고려해보라
기본 직렬화 형태의 문제점
문제점 구분 | 설명 |
설계 유연성 저하 |
기본 직렬화는 객체의 모든 필드를 직렬화함
→ 공개 API가 내부 구조에 영구히 묶임 → 클래스 설계 변경이 어려움 |
호환성 문제 |
클래스 구조 변경 시, 이전 버전에서 직렬화된 데이터와 호환되지 않음
|
비효율적인 공간 사용 |
불필요한 내부 필드까지 직렬화되어 너무 많은 저장 공간을 차지할 수 있음
|
성능 저하 |
모든 필드를 탐색하면서 직렬화하므로 시간이 오래 걸림
|
스택 오버플로우 가능성 |
객체 그래프에 순환 참조가 있을 경우 무한 루프에 빠져 스택 오버플로우 발생 가능
|
고려 사항
항목 | 설명 |
writeObject() |
사용자가 원하는 방식으로 직렬화하는 메서드
→ 성능 및 보안 측면에서 유리 |
readObject() |
사용자가 원하는 방식으로 역직렬화하는 메서드
→ 불변식(invariant) 보장 및 보안 검사에 사용 |
transient |
직렬화에서 제외할 필드를 표시하는 키워드
→ 객체의 논리적 상태와 무관한 필드에만 사용 권장 |
serialVersionUID | 클래스의 직렬화 버전을 식별하는 고유 ID – 명시적으로 설정 시 자동 생성보다 효율적 – 호환성 유지에 중요 |
synchronized |
직렬화 중 객체 상태 일관성 유지를 위해 읽기 메서드에 동기화 적용
|
예) writeObject()
더보기
public class CustomExample implements Serializable {
private transient String tempData; // 직렬화 제외
private String permanentData;
private void writeObject(ObjectOutputStream out) throws Exception {
out.defaultWriteObject(); // 기본 직렬화
out.writeObject(tempData != null ? tempData.toUpperCase() : null); // 사용자 정의 직렬화
}
}
예) readObject
더보기
public class SafeExample implements Serializable {
private String data;
private void readObject(ObjectInputStream in) throws Exception {
in.defaultReadObject(); // 기본 역직렬화
if (data == null || data.isEmpty()) {
throw new IllegalArgumentException("Invalid data"); // 불변식 보장
}
}
}