F-LAB JAVA · 3주차 · Phase 9 · I/O 강화
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
직렬화 (Serialization) 는 자바 객체를 바이트 스트림으로 변환하여 저장/전송 가능하게 하고, 역직렬화 (Deserialization) 는 다시 객체로 복원하는 기술이다.
Serializable마커 인터페이스 만 구현하면ObjectOutputStream.writeObject(obj)로 객체 그래프 (객체 + 참조 + 참조의 참조 + ...) 가 모두 자동 저장.
transient키워드는 직렬화에서 제외할 필드를 표시 (비밀번호, 캐시, Thread 등).
자바 직렬화는 6가지 큰 문제 (보안 취약점/성능/호환성/유연성 X/언어 종속/스키마 X) 로 현대에는 JSON, Protobuf, Avro 권장 — Effective Java 도 "사용하지 말라" 권고.
그래도 알아둬야 하는 이유: 레거시 시스템, RMI, JCache, JMS, 일부 프레임워크 에서 여전히 사용.
객체 = 조립된 가구 (의자 + 다리 + 쿠션 등)
직렬화:
가구를 분해해서 박스에 넣기
- 각 부품 + 조립도
- 박스가 바이트 스트림
전송/저장:
박스를 보내거나 보관
역직렬화:
박스 풀어서 다시 조립
- 같은 가구 복원
- 같은 부품 + 같은 구조
transient:
운반하면 안 되는 부분 (예: 휘발성 표시)
- 운반 X
- 도착 후 다시 만들기
마커 인터페이스 (Serializable):
"이 가구는 분해 가능" 표시
- 메서드 없음
- 단순 마크
→ 직렬화 = 객체 분해 + 재조립.
1. 직렬화의 정의와 목적
2. Serializable 인터페이스
3. ObjectInputStream / ObjectOutputStream
4. 객체 그래프 직렬화
5. transient 키워드
6. writeObject / readObject 커스텀
7. 직렬화의 6가지 문제
8. 현대의 대안 (JSON, Protobuf)
9. 면접 + 자기 점검
직렬화 (Serialization):
자바 객체 → 바이트 스트림
메모리의 객체 (참조 그래프) 를
저장/전송 가능한 형태 (선형 바이트) 로 변환.
역직렬화 (Deserialization):
바이트 스트림 → 자바 객체
반대 방향.
필요한 시나리오:
1. 파일 저장
- 객체의 상태 유지
- 다음 실행 시 복원
2. 네트워크 전송
- 한 서버 → 다른 서버
- RPC, RMI
3. 캐시
- Redis, Memcached
- 자바 객체 저장
4. 메시지 큐
- JMS, Kafka 등
- 객체 메시지
5. JVM 간 통신
- 같은 JVM 의 객체를 다른 JVM 으로
메모리의 객체:
Shipment 객체:
address: 0x1000
fields:
id: 1L
blNo: "ABC123" (참조 → "ABC123" 문자열 객체)
consignee: Company 객체 (참조 → 0x2000)
items: ArrayList (참조 → 0x3000)
→ ShipmentItem 객체들
→ 각각 참조...
파일로 저장하려면:
- 모든 참조를 따라가며 모두 저장
- 객체 그래프 전체 직렬화
- 자바가 자동으로 해줌 (Serializable 만 있으면)
// 1. 직렬화 가능 클래스
class Shipment implements Serializable {
private Long id;
private String blNo;
private double weight;
// 생성자, getter, setter
}
// 2. 객체 → 파일
Shipment s = new Shipment(1L, "BL-001", 100.5);
try (FileOutputStream fos = new FileOutputStream("shipment.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
oos.writeObject(s);
}
// 3. 파일 → 객체
try (FileInputStream fis = new FileInputStream("shipment.ser");
ObjectInputStream ois = new ObjectInputStream(fis)) {
Shipment loaded = (Shipment) ois.readObject();
// s 와 같은 데이터
}
ObjectOutputStream.writeObject 가 자동으로:
1. 클래스 정보 저장
- 클래스 이름
- serialVersionUID
2. 객체의 모든 필드 저장
- 기본 타입 (int, long 등)
- 참조 (다른 객체)
3. 참조 그래프 추적
- 참조의 참조의 참조...
- 모두 따라가며 직렬화
4. 순환 참조 처리
- 같은 객체는 한 번만
- 두 번째부터 참조
5. 메타데이터
- 필드 이름
- 타입 정보
public class ShipmentSerializer {
public void save(Path path, Shipment s) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(
new FileOutputStream(path.toFile())))) {
oos.writeObject(s);
}
}
public Shipment load(Path path) throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(
new FileInputStream(path.toFile())))) {
return (Shipment) ois.readObject();
}
}
}
직렬화의 정의와 목적은?
답:
1. 정의:
목적:
자동 처리:
사용법:
package java.io;
public interface Serializable {
// 메서드 없음!
}
핵심:
마커 인터페이스:
메서드는 없고
단순히 "이 클래스는 X 능력 있음" 표시
다른 예:
- Cloneable (clone 가능)
- RandomAccess (배열 무작위 접근)
대안:
- 어노테이션 (@Serializable)
- 자바 5+ 이후 어노테이션이 더 일반적
- 단, Serializable 은 역사적 이유로 인터페이스 유지
확인:
obj instanceof Serializable
// true / false
// 1. 단순 구현
public class Shipment implements Serializable {
private Long id;
private String blNo;
// 메서드 추가 필요 X
}
// 2. 상속 시 자동 적용
public class UrgentShipment extends Shipment {
// 부모가 Serializable → 자식도 가능
}
// 3. 컴포지션
public class ShipmentContainer implements Serializable {
private List<Shipment> shipments;
// Shipment 도 Serializable 이어야!
// 만약 Shipment 가 Serializable 아니면:
// NotSerializableException
}
모두 Serializable 이어야 직렬화 성공:
1. 직접 직렬화하는 객체
- writeObject(obj) 의 obj
2. 객체가 참조하는 모든 객체
- 필드의 객체
- 그 객체의 필드...
- 전체 그래프
3. 컬렉션의 요소
- List, Set, Map 의 요소
예외:
- transient 필드는 제외
- 기본 타입은 자동
- String 은 Serializable 구현됨
- 컬렉션 (ArrayList 등) 도 Serializable
// 필드가 Serializable 아니면
class Container implements Serializable {
private NonSerializable field; // ❌
}
class NonSerializable {
// Serializable 안 함
}
// 시도
oos.writeObject(new Container());
// NotSerializableException: NonSerializable
// 해결:
// 1. NonSerializable 도 Serializable 구현
// 2. transient 로 표시 (직렬화 제외)
// 3. 다른 방식 (writeObject 커스텀)
자주 사용하는 Serializable 클래스:
기본 타입 래퍼:
Integer, Long, Double, Boolean, Character ... ✓
String: ✓
StringBuilder, StringBuffer: ✓
컬렉션:
ArrayList, LinkedList, HashMap, HashSet, TreeMap, ... ✓
날짜:
LocalDateTime, Instant, LocalDate, ... ✓
(java.time 의 클래스들)
기본 Number:
BigInteger, BigDecimal: ✓
X (Serializable 아님):
Thread, Socket, FileInputStream, ServletContext
→ 의미 없거나 보안 위험
// 1. 단순한 Serializable Entity
public class Shipment implements Serializable {
private Long id;
private String blNo;
private String consignee;
private BigDecimal weight;
private LocalDateTime createdAt;
private ShipmentStatus status;
// 모든 필드가 Serializable
// 자동 직렬화 OK
}
// 2. 컬렉션 포함
public class ShipmentBatch implements Serializable {
private String batchId;
private List<Shipment> shipments; // ArrayList 등
private Map<String, String> metadata;
// 모두 Serializable
}
// 3. Enum 도 자동
public enum ShipmentStatus implements Serializable {
PENDING, SHIPPED, DELIVERED, CANCELLED;
// Enum 은 자동 Serializable
}
Serializable 인터페이스의 역할은?
답:
1. 마커 인터페이스:
구현:
모든 그래프:
NotSerializableException:
표준 클래스:
public class ObjectOutputStream extends OutputStream
implements ObjectOutput, ObjectStreamConstants {
public ObjectOutputStream(OutputStream out);
public final void writeObject(Object obj) throws IOException;
// DataOutput 메서드
public void writeInt(int val);
public void writeLong(long val);
public void writeUTF(String str);
// ... 기타
}
public class ObjectInputStream extends InputStream
implements ObjectInput, ObjectStreamConstants {
public ObjectInputStream(InputStream in);
public final Object readObject() throws IOException, ClassNotFoundException;
// DataInput 메서드
public int readInt();
public long readLong();
public String readUTF();
// ... 기타
}
핵심:
public interface ObjectOutput extends DataOutput, AutoCloseable {
void writeObject(Object obj) throws IOException;
// DataOutput 상속
// writeInt, writeLong 등
}
public interface ObjectInput extends DataInput, AutoCloseable {
Object readObject() throws ClassNotFoundException, IOException;
// DataInput 상속
}
// 인터페이스 계층:
// ObjectOutput extends DataOutput
// DataOutput 의 모든 기본 타입 메서드 사용 가능
oos.writeObject(shipment);
// 내부 동작:
// 1. 클래스 정보 쓰기
// - 클래스 이름
// - serialVersionUID
// - 필드 정보 (이름, 타입)
// 2. 객체의 필드 쓰기
// - 기본 타입: 그대로
// - String: writeUTF
// - 참조: 재귀적 writeObject
// 3. 참조 추적
// - 이미 직렬화된 객체는 참조만
// - "이 객체는 이전의 #5" 같은 식
// 4. 메타데이터
// - 그래프 끝 표시
Object obj = ois.readObject();
Shipment s = (Shipment) obj;
// 내부 동작:
// 1. 클래스 정보 읽기
// - 클래스 로드
// - serialVersionUID 검증
// 2. 객체 생성
// - 생성자 호출 X
// - JVM 의 내부 메커니즘
// - 기본값으로 초기화
// 3. 필드 복원
// - 기본 타입 읽기
// - 참조 복원 (재귀)
// 4. 캐스팅
// - Object 반환
// - 호출자가 캐스팅
// - ClassCastException 가능
// readObject 가 던질 수 있는 예외
try {
Object obj = ois.readObject();
} catch (ClassNotFoundException e) {
// 클래스 로드 실패
// 직렬화 시의 클래스가 현재 classpath 에 없음
}
// 시나리오:
// 1. JVM A 에서 직렬화
// - com.example.Shipment 클래스
// 2. JVM B 에서 역직렬화 시도
// - Shipment 클래스 없음
// - ClassNotFoundException
// 해결:
// 1. 같은 classpath
// 2. 또는 다른 방식 (JSON 등)
// 권장 패턴
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(
new FileOutputStream("data.ser")))) {
oos.writeObject(shipment);
}
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(
new FileInputStream("data.ser")))) {
Shipment s = (Shipment) ois.readObject();
}
// 한 파일에 여러 객체
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("data.ser"))) {
oos.writeObject(shipment1);
oos.writeObject(shipment2);
oos.writeObject(shipment3);
// 또는 컬렉션 한 번에
oos.writeObject(List.of(shipment1, shipment2, shipment3));
}
// 읽기 (같은 순서)
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("data.ser"))) {
Shipment s1 = (Shipment) ois.readObject();
Shipment s2 = (Shipment) ois.readObject();
Shipment s3 = (Shipment) ois.readObject();
// 또는
@SuppressWarnings("unchecked")
List<Shipment> list = (List<Shipment>) ois.readObject();
}
// 끝 도달 시:
// EOFException (기본 타입) 또는
// null 또는 OptionalDataException
public class ShipmentObjectSerializer {
public void save(Path path, Shipment s) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(
new FileOutputStream(path.toFile())))) {
oos.writeObject(s);
}
}
public Shipment load(Path path) throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(
new FileInputStream(path.toFile())))) {
return (Shipment) ois.readObject();
}
}
// 컬렉션 직렬화
public void saveAll(Path path, List<Shipment> shipments) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(
new GZIPOutputStream( // 압축
new FileOutputStream(path.toFile()))))) {
oos.writeObject(shipments);
}
}
@SuppressWarnings("unchecked")
public List<Shipment> loadAll(Path path) throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(
new GZIPInputStream(
new FileInputStream(path.toFile()))))) {
return (List<Shipment>) ois.readObject();
}
}
}
ObjectInputStream / ObjectOutputStream 의 동작은?
답:
1. 정의:
writeObject:
readObject:
권장:
여러 객체:
객체 그래프:
객체 A 가 B 를 참조,
B 가 C 를 참조,
...
연결된 모든 객체.
예:
Shipment
├── consignee: Company
│ ├── address: Address
│ └── contact: Person
├── items: ArrayList
│ ├── ShipmentItem 1
│ ├── ShipmentItem 2
│ └── ...
└── status: ShipmentStatus
class A implements Serializable {
B b;
}
class B implements Serializable {
C c;
}
class C implements Serializable {
String data;
}
A a = new A();
a.b = new B();
a.b.c = new C();
a.b.c.data = "Hello";
oos.writeObject(a);
// 자동으로:
// 1. A 저장
// 2. A.b 의 B 저장
// 3. B.c 의 C 저장
// 4. C.data 의 String 저장
// 모두 자동
객체 그래프:
A (0x1000)
└─ b → B (0x2000)
└─ c → C (0x3000)
└─ data → "Hello"
직렬화된 바이트 스트림:
[헤더]
[A 의 클래스 정보]
[A 의 필드: b 참조]
[B 의 클래스 정보]
[B 의 필드: c 참조]
[C 의 클래스 정보]
[C 의 필드: data 참조]
[String "Hello" 저장]
[그래프 끝]
역직렬화:
- 같은 구조 복원
- 새 주소
- 같은 데이터
// 순환 참조
class Node implements Serializable {
Node next;
String data;
}
Node n1 = new Node();
n1.data = "1";
Node n2 = new Node();
n2.data = "2";
n1.next = n2;
n2.next = n1; // 순환!
oos.writeObject(n1);
// 자바가 자동 처리:
// 1. n1 저장
// 2. n1.next → n2 저장
// 3. n2.next → n1 (이미 저장됨, 참조만)
// 즉, 무한 루프 X
// 자동으로 중복 방지
// 역직렬화 후
Node loaded = (Node) ois.readObject();
loaded.next.next == loaded; // true (순환 복원)
// 두 곳에서 같은 객체 참조
class Order implements Serializable {
Customer customer;
}
class Invoice implements Serializable {
Customer customer; // 같은 Customer
}
Customer c = new Customer("Alice");
Order o = new Order();
o.customer = c;
Invoice i = new Invoice();
i.customer = c; // 같은 인스턴스
// 컬렉션에 둘 다 넣어 직렬화
oos.writeObject(List.of(o, i));
// 자바가 자동:
// 1. Order 저장
// 2. Customer "Alice" 저장
// 3. Invoice 저장
// 4. Invoice.customer 는 이미 저장된 Customer 참조
// 역직렬화 후
List<Object> list = (List<Object>) ois.readObject();
Order ro = (Order) list.get(0);
Invoice ri = (Invoice) list.get(1);
ro.customer == ri.customer; // true (같은 인스턴스 복원)
장점:
1. 자동 처리
- 사용자가 신경 X
- 복잡한 구조도 한 줄
2. 참조 무결성
- 같은 객체는 같은 참조
- 순환도 OK
3. 깊은 복사
- 객체 + 그래프 전체
- 새로운 인스턴스
활용:
- deep copy
- 객체 스냅샷
- 캐시
class Container implements Serializable {
transient NonSerializable nonSer; // ✓ transient 로 제외
Serializable ser;
// ❌ 만약 transient 없으면
// NonSerializable field;
// → NotSerializableException
}
// 전체 그래프 검증 필요:
// - 모든 참조 추적
// - 모든 객체가 Serializable
// - 또는 transient
// 직렬화로 깊은 복사
public static <T extends Serializable> T deepCopy(T obj) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(obj);
}
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
try (ObjectInputStream ois = new ObjectInputStream(bais)) {
@SuppressWarnings("unchecked")
T copy = (T) ois.readObject();
return copy;
}
}
// 사용
Shipment original = new Shipment(...);
Shipment copy = deepCopy(original);
original != copy; // true (다른 인스턴스)
original.equals(copy); // true (값은 같음 — equals 구현 시)
// 단점:
// - 성능 ↓ (직렬화/역직렬화 비용)
// - Serializable 필수
// - 일반적 deep copy 보다 느림
public class ShipmentDeepCopier {
@SuppressWarnings("unchecked")
public <T extends Serializable> T deepCopy(T obj) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(obj);
}
try (ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(baos.toByteArray()))) {
return (T) ois.readObject();
}
} catch (Exception e) {
throw new RuntimeException("Deep copy failed", e);
}
}
// 활용 — 트랜잭션 시작 시 스냅샷
public void processWithRollback(Shipment original) {
Shipment backup = deepCopy(original);
try {
modifyShipment(original);
commitChanges(original);
} catch (Exception e) {
// 롤백 — backup 으로
restoreFromBackup(backup);
throw e;
}
}
}
객체 그래프 직렬화의 메커니즘은?
답:
1. 자동 추적:
순환 참조:
같은 객체:
전체 그래프 Serializable:
활용:
transient:
필드를 직렬화에서 제외하는 키워드.
목적:
1. 의미 없는 필드 (캐시, 임시 계산값)
2. 직렬화 불가능 (Thread, Socket)
3. 보안 (비밀번호, 키)
4. 크기 ↓ (큰 필드 제외)
public class User implements Serializable {
private String username;
private transient String password; // ★ 직렬화 X
private transient Thread workerThread; // 의미 없음
private transient byte[] cachedData; // 캐시
private String email;
}
// 직렬화 시:
// - username: 저장
// - password: ★ 저장 X (보안)
// - workerThread: 저장 X
// - cachedData: 저장 X
// - email: 저장
// 역직렬화 시:
// - 저장된 필드 복원
// - transient 필드는 ★ 기본값 (null, 0, false)
public class Cache implements Serializable {
private int hitCount;
private transient Map<String, byte[]> data; // 큰 캐시
private long lastModified;
}
// 직렬화
Cache c = new Cache();
c.hitCount = 100;
c.data = ...; // 큰 데이터
c.lastModified = ...;
oos.writeObject(c);
// data 는 제외 (크기 ↓)
// 역직렬화
Cache loaded = (Cache) ois.readObject();
loaded.hitCount; // 100 (정상 복원)
loaded.data; // null! ★ 기본값
loaded.lastModified; // 정상 복원
// data 재초기화 필요
// readObject 커스텀 활용
// 1. 비밀번호 (보안)
class Credential implements Serializable {
private String username;
private transient String password;
// 직렬화 시 비밀번호 노출 X
}
// 2. 임시 캐시
class Calculator implements Serializable {
private int input;
private transient int cachedResult;
// 캐시는 재계산
}
// 3. 의미 없는 자원
class DatabaseConnection implements Serializable {
private String connectionUrl;
private transient Connection conn;
// Connection 은 직렬화 의미 X
}
// 4. 큰 데이터
class Image implements Serializable {
private int width;
private int height;
private transient byte[] pixels; // 큰 데이터
// 필요 시 재로드
}
// 5. 동시성 객체
class Counter implements Serializable {
private int value;
private transient Object lock = new Object();
// Lock 객체는 직렬화 X
}
static 필드:
- 클래스에 속함
- 객체에 속하지 않음
- ★ 자동 직렬화 안 됨
- transient 명시 불필요
transient 필드:
- 객체에 속함
- 명시적으로 제외
class A implements Serializable {
static int classCount; // 자동 제외
transient int temp; // 명시 제외
int value; // 직렬화 O
}
public class Cache implements Serializable {
private String key;
private transient byte[] data;
// 역직렬화 후 재초기화
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
ois.defaultReadObject(); // 일반 필드 복원
// data 는 null
// 재로드
data = loadFromDisk(key);
}
private byte[] loadFromDisk(String key) {
// ...
}
}
public class ShipmentCache implements Serializable {
private static final long serialVersionUID = 1L;
// 기본 필드
private Long shipmentId;
private LocalDateTime cachedAt;
// 직렬화 X
private transient Map<String, byte[]> cachedDocuments; // 큰 캐시
private transient AtomicLong hitCounter; // 동시성 객체
public ShipmentCache(Long shipmentId) {
this.shipmentId = shipmentId;
this.cachedAt = LocalDateTime.now();
this.cachedDocuments = new ConcurrentHashMap<>();
this.hitCounter = new AtomicLong();
}
// 역직렬화 후 transient 초기화
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
ois.defaultReadObject();
this.cachedDocuments = new ConcurrentHashMap<>();
this.hitCounter = new AtomicLong();
}
}
// 보안 - 비밀번호
public class UserCredential implements Serializable {
private String username;
private String email;
// 절대 직렬화 X
private transient String password;
private transient byte[] apiSecret;
// 역직렬화 후 password 는 null
// 직접 설정해야 함
}
transient 키워드의 의미와 활용은?
답:
1. 의미:
활용 5가지:
static 과:
재초기화:
자동 직렬화의 한계:
1. 모든 필드 자동
- 일부만 직렬화하고 싶을 때
2. 형식 제어 X
- 자바 표준 형식
3. transient 의 재초기화
- 자동으로 안 됨
4. 보안 검증
- 역직렬화 시 검증 필요
5. 호환성
- 옛 버전 → 새 버전
class MyClass implements Serializable {
// 직렬화 커스텀 메서드 — private 필수
private void writeObject(ObjectOutputStream oos) throws IOException {
// 1. 기본 필드 처리
oos.defaultWriteObject();
// 2. 추가 데이터
oos.writeInt(customValue);
}
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
// 1. 기본 필드 복원
ois.defaultReadObject();
// 2. 추가 데이터 읽기
int customValue = ois.readInt();
// 3. transient 재초기화
this.cachedData = recompute();
}
}
// ★ 정확한 시그니처
private void writeObject(ObjectOutputStream out) throws IOException;
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
// 잘못된 시그니처:
public void writeObject(...) — ❌ private 이어야
void writeObject(...) — ❌ private 이어야
// JVM 이 reflection 으로 호출
// private 만 받음
class SecureData implements Serializable {
private String userId;
private transient String encryptionKey; // 직렬화 X
private byte[] encryptedData;
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject();
// encryptionKey 는 transient 라 저장 X
}
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
ois.defaultReadObject();
// 검증
if (userId == null || encryptedData == null) {
throw new InvalidObjectException("Required fields missing");
}
if (encryptedData.length > MAX_SIZE) {
throw new InvalidObjectException("Data too large");
}
// 키 재로드 (외부에서)
// encryptionKey = loadKey(userId);
}
}
class Person implements Serializable {
private static final long serialVersionUID = 2L;
private String name;
private int age;
private String email; // V2 에서 추가
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
ois.defaultReadObject();
// V1 → V2 호환
if (email == null) {
email = "unknown@example.com"; // 기본값
}
}
}
// V1 파일을 V2 코드로 읽기
// - name, age 는 정상 복원
// - email 은 V1 파일에 없음 → null
// - readObject 에서 기본값 처리
class CustomMap implements Serializable {
private transient Map<String, Object> map;
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject();
// 크기 + 각 엔트리
oos.writeInt(map.size());
for (Map.Entry<String, Object> entry : map.entrySet()) {
oos.writeUTF(entry.getKey());
oos.writeObject(entry.getValue());
}
}
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
ois.defaultReadObject();
int size = ois.readInt();
map = new HashMap<>(size);
for (int i = 0; i < size; i++) {
String key = ois.readUTF();
Object value = ois.readObject();
map.put(key, value);
}
}
}
class MyClass implements Serializable {
// 1. writeReplace — 직렬화 시 다른 객체로 교체
private Object writeReplace() throws ObjectStreamException {
return new ProxyClass(this);
}
// 2. readResolve — 역직렬화 후 다른 객체로 교체
private Object readResolve() throws ObjectStreamException {
return INSTANCE; // Singleton 보호
}
// 3. writeObject / readObject (위에서 봄)
// 4. readObjectNoData — 다른 버전 호환
private void readObjectNoData() throws ObjectStreamException {
// V1 객체를 V2 로 읽을 때 데이터 없는 필드 처리
}
}
public class ShipmentWithCustomSerialization implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String blNo;
private List<ShipmentItem> items;
private transient Map<String, byte[]> cachedDocs;
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject();
// 추가 메타데이터
oos.writeUTF("ILIC-v1.0");
oos.writeLong(System.currentTimeMillis());
}
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
ois.defaultReadObject();
// 메타데이터 검증
String version = ois.readUTF();
if (!version.startsWith("ILIC-")) {
throw new InvalidObjectException("Invalid version: " + version);
}
long savedAt = ois.readLong();
// 너무 오래된 데이터?
if (savedAt < System.currentTimeMillis() - Duration.ofDays(365).toMillis()) {
log.warn("Shipment data is over 1 year old");
}
// transient 재초기화
this.cachedDocs = new ConcurrentHashMap<>();
// 검증
if (id == null || blNo == null) {
throw new InvalidObjectException("Required fields missing");
}
}
}
writeObject / readObject 커스텀의 활용은?
답:
1. 시그니처:
자주 활용:
활용 5가지:
다른 커스텀:
보안 취약점:
악의적 데이터로 역직렬화 → 임의 코드 실행
CVE 사례 무수히 많음
예: Apache Commons Collections (2015)
- InvokerTransformer 의 코드 실행
- Ysoserial 같은 공격 도구
- 자바 생태계 큰 충격
원인:
- readObject 가 모든 클래스 로드
- 검증 없이 객체 생성
- 임의 객체 그래프 가능
권장:
- 신뢰할 수 없는 데이터 역직렬화 X
- JEP 290 (filterInputObjectStreams)
- 또는 다른 형식 (JSON)
// Java 9+ 의 ObjectInputFilter
ObjectInputStream ois = new ObjectInputStream(in);
ois.setObjectInputFilter(filter -> {
Class<?> clazz = filter.serialClass();
if (clazz == null) return ObjectInputFilter.Status.ALLOWED;
// 허용 클래스만
if (clazz.getPackageName().startsWith("com.ilic.")) {
return ObjectInputFilter.Status.ALLOWED;
}
return ObjectInputFilter.Status.REJECTED;
});
Object obj = ois.readObject();
// 허용된 클래스만 로드
직렬화의 성능 문제:
1. 느림
- JSON 보다 약 2-3배 느림
- Protobuf 보다 10배+ 느림
2. 크기 큼
- 클래스 메타데이터 포함
- 필드 이름 등
3. GC 압박
- 임시 객체 많이 생성
벤치마크 (예시):
자바 직렬화: 100ms, 5KB
JSON (Jackson): 30ms, 3KB
Protobuf: 5ms, 1KB
호환성 문제:
1. 같은 클래스 필수
- 직렬화 시 / 역직렬화 시
- 다른 JVM 에 같은 클래스 있어야
2. serialVersionUID 변경
- 필드 추가/변경 시
- 옛 데이터 못 읽음
3. 패키지 이동
- 클래스 이동하면 호환 X
4. 다른 언어
- 자바 외 X
- 다른 시스템과 호환 X
유연성 부족:
1. 형식 자바 고정
- 사용자 정의 형식 X
- 자바 표준 형식만
2. 스키마 X
- JSON Schema, Protobuf .proto 같은
- 형식 정의 어려움
3. 도구 부족
- JSON 의 풍부한 도구 vs
- 자바 직렬화 도구 미흡
캡슐화 위반:
private 필드도 직렬화
→ 형식이 코드 구현에 강결합
영향:
1. 리팩토링 어려움
- 필드 이름 변경 = 호환성 깨짐
2. 보안
- private 정보 노출
3. 유지보수
- 형식 = 구현 = 변경 어려움
디버깅 어려움:
1. 바이너리 형식
- 사람이 읽기 어려움
- 전용 도구 필요
2. 에러 메시지 부족
- StreamCorruptedException
- 어느 위치인지 모름
3. 도구 부족
- hex dump 정도
vs JSON:
- 텍스트, 가독성
- 풍부한 도구
Effective Java (3rd Edition):
"자바 직렬화는 위험하며 피해야 한다."
Item 85: Prefer alternatives to Java serialization
Bloch 의 권고:
- 새 시스템에 자바 직렬화 사용 X
- JSON, Protobuf, Avro 등 권장
- 레거시만 어쩔 수 없이 유지
JEP 290 (Java 9):
- Object Filter 도입
- 보안 강화
JEP 415 (Java 17):
- Context-Specific Deserialization Filters
장기 계획:
- 자바 직렬화 deprecation 검토 중
- Project Amber 의 새 메커니즘
직렬화의 6가지 문제는?
답:
1. 보안 취약점: 임의 코드 실행 (CVE 다수)
2. 성능: JSON 의 2-3배 느림, Protobuf 의 10배+
3. 호환성: 같은 클래스 필수, 다른 언어 X
4. 유연성 X: 자바 표준 형식만
5. 캡슐화 위반: private 필드 노출
6. 디버깅 어려움: 바이너리, 도구 부족
Effective Java 권고: "Prefer alternatives to Java serialization"
| 형식 | 가독성 | 크기 | 속도 | 스키마 | 언어 |
|---|---|---|---|---|---|
| Java 직렬화 | ↓ | 중 | 중 | X | Java |
| JSON | ↑↑ | 큼 | 중 | 옵션 | 모두 |
| XML | ↑↑ | 큼↑ | ↓ | XSD | 모두 |
| Protobuf | ↓ | 작음↑ | ↑↑ | 필수 (.proto) | 모두 |
| Avro | ↓ | 작음 | ↑↑ | 필수 (JSON) | 모두 |
| MessagePack | ↓ | 작음 | ↑↑ | X | 모두 |
| Kryo (Java) | ↓ | 작음 | ↑↑ | X | Java |
// Jackson 의 활용
ObjectMapper mapper = new ObjectMapper();
// 직렬화
Shipment s = new Shipment(...);
String json = mapper.writeValueAsString(s);
// {"id":1,"blNo":"BL-001",...}
// 파일 저장
mapper.writeValue(Path.of("shipment.json").toFile(), s);
// 역직렬화
Shipment loaded = mapper.readValue(json, Shipment.class);
Shipment fromFile = mapper.readValue(
Path.of("shipment.json").toFile(), Shipment.class);
// 컬렉션
List<Shipment> list = mapper.readValue(
json, new TypeReference<List<Shipment>>(){});
// 장점:
// - 가독성
// - 모든 언어 호환
// - 도구 풍부
// - 스키마 옵션
// shipment.proto
syntax = "proto3";
package com.ilic;
message Shipment {
int64 id = 1;
string bl_no = 2;
string consignee = 3;
double weight = 4;
int64 created_at = 5;
ShipmentStatus status = 6;
}
enum ShipmentStatus {
PENDING = 0;
SHIPPED = 1;
DELIVERED = 2;
}
// 자동 생성된 Java 코드
Shipment s = Shipment.newBuilder()
.setId(1)
.setBlNo("BL-001")
.setWeight(100.5)
.build();
// 직렬화 — 바이너리, 매우 작음
byte[] bytes = s.toByteArray();
// 역직렬화
Shipment loaded = Shipment.parseFrom(bytes);
// 장점:
// - 매우 작음 (vs JSON 10배 작음)
// - 매우 빠름
// - 스키마 강제 (.proto)
// - 모든 언어
// - 버전 관리 (필드 번호)
// Shipment 객체 (10개 필드)
// Java 직렬화
// - ~500 바이트
// - ~10ms
// JSON (Jackson)
// - ~200 바이트
// - ~3ms
// Protobuf
// - ~50 바이트
// - ~0.3ms
// 차이:
// - 크기: Protobuf < JSON < Java
// - 속도: Protobuf > JSON > Java
// - 가독성: JSON > Java > Protobuf
JSON 권장:
✓ REST API
✓ 설정 파일
✓ 사람이 봄
✓ 다양한 언어
✓ 가독성
Protobuf 권장:
✓ 마이크로서비스 (gRPC)
✓ 대량 데이터
✓ 성능 우선
✓ 스키마 강제
✓ 작은 메시지
Java 직렬화:
✗ 일반적으로 권장 X
✓ RMI (제한적)
✓ JCache, JMS (불가피)
✓ 레거시
// 자바 직렬화 → JSON
public class MigrationTool {
private final ObjectMapper mapper = new ObjectMapper();
// 옛 자바 직렬화 데이터를 JSON 으로
public void migrate(Path source, Path dest) throws Exception {
Object obj;
try (ObjectInputStream ois = new ObjectInputStream(
Files.newInputStream(source))) {
obj = ois.readObject();
}
// JSON 으로
mapper.writeValue(dest.toFile(), obj);
}
}
// 단계적 전환:
// 1. JSON 직렬화 추가
// 2. 새 데이터는 JSON
// 3. 옛 데이터 마이그레이션
// 4. 자바 직렬화 코드 제거
// REST API → JSON (Jackson)
@RestController
public class ShipmentController {
@GetMapping("/api/shipments/{id}")
public Shipment get(@PathVariable Long id) {
return service.findById(id);
// Spring 이 자동으로 JSON 직렬화
}
}
// 내부 마이크로서비스 → gRPC (Protobuf)
@GrpcService
public class ShipmentGrpcService extends ShipmentServiceGrpc.ShipmentServiceImplBase {
@Override
public void getShipment(GetShipmentRequest request,
StreamObserver<ShipmentResponse> responseObserver) {
// ...
}
}
// 캐시 → JSON 또는 Protobuf
@Cacheable("shipments")
public Shipment findById(Long id) {
// Redis 에 JSON 또는 byte[] 저장
}
// 메시지 큐 → Avro 또는 Protobuf
@KafkaListener(topics = "shipments")
public void handle(ShipmentEvent event) {
// 스키마 보장
}
// 자바 직렬화 사용 안 함
현대의 직렬화 대안은?
답:
1. JSON (Jackson):
Protobuf:
Avro:
MessagePack:
Kryo (Java):
자바 직렬화는 일반적으로 권장 X
| Q | 핵심 답변 |
|---|---|
| 직렬화 정의? | 객체 ↔ 바이트 |
| Serializable 의 역할? | 마커 인터페이스 |
| 마커 인터페이스? | 메서드 없이 능력 표시 |
| ObjectOutputStream? | writeObject 로 직렬화 |
| 객체 그래프? | 모든 참조 자동 추적 |
| transient? | 직렬화 제외 |
| transient 활용? | 비밀번호, 캐시, 자원 |
| writeObject 커스텀? | private, defaultWriteObject |
| 순환 참조? | 자동 처리 |
| 같은 객체 참조? | 한 번만 저장 |
| Java 직렬화의 문제? | 보안/성능/호환/유연/캡슐화/디버깅 |
| 대안? | JSON, Protobuf |
| 언제 사용? | 레거시, RMI, JCache |
| Externalizable? | 완전 수동 제어 |
답:
// Serializable — 자동
class A implements Serializable {
private String data;
// 자동 직렬화
}
// Externalizable — 완전 수동
class B implements Externalizable {
private String data;
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(data);
}
public void readExternal(ObjectInput in) throws IOException {
data = in.readUTF();
}
public B() { } // public 기본 생성자 필수
}
// 차이:
// - Serializable: 자동 (편함)
// - Externalizable: 수동 (제어 강함, 빠름)
// - Externalizable 은 인기 X (사용 어려움)
답:
답:
답:
답:
class A implements Serializable {
private final String name; // ✓ 직렬화 OK
public A(String name) {
this.name = name;
}
}
// 역직렬화:
// - 생성자 호출 X
// - JVM 이 reflection 으로 final 필드 설정
// - final 의 약속 깨짐 (특수 케이스)
1. 자바 직렬화
2. 핵심 키워드
3. 현대 권장
이번 Unit에서 직렬화의 기초를 봤다면, 다음은 버전 관리의 핵심.
🚀 Phase 9 — I/O 강화
✅ Unit 9.1 try-with-resources
✅ Unit 9.2 BufferedInputStream / BufferedOutputStream
✅ Unit 9.3 DataInputStream / DataOutputStream
✅ Unit 9.4 Serialization (직렬화) ← 여기
⏭ Unit 9.5 serialVersionUID
✅ Phase 1 ~ 8 완주 (37 Unit)
🚀 Phase 9 — I/O 강화 (4/5 진행)
총: 41/43 Unit (약 95%)