87. 커스텀 직렬화 형태를 고려해보라
0. 시작 하기 전, Serializable
핵심 요약
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
추가public class MyData implements Serializable { // ... 클래스 내용 ... }
주요 용도:
데이터 영속화: 프로그램 종료 후에도 데이터 보존
(예: 게임 저장)
네트워크 통신: 원격 시스템 간 객체 전달
(예: 채팅 메시지 객체 전송)
캐싱: 자주 쓰는 객체를 저장해두고 빠르게 재사용
serialVersionUID
: 클래스 버전 관리용 고유 번호. 직렬화/역직렬화 시 이 번호가 일치하는지 확인하여 호환성 체크 (매우 중요)주의: 그냥
implements Serializable
만 쓰면(기본 직렬화) 나중에 클래스 내부를 수정했을 때 저장된 객체를 못 읽는 문제가 생길 수 있음 (유지보수 어려움)대안: JSON, XML, Protocol Buffers 등 다른 데이터 저장/전송 방식도 많이 사용됨.
1. 왜 기본 직렬화는 위험할 수 있는가? 🤔
핵심 문제: 기본 직렬화는 객체의 논리적 데이터가 아닌, 물리적 표현을 그대로 저장.
// 코드 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;
}
...// 나머지 코드는 생략
}
%%{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
발생 가능한 문제점들
%%{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
1. API 고착화
클래스 내부 구현(private 필드, 내부 클래스 구조 등)이 직렬화 형태에 포함되어 공개 API처럼 되어버림
향후 내부 리팩토링이 매우 어려워짐 (
StringList
의Entry
클래스 예시)
2. 성능 저하 (공간)
불필요한 내부 데이터(예: 연결 리스트의 포인터)까지 저장 -> 비효율적 직렬화된 크기
3. 성능 저하 (시간)
복잡한 객체 그래프를 그대로 탐색 -> 직렬화/역직렬화 속도 저하
4. StackOverflowError 위험
객체 그래프가 깊거나 크면 재귀적인 직렬화 과정에서 스택 오버플로 발생 가능
5. 정확성 문제
객체의 불변식이 내부 구현 방식에 의존하는 경우, 기본 직렬화/역직렬화 후 객체가 비정상 상태 유발
예: Hashtable의 해시 버킷 위치
기억하세요
Serializable
을 구현하고 기본 직렬화를 사용하면, 그 클래스의 현재 내부 구현에 영원히 묶일 수 있습니다.
2. 그렇다면, 언제 기본 직렬화를 써도 괜찮을까? ✅
황금률: 객체의 논리적 내용과 물리적 표현(필드) 이 거의 동일할 때만 사용을 고려합니다.
예시:
Name
클래스 (lastName
,firstName
,middleName
필드가 곧 논리적 데이터)
// 코드 87-1: 기본 직렬화 형태에 적합한 후보 (개념)
public class Name implements Serializable {
private final String lastName;
private final String firstName;
private final String middleName;
// ... 생성자, 메서드 등 ...
}
%%{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
메서드가 필요할 수 있습니다.
readObject
메서드가 필요할 수 있습니다.예:
Name
필드가null
이 아님을 보장
3. 해결책: 커스텀 직렬화 (writeObject/readObject) 💡
%%{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
키워드:
transient
키워드:기본 직렬화 과정에서 제외할 필드에 표시
언제 사용?
다른 필드에서 유도되는 값 (캐시 등)
논리적 상태와 무관한 필드 (네이티브 리소스 포인터 등)
커스텀 직렬화 시 저장하지 않을 내부 구현 필드
private void writeObject(ObjectOutputStream out) throws IOException;
private void writeObject(ObjectOutputStream out) throws IOException;
객체를 어떻게 스트림에 쓸지 직접 정의
필수:
out.defaultWriteObject();
를 먼저 호출 (미래 호환성 위해)논리적 데이터만 순서대로
out.writeInt()
,out.writeObject()
등으로 저장
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
스트림에서 데이터를 읽어 객체를 어떻게 복원할지 직접 정의
필수:
in.defaultReadObject();
를 먼저 호출 (미래 호환성 위해)writeObject
에서 저장한 순서대로 데이터를 읽어 객체 상태를 재구성transient
필드나 계산된 필드를 여기서 초기화/복원해야 할 수 있습니다.
개념 예시 (StringList):
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() 및 다른 메서드 생략 ...
}
장점: 유연성 확보, 성능(공간/시간) 향상, 안정성 증가(스택 오버플로 방지)
4. 직렬화 시 반드시 지켜야 할 것들! 📌
1. serialVersionUID
를 명시적으로 선언하세요!
serialVersionUID
를 명시적으로 선언하세요!%%{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
필드 초기화를 신경 쓰세요.역직렬화 시
transient
필드는 기본값(null, 0, false)으로 시작readObject
에서 직접 값을 설정하거나, 지연 초기화(Lazy Initialization)를 사용
3. 동기화를 고려하세요.
%%{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 태그 사용. (어떤 순서로 무엇을 저장/읽는지 설명)
5. 핵심 정리 ✨
클래스를
Serializable
로 만들기로 했다면, 어떤 직렬화 형태를 사용할지 신중하게 설계해야기본 직렬화는 객체의 논리적/물리적 표현이 거의 일치하는 매우 단순한 경우에만 고려
대부분의 경우, 커스텀 직렬화(
writeObject
/readObject
) 가 유연성, 성능, 안정성 면에서 더 나은 선택직렬화 형태는 클래스의 공개 API와 같고, 한번 배포되면 변경하기 매우 어려우니, 설계에 충분한 시간을 투자
Last updated