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"); // 불변식 보장
        }
    }
}