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

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

```mermaid
%%{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` 추가

  ```java
  public class MyData implements Serializable {
      // ... 클래스 내용 ...
  }
  ```
* **주요 용도:**
  * 데이터 **영속화**: 프로그램 종료 후에도 데이터 보존
    * (예: 게임 저장)
  * **네트워크 통신**: 원격 시스템 간 객체 전달
    * (예: 채팅 메시지 객체 전송)
  * **캐싱**: 자주 쓰는 객체를 저장해두고 빠르게 재사용
* **`serialVersionUID`:** 클래스 버전 관리용 고유 번호. 직렬화/역직렬화 시 이 번호가 일치하는지 확인하여 호환성 체크 (매우 중요)
* **주의:** 그냥 `implements Serializable`만 쓰면(기본 직렬화) 나중에 클래스 내부를 수정했을 때 저장된 객체를 못 읽는 문제가 생길 수 있음 (유지보수 어려움)
* **대안:** JSON, XML, Protocol Buffers 등 다른 데이터 저장/전송 방식도 많이 사용됨.

\ <br>

***

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

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

<br>

```java
// 코드 87-2 기본 직렬화 형태에 접합하지 않은 클래스
public final class StringList implements Serializble {
    private int size = 0;
    private Entry head = null;

    private static class Entry implements Serialzible {
        String data;
        Entry next;
        Entry previous;
    }

    ...// 나머지 코드는 생략
}
```

```mermaid
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#f0f8ff', 'primaryTextColor': '#000', 'primaryBorderColor': '#7570b3', 'lineColor': '#333', 'edgeLabelBackground':'#fff', 'clusterBkg': '#fcfcfc'}}}%%

graph TD
    subgraph "논리적 표현 (WANT)"
        L1["StringList"]
        L2["'A', 'B', 'C'"]
        L1 --> L2
    end

    subgraph "물리적 표현 (Real)"

        P1["StringList 객체"]
        P2["Entry(data='A')"]
        P3["Entry(data='B')"]
        P4["Entry(data='C')"]
        P5["기타 내부 포인터..."]

        P1 -->|"head"| P2
        P2 -->|"next"| P3
        P3 -->|"next"| P4
        P4 -->|"next"| null
        P4 -->|"prev"| P3
        P3 -->|"prev"| P2
        P2 -->|"prev"| null
        P1 -->|"size=3"| P5
    end

    style L1 fill:#d0e0ff,stroke:#333
    style L2 fill:#d0e0ff,stroke:#333
    style P1 fill:#ffe0e0,stroke:#333
    style P2 fill:#ffe0e0,stroke:#333
    style P3 fill:#ffe0e0,stroke:#333
    style P4 fill:#ffe0e0,stroke:#333
    style P5 fill:#ffe0e0,stroke:#333
```

<br>

### **발생 가능한 문제점들**

```mermaid
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#f0f8ff', 'primaryTextColor': '#000', 'primaryBorderColor': '#7570b3', 'lineColor': '#333', 'edgeLabelBackground':'#fff', 'clusterBkg': '#fcfcfc'}}}%%
graph TD
    A["기본 직렬화<br>문제점"] --> B["API 고착화 ⛓️<br>(내부 구조 노출,<br>변경 어려움)"];
    A --> C["공간 낭비 💾<br>(불필요한<br>데이터 저장)"];
    A --> D["시간 낭비 ⏳<br>(느린<br>직렬화/역직렬화)"];
    A --> E["Stack Overflow 💥<br>(큰 객체 그래프<br>처리 시 오류)"];

    style A fill:#ffcccc,stroke:#a00,stroke-width:2px
    style B fill:#fff0f0,stroke:#a00
    style C fill:#fff0f0,stroke:#a00
    style D fill:#fff0f0,stroke:#a00
    style E fill:#fff0f0,stroke:#a00
```

\ <br>

### 1. **API 고착화**

* 클래스 내부 구현(private 필드, 내부 클래스 구조 등)이 직렬화 형태에 포함되어 공개 API처럼 되어버림
* 향후 내부 리팩토링이 매우 어려워짐 (`StringList`의 `Entry` 클래스 예시)

### 2. **성능 저하 (공간)**

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

### 3. **성능 저하 (시간)**

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

### 4. **StackOverflowError 위험**

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

### 5. **정확성 문제**

* 객체의 불변식이 내부 구현 방식에 의존하는 경우, 기본 직렬화/역직렬화 후 객체가 비정상 상태 유발
  * 예: Hashtable의 해시 버킷 위치

<br>

### **기억하세요**

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

\ <br>

***

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

<br>

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

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

```java
// 코드 87-1: 기본 직렬화 형태에 적합한 후보 (개념)
public class Name implements Serializable {
    private final String lastName;
    private final String firstName;
    private final String middleName;
    // ... 생성자, 메서드 등 ...
}
```

```mermaid
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#f0f8ff', 'primaryTextColor': '#000', 'primaryBorderColor': '#7570b3', 'lineColor': '#333', 'edgeLabelBackground':'#fff', 'clusterBkg': '#fcfcfc'}}}%%

graph LR
    subgraph "논리적 표현 (WANT)"
        L1["Name"]
        L2["lastName: '홍'<br/>firstName: '길동'<br/>middleName: null"]
        L1 --> L2
    end

    subgraph "물리적 표현 (REAL)"
        P1["Name 객체"]
        P2["lastName = '홍'"]
        P3["firstName = '길동'"]
        P4["middleName = null"]

        P1 --> P2
        P1 --> P3
        P1 --> P4
    end

    L2 -.->|거의 일치| P2
    L2 -.->|거의 일치| P3
    L2 -.->|거의 일치| P4

    style L1 fill:#d0e0ff,stroke:#333
    style L2 fill:#d0e0ff,stroke:#333
    style P1 fill:#d0e0ff,stroke:#333
    style P2 fill:#d0e0ff,stroke:#333
    style P3 fill:#d0e0ff,stroke:#333
    style P4 fill:#d0e0ff,stroke:#333
```

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

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

\ <br>

***

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

```mermaid
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#f0f8ff', 'primaryTextColor': '#000', 'primaryBorderColor': '#7570b3', 'lineColor': '#333', 'edgeLabelBackground':'#fff', 'clusterBkg': '#fcfcfc'}}}%%

flowchart TD
    subgraph "직렬화 (writeObject)"
        A[객체 상태] --> B["writeObject() 호출"]
        B --> C["defaultWriteObject()<br>(transient 아닌 필드 처리)"]
        C --> D["개발자가 직접<br>논리 데이터 추출/저장"]
        D --> E["스트림 출력"]

        F[("transient 필드<br>size, head...")] -. 무시됨 .-> C
    end

    subgraph "역직렬화 (readObject)"
        G["스트림 입력"] --> H["readObject() 호출"]
        H --> I["defaultReadObject()<br>(transient 아닌 필드 처리)"]
        I --> J["개발자가 직접<br>논리 데이터 읽기/객체 재구성"]
        J --> K["객체 완성"]

        L[("transient 필드<br>재계산/초기화")] -. 직접 설정 .-> J
    end

    E ~~~ G

    style A fill:#e0f0e0,stroke:#333
    style B fill:#e0f0e0,stroke:#333
    style C fill:#f0f0f0,stroke:#333
    style D fill:#ffe0e0,stroke:#333,stroke-width:2
    style E fill:#e0f0e0,stroke:#333
    style F fill:#ffe0e0,stroke:#333,stroke-dasharray: 5 5

    style G fill:#e0f0e0,stroke:#333
    style H fill:#e0f0e0,stroke:#333
    style I fill:#f0f0f0,stroke:#333
    style J fill:#ffe0e0,stroke:#333,stroke-width:2
    style K fill:#e0f0e0,stroke:#333
    style L fill:#ffe0e0,stroke:#333,stroke-dasharray: 5 5
```

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

### **핵심 도구:**

#### `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):**

```java
public final class StringList implements Serializable {
    private transient int size = 0;
    private transient Entry head = null;

    // Entry 클래스는 이제 Serializable 불필요

    /**
    * @serialData 리스트 크기(int) 후, 모든 문자열 원소(String) 순서대로 기록.
    */
    private void writeObject(ObjectOutputStream s) throws IOException {
        s.defaultWriteObject();
        s.writeInt(size);      // 논리적 데이터: 크기 저장
        for (Entry e = head; e != null; e = e.next)
            s.writeObject(e.data); // 논리적 데이터: 문자열 원소들 저장
    }

    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        int numElements = s.readInt(); // 크기 읽기
        for (int i = 0; i < numElements; i++)
            add((String) s.readObject()); // 원소 읽어서 리스트 재구성 (add 메서드 활용)
    }
    // ... add() 및 다른 메서드 생략 ...
}
```

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

\ <br>

***

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

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

```mermaid
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#f0f8ff', 'primaryTextColor': '#000', 'primaryBorderColor': '#7570b3', 'lineColor': '#333', 'edgeLabelBackground':'#fff', 'clusterBkg': '#fcfcfc'}}}%%
graph TD
    subgraph "시나리오3-UID 명시-호환 차단"
        C5["클래스 V1<br>(UID=123L)"] --> SD3["직렬화된 데이터 V1<br>(UID=123L 포함)"];
        C6["클래스 V3<br>(V1과 호환X,<br>UID=789L **명시/변경**)<br>(UID=789L)"];
        C6 --->|UID 불일치!| SD3;
        X3["💥 InvalidClassException<br>(의도된 차단)"]
        SD3 --> X3
        style X3 fill:#ffcccc,stroke:#a00
    end

    subgraph "시나리오2-UID 명시-호환 유지"
        C3["클래스 V1<br>(자동 UID=123L)"] --> SD2["직렬화된 데이터 V1<br>(UID=123L 포함)"];
        C4["클래스 V2<br>(V1 변경, UID=123L **명시**)<br>(UID=123L 유지)"];
        C4 --->|UID 일치!| SD2;
        X2["✅ 읽기 성공<br>(readObject에서<br>변경 처리 필요)"]
        SD2 --> X2
        style X2 fill:#ccffcc,stroke:#0a0
    end

    subgraph "시나리오1-UID 명시X"
        C1["클래스 V1<br>(자동 UID=123L)"] --> SD1["직렬화된 데이터 V1<br>(UID=123L 포함)"];
        C2["클래스 V2<br>(V1 변경, UID 명시X)<br>(자동 UID=456L)"];
        C2 --->|UID 불일치!| SD1;
        X1["💥 InvalidClassException"]
        SD1 --> X1;
        style X1 fill:#ffcccc,stroke:#a00
    end

    style C1 fill:#f0f8ff,stroke:#7570b3
    style SD1 fill:#fffacd,stroke:#8B4513
    style C2 fill:#f0f8ff,stroke:#7570b3
    style C3 fill:#f0f8ff,stroke:#7570b3
    style SD2 fill:#fffacd,stroke:#8B4513
    style C4 fill:#f0f8ff,stroke:#7570b3
    style C5 fill:#f0f8ff,stroke:#7570b3
    style SD3 fill:#fffacd,stroke:#8B4513
    style C6 fill:#f0f8ff,stroke:#7570b3
```

#### **왜?**

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

#### **어떻게?**

* `private static final long serialVersionUID = <고유한 long 값>;`
* 새 클래스는 아무 값, 기존 클래스는 `serialver`로 확인한 이전 값 사용 권장

#### **언제 변경?**

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

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

* 역직렬화 시 `transient` 필드는 기본값(null, 0, false)으로 시작
* `readObject`에서 직접 값을 설정하거나, 지연 초기화(Lazy Initialization)를 사용

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

```mermaid
%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#f0f8ff', 'primaryTextColor': '#000', 'primaryBorderColor': '#7570b3', 'lineColor': '#333', 'edgeLabelBackground':'#fff', 'clusterBkg': '#fcfcfc'}}}%%
graph TD
    subgraph "동기화 없음 (No Sync 🚫)"
        direction LR
        T1_A["스레드 A<br>(상태 변경 시도)"]
        T1_B["스레드 B<br>(writeObject 실행 중)"]
        OBJ1["객체 상태<br>(변경 중...)"]
        STR1["직렬화 스트림<br>(일관성 없는 데이터!)"]

        T1_A -->|변경| OBJ1
        T1_B -->|읽기| OBJ1
        T1_B -->|쓰기| STR1
        style STR1 fill:#ffcccc,stroke:#a00
    end

    subgraph "동기화 적용 (Sync ✅)"
        direction LR
        T2_A["스레드 A<br>(상태 변경 시도)"]
        T2_B["스레드 B<br>(synchronized writeObject<br>실행 중 🔒)"]
        OBJ2["객체 상태<br>(보호됨)"]
        STR2["직렬화 스트림<br>(일관된 스냅샷!)"]
        BLOCKED["(대기 중 ⏳)"]


        T2_B --"락(Lock) 획득"--> OBJ2
        T2_A -->|변경 시도| OBJ2
        OBJ2 -.-> BLOCKED; T2_A -.-> BLOCKED
        T2_B -->|읽기-안전| OBJ2
        T2_B -->|쓰기-안전| STR2
        style STR2 fill:#ccffcc,stroke:#0a0
    end

    style OBJ1 fill:#fffacd,stroke:#8B4513
    style OBJ2 fill:#fffacd,stroke:#8B4513
```

* 클래스가 스레드 안전성을 위해 `synchronized` 등을 사용한다면, `writeObject` 메서드도 **반드시** 동일한 동기화 전략
  * 예: `synchronized` 키워드 추가
  * 데이터 일관성 및 교착 상태 방지를 위함

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

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

<br>

***

## 5. 핵심 정리 ✨

* 클래스를 `Serializable`로 만들기로 했다면, **어떤 직렬화 형태를 사용할지 신중하게 설계**해야
* **기본 직렬화**는 객체의 논리적/물리적 표현이 거의 일치하는 **매우 단순한 경우**에만 고려
* 대부분의 경우, **커스텀 직렬화(`writeObject`/`readObject`)** 가 유연성, 성능, 안정성 면에서 더 나은 선택
* 직렬화 형태는 **클래스의 공개 API**와 같고, 한번 배포되면 변경하기 매우 어려우니, 설계에 충분한 시간을 투자
