87. 커스텀 직렬화 형태를 고려해보라

0. 시작 하기 전, Serializable 핵심 요약

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#f0f8ff', 'primaryTextColor': '#000', 'primaryBorderColor': '#7570b3', 'lineColor': '#333', 'edgeLabelBackground':'#fff', 'clusterBkg': '#fcfcfc'}}}%%
graph LR
    subgraph 실행 과정
        O[MyClass 객체] -- 직렬화 (객체 -> 바이트) --> BYTES[바이트 스트림];
        BYTES -- 저장 --> Storage[(파일/DB)];
        BYTES -- 전송 --> Network((네트워크));
        Storage -- 로딩 --> BYTES_IN[바이트 스트림];
        Network -- 수신 --> BYTES_IN;
        BYTES_IN -- 역직렬화 (바이트 -> 객체) --> O2[MyClass 객체 복원];
    end

    subgraph 클래스 정의
        A[MyClass] -- 구현 --> B(Serializable);
    end

 style B fill:#ccf,stroke:#333,stroke-width:2px

핵심 포인트

  • 정체:

    • 자바의 '표식' 인터페이스 (java.io.Serializable)

    • 구현할 메소드는 없음

  • 목적: 특정 클래스의 객체를 저장(파일/DB) 하거나 전송(네트워크) 가능하게 만듦.

  • 작동 방식:

    • 직렬화(Serialization): 객체의 현재 상태(데이터) -> 바이트 스트림(byte stream) 변환

    • 역직렬화(Deserialization): 바이트 스트림 -> 원래 객체 상태로 복원

  • 사용법: 클래스 선언부에 implements Serializable 추가

  • 주요 용도:

    • 데이터 영속화: 프로그램 종료 후에도 데이터 보존

      • (예: 게임 저장)

    • 네트워크 통신: 원격 시스템 간 객체 전달

      • (예: 채팅 메시지 객체 전송)

    • 캐싱: 자주 쓰는 객체를 저장해두고 빠르게 재사용

  • serialVersionUID: 클래스 버전 관리용 고유 번호. 직렬화/역직렬화 시 이 번호가 일치하는지 확인하여 호환성 체크 (매우 중요)

  • 주의: 그냥 implements Serializable만 쓰면(기본 직렬화) 나중에 클래스 내부를 수정했을 때 저장된 객체를 못 읽는 문제가 생길 수 있음 (유지보수 어려움)

  • 대안: JSON, XML, Protocol Buffers 등 다른 데이터 저장/전송 방식도 많이 사용됨.


1. 왜 기본 직렬화는 위험할 수 있는가? 🤔

핵심 문제: 기본 직렬화는 객체의 논리적 데이터가 아닌, 물리적 표현을 그대로 저장.

발생 가능한 문제점들

1. API 고착화

  • 클래스 내부 구현(private 필드, 내부 클래스 구조 등)이 직렬화 형태에 포함되어 공개 API처럼 되어버림

  • 향후 내부 리팩토링이 매우 어려워짐 (StringListEntry 클래스 예시)

2. 성능 저하 (공간)

  • 불필요한 내부 데이터(예: 연결 리스트의 포인터)까지 저장 -> 비효율적 직렬화된 크기

3. 성능 저하 (시간)

  • 복잡한 객체 그래프를 그대로 탐색 -> 직렬화/역직렬화 속도 저하

4. StackOverflowError 위험

  • 객체 그래프가 깊거나 크면 재귀적인 직렬화 과정에서 스택 오버플로 발생 가능

5. 정확성 문제

  • 객체의 불변식이 내부 구현 방식에 의존하는 경우, 기본 직렬화/역직렬화 후 객체가 비정상 상태 유발

    • 예: Hashtable의 해시 버킷 위치

기억하세요

Serializable을 구현하고 기본 직렬화를 사용하면, 그 클래스의 현재 내부 구현영원히 묶일 수 있습니다.


2. 그렇다면, 언제 기본 직렬화를 써도 괜찮을까? ✅

황금률: 객체의 논리적 내용물리적 표현(필드)거의 동일할 때만 사용을 고려합니다.

  • 예시: Name 클래스 (lastName, firstName, middleName 필드가 곧 논리적 데이터)

주의: 기본 직렬화가 적합해 보여도, 역직렬화 시 데이터 유효성 검사불변식 보장을 위해 readObject 메서드가 필요할 수 있습니다.

  • 예: Name 필드가 null이 아님을 보장


3. 해결책: 커스텀 직렬화 (writeObject/readObject) 💡

목표: 객체의 논리적인 데이터만 효율적으로 저장하고 복원

핵심 도구:

transient 키워드:

  • 기본 직렬화 과정에서 제외할 필드에 표시

  • 언제 사용?

    • 다른 필드에서 유도되는 값 (캐시 등)

    • 논리적 상태와 무관한 필드 (네이티브 리소스 포인터 등)

    • 커스텀 직렬화 시 저장하지 않을 내부 구현 필드

private void writeObject(ObjectOutputStream out) throws IOException;

  • 객체를 어떻게 스트림에 쓸지 직접 정의

  • 필수: out.defaultWriteObject();먼저 호출 (미래 호환성 위해)

  • 논리적 데이터만 순서대로 out.writeInt(), out.writeObject() 등으로 저장

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;

  • 스트림에서 데이터를 읽어 객체를 어떻게 복원할지 직접 정의

  • 필수: in.defaultReadObject();먼저 호출 (미래 호환성 위해)

  • writeObject에서 저장한 순서대로 데이터를 읽어 객체 상태를 재구성

  • transient 필드나 계산된 필드를 여기서 초기화/복원해야 할 수 있습니다.

개념 예시 (StringList):

장점: 유연성 확보, 성능(공간/시간) 향상, 안정성 증가(스택 오버플로 방지)


4. 직렬화 시 반드시 지켜야 할 것들! 📌

1. serialVersionUID를 명시적으로 선언하세요!

왜?

  • 호환성 문제 예방 (클래스 변경 시 UID 자동 계산 변경 방지), 약간의 성능 향상

어떻게?

  • private static final long serialVersionUID = <고유한 long 값>;

  • 새 클래스는 아무 값, 기존 클래스는 serialver로 확인한 이전 값 사용 권장

언제 변경?

  • 오직 구버전과의 호환성을 의도적으로 끊고 싶을 때만! (이 외에는 절대 변경 금지)

2. transient 필드 초기화를 신경 쓰세요.

  • 역직렬화 시 transient 필드는 기본값(null, 0, false)으로 시작

  • readObject에서 직접 값을 설정하거나, 지연 초기화(Lazy Initialization)를 사용

3. 동기화를 고려하세요.

  • 클래스가 스레드 안전성을 위해 synchronized 등을 사용한다면, writeObject 메서드도 반드시 동일한 동기화 전략

    • 예: synchronized 키워드 추가

    • 데이터 일관성 및 교착 상태 방지를 위함

4. 직렬화 형태를 문서화하세요.

  • 기본 직렬화 필드: @serial Javadoc 태그 사용

  • 커스텀 직렬화 형식/메서드: @serialData Javadoc 태그 사용. (어떤 순서로 무엇을 저장/읽는지 설명)


5. 핵심 정리 ✨

  • 클래스를 Serializable로 만들기로 했다면, 어떤 직렬화 형태를 사용할지 신중하게 설계해야

  • 기본 직렬화는 객체의 논리적/물리적 표현이 거의 일치하는 매우 단순한 경우에만 고려

  • 대부분의 경우, 커스텀 직렬화(writeObject/readObject) 가 유연성, 성능, 안정성 면에서 더 나은 선택

  • 직렬화 형태는 클래스의 공개 API와 같고, 한번 배포되면 변경하기 매우 어려우니, 설계에 충분한 시간을 투자

Last updated