F-LAB JAVA · 3주차 · Phase 9 · I/O 강화
🏆 Phase 9 완주 — I/O 강화 마스터
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
serialVersionUID는 직렬화 가능 클래스의 버전 식별자 (64비트 long) 로, 직렬화 시점과 역직렬화 시점의 클래스가 호환되는지 판단한다.
명시 안 하면 컴파일러가 클래스의 필드/메서드 시그니처로 자동 계산 — 작은 변경 (필드 추가, 메서드 추가) 만으로도 값이 달라져 옛 데이터를 못 읽음.
명시 권장 (private static final long serialVersionUID = 1L) — 호환성 깰 의도 없는 한 유지, 의도적으로 깰 때만 변경.
변경 시점: 필드 타입 변경, 필드 삭제, 클래스 이름 변경 등 비호환 변경 만 — 필드 추가는 일반적으로 호환됨.
serialver도구 또는 IDE 의 자동 계산으로 값 생성, 일반적으로는 단순 1L 도 충분.
serialVersionUID = 책의 ISBN
다른 ISBN = 다른 책
같은 ISBN = 같은 (또는 호환되는) 책
자동 계산:
목차/구성 바뀌면 자동으로 다른 ISBN
- 책 살짝 수정 → 새 ISBN
- 옛 책 가진 사람: "이 ISBN 책이 없습니다"
명시적:
ISBN 을 일부러 같게 유지
- 작은 수정 후에도 같은 ISBN
- 옛 책 가진 사람도 호환
언제 새 ISBN?
- 책 내용 완전히 바뀜
- 구조 변경 (장 삭제 등)
- 의도적으로 호환성 깸
→ serialVersionUID = 버전 식별자.
1. serialVersionUID 의 정의와 역할
2. 자동 계산 vs 명시적 선언
3. InvalidClassException
4. 호환성 변경 vs 비호환성 변경
5. 변경 vs 유지의 선택
6. serialver 도구와 IDE
7. 보안 고려사항
8. 실무 전략과 함정
9. 면접 + 자기 점검 + Phase 9 졸업 시험
public class Shipment implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String blNo;
// ...
}
핵심:
private static final long 필드역할:
직렬화된 데이터의 클래스 버전 ↔ 역직렬화 시점의 클래스 버전 비교.
흐름:
1. 직렬화 시: serialVersionUID 도 함께 저장
2. 역직렬화 시: 저장된 값 vs 현재 클래스 값
3. 같으면: 진행
4. 다르면: InvalidClassException
목적:
- 옛 데이터 호환 보장
- 의도적 비호환은 명시
직렬화 시 저장되는 메타데이터:
[클래스 이름]
[serialVersionUID] ← 여기
[필드 목록]
[데이터]
역직렬화 시:
- 클래스 이름으로 클래스 찾기
- serialVersionUID 비교
- 일치하면 필드 매핑
- 데이터 복원
// 정확한 선언
private static final long serialVersionUID = 1L;
// 분석:
// - private: 외부 접근 X (선택)
// - static: 클래스 레벨 (인스턴스 X)
// - final: 변경 X
// - long: 64비트
// - 1L: long literal
// 변형 (덜 권장):
public static final long serialVersionUID = 1L;
// public — JVM 은 reflection 으로 접근, modifier 무관
static final long serialVersionUID = 1L;
// package-private — JVM 은 OK
// 하지만 private 권장 (관례)
컴파일러:
- serialVersionUID 가 명시되어 있으면 그 값 사용
- 없으면 자동 계산 (다음 섹션)
JVM:
- 직렬화 시 serialVersionUID 저장
- 역직렬화 시 클래스의 현재 값 vs 저장된 값
- 다르면 InvalidClassException
// V1 클래스
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
}
// 직렬화
User u = new User("Alice", 30);
oos.writeObject(u);
// 저장된 데이터에 serialVersionUID = 1 포함
// V2 클래스 (필드 추가, 같은 serialVersionUID)
public class User implements Serializable {
private static final long serialVersionUID = 1L; // 그대로
private String name;
private int age;
private String email; // 추가
}
// 역직렬화
User u = (User) ois.readObject();
// serialVersionUID 일치 (1 == 1)
// → 정상 (email 은 null)
// V3 (serialVersionUID 변경)
public class User implements Serializable {
private static final long serialVersionUID = 2L; // ★ 변경
private String name;
private int age;
}
// V1 데이터 역직렬화 시도
ois.readObject();
// ★ InvalidClassException
// "local class incompatible: stream classdesc serialVersionUID = 1,
// local class serialVersionUID = 2"
public class Shipment implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String blNo;
private String consignee;
private BigDecimal weight;
private LocalDateTime createdAt;
private ShipmentStatus status;
// 호환성 유지 위해 1L 유지
// 의도적 비호환 시에만 변경
}
public enum ShipmentStatus implements Serializable {
PENDING, SHIPPED, DELIVERED, CANCELLED;
// enum 은 자동 Serializable
// serialVersionUID 명시 안 해도 OK (관례적 명시 가능)
}
serialVersionUID 의 정의와 역할은?
답:
1. 정의:
역할:
목적:
선언:
private static final long serialVersionUID = 1L;serialVersionUID 명시 안 하면:
컴파일러가 자동 계산:
- 클래스 이름
- 클래스 modifier (public, final 등)
- implements 인터페이스 목록
- 필드 (이름, 타입, modifier)
- 메서드 (이름, 시그니처, modifier)
- 생성자
...
SHA-1 해시 → long 변환
작은 변경에도 값 달라짐!
// V1 — serialVersionUID 명시 X
public class User implements Serializable {
private String name;
private int age;
}
// 자동 계산: 예를 들어 0x123456789ABCDEF0L
// V2 — 필드 추가 (호환 변경)
public class User implements Serializable {
private String name;
private int age;
private String email; // 추가
}
// 자동 계산: 0xFEDCBA9876543210L (다름!)
// V1 데이터를 V2 코드로 역직렬화
// → InvalidClassException
// → 옛 데이터 못 읽음
// 단순 필드 추가만으로도 호환성 깨짐
// V1 — 명시
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
}
// V2 — 같은 값 유지
public class User implements Serializable {
private static final long serialVersionUID = 1L; // 그대로
private String name;
private int age;
private String email; // 추가
}
// V1 데이터를 V2 로 역직렬화
// → 정상
// → email = null (V1 에 없으니)
자동 계산이 위험한 이유:
1. 작은 변경에도 값 변경
- 메서드 추가
- private 메서드 시그니처 변경
- 인터페이스 추가
- import 변경 (간접 영향 X, 직접 영향 가능)
2. 컴파일러 버전에 의존
- JDK 11 컴파일 vs JDK 17 컴파일
- 같은 코드, 다른 값 가능 (드물지만)
3. 호환성 의도 표현 어려움
- "이건 호환됨" 명시 X
4. 디버깅 어려움
- 왜 호환 깨졌는지 모름
명시의 장점:
1. 의도 명확
- 같은 값 = 호환 유지
- 변경 = 의도적 비호환
2. 컴파일러 독립
- JDK 버전 무관
3. 유지 보수
- 작은 변경 시 호환
4. 컴파일 경고 회피
- 명시 안 하면 보통 경고
- "serializable class has no definition of serialVersionUID"
// IntelliJ, Eclipse 등
public class User implements Serializable {
// serialVersionUID 명시 X
// → 컴파일러 또는 IDE 경고
// ★ serializable class has no definition of serialVersionUID
}
// 해결:
// 1. 명시 (권장)
private static final long serialVersionUID = 1L;
// 2. @SuppressWarnings (비권장)
@SuppressWarnings("serial")
public class User implements Serializable {
// ...
}
// 3. IDE 의 자동 추가 기능
// "Add serialVersionUID"
// 항상 명시
public class Shipment implements Serializable {
private static final long serialVersionUID = 1L;
// 필드들
}
// IDE 단축키 (IntelliJ):
// Alt+Enter → "Add serialVersionUID field"
// 또는 Constants 클래스
public class SerialVersionUIDs {
public static final long SHIPMENT = 1L;
public static final long ORDER = 1L;
public static final long CUSTOMER = 1L;
}
// 사용
public class Shipment implements Serializable {
private static final long serialVersionUID = SerialVersionUIDs.SHIPMENT;
}
// 일관된 관리
자동 계산 vs 명시적 선언의 차이는?
답:
1. 자동 계산:
명시적:
private static final long serialVersionUID = 1L자동 계산의 위험:
권장:
public class InvalidClassException extends ObjectStreamException {
public String classname;
public InvalidClassException(String reason);
public InvalidClassException(String cname, String reason);
@Override
public String getMessage();
}
핵심:
InvalidClassException 의 주요 원인:
1. serialVersionUID 불일치
- 직렬화 시 V1
- 역직렬화 시 V2 (다른 UID)
2. 클래스 이름 변경
- 같은 클래스 위치, 다른 이름
3. 비 Serializable 부모로 변경
- 부모가 Serializable 였는데
- 새 부모는 X
4. Serializable 인터페이스 제거
- 클래스가 더 이상 Serializable X
5. 필드 타입 변경
- int → long 같은 변경
- serialVersionUID 가 같아도 별도 검사
6. 비 Serializable 부모의 기본 생성자 없음
- 부모에 기본 생성자 필수
일반적 메시지:
"java.io.InvalidClassException:
com.example.User;
local class incompatible:
stream classdesc serialVersionUID = 1,
local class serialVersionUID = 2"
분석:
- 클래스: com.example.User
- 데이터의 UID: 1
- 코드의 UID: 2
- 호환 X
해결:
- 코드의 UID 를 1로 (옛 데이터 읽기)
- 또는 데이터 재생성 (UID 2로)
- 또는 변환 로직
// V1 (데이터 생성)
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
}
User u = new User("Alice");
oos.writeObject(u);
// 파일: serialVersionUID = 1 + 데이터
// V2 (코드 변경)
public class User implements Serializable {
private static final long serialVersionUID = 2L; // ★ 변경
private String name;
}
// V1 데이터 읽기 시도
ois.readObject();
// InvalidClassException:
// stream classdesc serialVersionUID = 1,
// local class serialVersionUID = 2
// V1 — 명시 안 함 (자동 계산: 예를 들어 1234567)
public class User implements Serializable {
private String name;
private int age;
}
User u = new User("Alice", 30);
oos.writeObject(u);
// UID = 1234567
// V2 — 메서드 추가 (자동 재계산: 7654321)
public class User implements Serializable {
private String name;
private int age;
public String greet() { // 추가
return "Hello";
}
}
// V1 데이터 읽기
ois.readObject();
// InvalidClassException
// UID 가 자동으로 달라짐
// 메서드 추가만으로 호환성 깨짐!
// V1
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private int id; // int
}
// V2 — 같은 UID, 타입 변경
public class User implements Serializable {
private static final long serialVersionUID = 1L; // 같음
private long id; // ★ long 으로
}
// V1 데이터 읽기
ois.readObject();
// InvalidClassException
// "incompatible types"
// UID 가 같아도 필드 타입은 호환 X
// 옵션 1: 예외 잡고 fallback
try (ObjectInputStream ois = new ObjectInputStream(...)) {
User u = (User) ois.readObject();
} catch (InvalidClassException e) {
log.warn("Incompatible version, using default");
User u = new User(); // 기본
}
// 옵션 2: 변환 로직
public User readUser(ObjectInputStream ois) throws IOException {
try {
return (User) ois.readObject();
} catch (InvalidClassException e) {
if (e.getMessage().contains("serialVersionUID = 1")) {
// V1 데이터 → 변환
return readV1User(ois);
}
throw e;
}
}
// 옵션 3: 다른 형식으로 마이그레이션
// JSON, Protobuf 등으로
public class ShipmentLoader {
public Shipment loadShipment(Path path) {
try (ObjectInputStream ois = new ObjectInputStream(
Files.newInputStream(path))) {
return (Shipment) ois.readObject();
} catch (InvalidClassException e) {
log.error("Incompatible shipment data: {}", e.getMessage());
return migrateOrFail(path, e);
} catch (Exception e) {
throw new RuntimeException("Failed to load shipment", e);
}
}
private Shipment migrateOrFail(Path path, InvalidClassException e) {
// 옛 형식 감지
if (e.getMessage().contains("serialVersionUID = 1")) {
return migrateV1(path);
}
if (e.getMessage().contains("serialVersionUID = 2")) {
return migrateV2(path);
}
throw new RuntimeException("Unknown version", e);
}
private Shipment migrateV1(Path path) {
// V1 → 현재 버전 변환 로직
// ...
return new Shipment();
}
private Shipment migrateV2(Path path) {
// V2 → 현재 버전 변환 로직
return new Shipment();
}
}
InvalidClassException 의 원인은?
답:
1. 주요 원인:
메시지 분석:
처리:
예방:
호환성 변경 (Compatible Changes):
옛 데이터를 새 코드로 읽을 수 있는 변경.
serialVersionUID 유지 가능.
1. 새 필드 추가
- 옛 데이터엔 없음
- 역직렬화 시 기본값 (null/0/false)
2. 새 메서드 추가
- 직렬화 X
3. 새 인터페이스 구현
- 데이터 영향 X
4. 메서드 변경 (시그니처 동일)
- 구현만 다름
5. transient 필드 추가
- 직렬화 X
6. static 필드 추가
- 직렬화 X
비호환성 변경 (Incompatible Changes):
옛 데이터를 읽을 수 없는 변경.
serialVersionUID 변경 필요.
1. 필드 삭제
- 데이터에 있는데 클래스에 없음
- StreamCorruptedException
2. 필드 타입 변경
- int → long 등
- InvalidClassException
3. 필드 modifier 변경 (static, transient 등)
- 직렬화 영향 변경
- InvalidClassException
4. 클래스 이름 변경
- 다른 클래스로 인식
5. 패키지 이동
- 클래스 풀 패스 변경
6. 클래스 종류 변경
- 클래스 → 인터페이스 등
| 변경 | 호환? | 이유 |
|---|---|---|
| 새 필드 추가 | ✓ | 옛 데이터에 없음 (기본값) |
| 필드 삭제 | ✗ | 데이터에 있는데 클래스에 없음 |
| 필드 이름 변경 | ✗ | 다른 필드로 인식 |
| 필드 타입 변경 | ✗ | 형식 불일치 |
| 메서드 추가 | ✓ | 직렬화 영향 X |
| 메서드 삭제 | ✓ | 직렬화 영향 X |
| 메서드 변경 | ✓ | 시그니처 동일 시 |
| 인터페이스 추가 | ✓ | 큰 영향 X |
| Serializable 제거 | ✗ | 직렬화 불가 |
| 부모 클래스 변경 | 보통 ✗ | 필드 상속 영향 |
| transient 추가 | ✓ | 그 필드는 무시 |
| transient 제거 | 변경 | 필드 추가와 비슷 |
// V1
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
}
// V2 — 필드 추가
public class User implements Serializable {
private static final long serialVersionUID = 1L; // 같음 (호환)
private String name;
private int age;
private String email; // 추가
}
// V1 데이터 → V2 코드
// - name, age: 복원
// - email: null (기본값)
// 정상 동작
// V2 데이터 → V1 코드
// - email 필드는 V1 클래스에 없음
// - V1 클래스 로딩 후 email 데이터는 무시
// 정상 동작 (이론적 — 약간 까다로움)
// V1
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private String email;
}
// V2 — 필드 삭제
public class User implements Serializable {
private static final long serialVersionUID = 1L; // 같음 (위험)
private String name;
private int age;
// email 삭제
}
// V1 데이터 → V2 코드
// 동작은 함 (email 데이터 무시)
// 단, 데이터 손실
// serialVersionUID 변경 권장
// 더 안전:
// V2 — UID 변경
private static final long serialVersionUID = 2L;
// V1
public class Product implements Serializable {
private static final long serialVersionUID = 1L;
private int price; // int
}
// V2 — 타입 변경
public class Product implements Serializable {
private static final long serialVersionUID = 1L; // 같지만
private long price; // long ★
}
// V1 데이터 → V2 코드
// InvalidClassException:
// "incompatible types"
// 필드 타입 검증 별도
// 해결:
// 1. serialVersionUID 변경 + 변환 로직
// 2. 또는 writeObject/readObject 커스텀
public class Product implements Serializable {
private static final long serialVersionUID = 2L; // 변경
private long price;
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
// 옛 데이터 처리...
}
}
// V1
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
public String greet() {
return "Hello, " + name;
}
}
// V2 — 메서드 추가/변경
public class User implements Serializable {
private static final long serialVersionUID = 1L; // 같음
private String name;
public String greet() {
return "안녕, " + name + "!"; // 구현 변경
}
public String farewell() { // 메서드 추가
return "Bye, " + name;
}
}
// V1 데이터 → V2 코드
// 정상 — 메서드는 직렬화 영향 X
// 새 구현으로 호출됨
// V1 — 부모가 Serializable
class Animal implements Serializable {
String type;
}
class Dog extends Animal implements Serializable {
private static final long serialVersionUID = 1L;
String name;
}
// V2 — 부모가 Serializable X
class Animal { // Serializable 제거
String type;
}
class Dog extends Animal implements Serializable {
private static final long serialVersionUID = 1L; // 같음
String name;
}
// V1 데이터 → V2 코드
// 보통 비호환 (Animal 의 필드 처리 문제)
// 또는 NotSerializableException
// V1 — 초기 출시
public class Shipment implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String blNo;
}
// V2 — 필드 추가 (호환)
public class Shipment implements Serializable {
private static final long serialVersionUID = 1L; // 같음
private Long id;
private String blNo;
private String consignee; // 추가 (호환)
private BigDecimal weight; // 추가 (호환)
}
// V3 — 타입 변경 (비호환)
public class Shipment implements Serializable {
private static final long serialVersionUID = 2L; // 변경
private Long id;
private String blNo;
private String consignee;
private BigDecimal weight;
private LocalDateTime createdAt; // String → LocalDateTime (비호환)
// V1, V2 데이터를 V3 으로 마이그레이션 필요
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
ois.defaultReadObject();
if (createdAt == null) {
createdAt = LocalDateTime.now(); // 기본값
}
}
}
호환성 vs 비호환성 변경?
답:
1. 호환성 (UID 유지):
비호환성 (UID 변경):
판단 기준:
권장:
권장:
serialVersionUID 는 가능한 유지.
의도적으로 깰 때만 변경.
이유:
- 호환성 유지 → 데이터 손실 X
- 마이그레이션 비용 ↓
- 운영 안정성
언제 변경:
- 비호환 변경 발생
- 옛 데이터 읽을 수 없음을 명시
- 데이터 정리 후
변경 결정 체크리스트:
1. 필드 추가만?
→ UID 유지
2. 필드 삭제 또는 타입 변경?
→ UID 변경 권장
3. 옛 데이터 읽을 일 있나?
- 있으면: UID 유지 + 변환 로직
- 없으면: UID 변경 OK
4. 의도적 비호환?
→ UID 변경
5. 데이터 마이그레이션 완료?
→ UID 변경 OK
// V1 데이터 존재
// 옛 파일들: serialVersionUID = 1
// V2 코드 — UID 를 2 로
private static final long serialVersionUID = 2L;
// 영향:
// 1. V1 데이터 읽기 시 InvalidClassException
// 2. 옛 캐시 (Redis 등) 무효화
// 3. 메시지 큐의 옛 메시지 처리 불가
// 4. RMI 의 옛 클라이언트와 통신 불가
// 따라서:
// - 모든 옛 데이터 정리 후 변경
// - 또는 변환 로직 준비
// - 또는 다른 직렬화 방식 마이그레이션
// UID 유지하면서 호환성 위해:
// 1. transient 활용
public class Cache implements Serializable {
private static final long serialVersionUID = 1L;
private String key;
private transient byte[] cachedData; // 직렬화 X
}
// 2. readObject 커스텀
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String email; // V2 추가
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
ois.defaultReadObject();
if (email == null) {
email = "unknown@example.com";
}
}
}
// 3. 새 필드의 기본값
public class Order implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String orderStatus = "PENDING"; // 기본값
// V1 데이터에 없으면 PENDING
}
// 패턴 1: 초기 명시 (1L)
public class Initial implements Serializable {
private static final long serialVersionUID = 1L;
// 시작
}
// 패턴 2: 호환 변경 (UID 유지)
public class Compatible implements Serializable {
private static final long serialVersionUID = 1L; // 같음
private String existing;
private String newField; // 추가
}
// 패턴 3: 비호환 변경 (UID 증가)
public class Incompatible implements Serializable {
private static final long serialVersionUID = 2L; // 증가
private long id; // int → long
}
// 패턴 4: 큰 변경 (UID 큰 변화)
public class MajorChange implements Serializable {
private static final long serialVersionUID = 1000L; // 큰 변화
// 완전 다른 형식
}
serialVersionUID 의 값 전략:
1. 순차 증가
1L → 2L → 3L
- 단순
- 가독성 ↑
2. 날짜 기반
20240101L
- 변경 시점 명확
3. 빌드 번호
1_2024_01_01L
- 메이저 + 날짜
4. 무작위 (자동 계산 값)
0x1234567890ABCDEFL
- serialver 도구 결과
- 의미 없음 (단순 식별자)
권장:
- 1L 부터 시작
- 비호환 변경 시 증가
- 단순함이 가독성 ↑
// 첫 출시
public class Shipment implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String blNo;
}
// 첫 업데이트 (호환)
public class Shipment implements Serializable {
private static final long serialVersionUID = 1L; // 유지
private Long id;
private String blNo;
private String consignee; // 추가
private BigDecimal weight; // 추가
}
// 큰 변경 (비호환)
// 1. 마이그레이션 도구 작성
// 2. 모든 옛 데이터 변환
// 3. UID 변경
public class Shipment implements Serializable {
private static final long serialVersionUID = 2L; // 변경
private Long id;
private String blNo;
private String consignee;
private BigDecimal weight;
private LocalDateTime createdAt; // 형식 변경
private List<ShipmentItem> items; // 컬렉션 추가
// ...
}
// 또는 더 큰 변경 — JSON 으로 마이그레이션
// 자바 직렬화 사용 중단
serialVersionUID 변경 vs 유지의 선택은?
답:
1. 유지 (일반적):
변경 (비호환):
변경 결정:
권장 전략:
# JDK 제공 도구
$ serialver com.example.User
# 출력:
# com.example.User: static final long serialVersionUID = 1234567890123456789L;
# Java 11+ 부터는 jdk.jdeps 모듈
# 일부 배포판에서 제거됨
# 시나리오: 클래스의 자동 계산 UID 찾기
# 1. 컴파일
$ javac User.java
# 2. serialver
$ serialver com.example.User
com.example.User: static final long serialVersionUID = -8745890987654321L;
# 3. 복사하여 클래스에 추가
public class User implements Serializable {
private static final long serialVersionUID = -8745890987654321L;
// ...
}
IntelliJ:
1. 클래스 선언 위에 커서
2. Alt+Enter
3. "Add 'serialVersionUID' field"
4. 자동 계산된 값 추가
설정:
- Settings > Editor > Inspections > Java > Serialization issues
- "Serializable class without 'serialVersionUID'" 활성화
Eclipse:
1. 경고 표시 위 클릭
2. "Add generated serial version ID"
VS Code:
- 확장 (Extension Pack for Java)
- Quick Fix
자동 생성된 UID:
private static final long serialVersionUID = -8745890987654321L;
장점:
- "이 클래스의 시그니처 해시" 같은 의미
- 자동 계산 값과 같음 (이론적)
단점:
- 의미 없는 숫자
- 가독성 ↓
- 값 자체엔 정보 없음
단순 1L:
private static final long serialVersionUID = 1L;
장점:
- 단순, 가독성
- 버전 관리 명확
- 1, 2, 3... 순차
단점:
- 자동 계산 값과 다름 (의미 없는 차이)
권장: 1L (단순함이 좋음)
// 런타임에 자동 계산 값 확인 가능
import java.io.ObjectStreamClass;
ObjectStreamClass desc = ObjectStreamClass.lookup(User.class);
long uid = desc.getSerialVersionUID();
System.out.println("UID: " + uid);
// 명시 안 한 클래스의 자동 계산 값 보기
// 디버깅용
// 옵션 1: 명시 (권장)
public class A implements Serializable {
private static final long serialVersionUID = 1L;
}
// 옵션 2: 어노테이션
@SuppressWarnings("serial")
public class B implements Serializable {
// UID 없음
}
// 옵션 3: 컴파일 옵션 (전체 비활성, 비권장)
// javac -Xlint:-serial
// 권장:
// - 항상 명시
// - SuppressWarnings 는 최후의 수단
// 일관된 패턴
public class Shipment implements Serializable {
private static final long serialVersionUID = 1L;
// ...
}
public class Order implements Serializable {
private static final long serialVersionUID = 1L;
// ...
}
public class Customer implements Serializable {
private static final long serialVersionUID = 1L;
// ...
}
// 단순한 1L 로 시작
// 비호환 변경 시만 증가
// IDE 의 자동 추가 활용
serialver 도구와 IDE 의 활용은?
답:
1. serialver 도구:
IDE 자동 생성:
자동 vs 1L:
확인:
권장:
serialVersionUID 의 보안 측면:
1. 검증 기능 X
- 단순 식별자
- 악의적 데이터 검증 X
2. 클래스 위변조
- serialVersionUID 만 맞으면
- 클래스 내부 변경 가능
3. 직렬화 자체의 취약점
- serialVersionUID 와 무관
- 일반적 직렬화 보안 문제
→ serialVersionUID 는 호환성용,
보안용 X
// Java 9+ 의 보안 강화
import java.io.ObjectInputFilter;
// 글로벌 필터
ObjectInputFilter.Config.setSerialFilter(
"com.ilic.*;java.util.*;!*"
);
// com.ilic 패키지와 java.util 만 허용
// ! 는 거부
// 또는 ObjectInputStream 별
ObjectInputStream ois = new ObjectInputStream(in);
ois.setObjectInputFilter(filter -> {
Class<?> clazz = filter.serialClass();
if (clazz != null && clazz.getPackageName().startsWith("com.ilic.")) {
return ObjectInputFilter.Status.ALLOWED;
}
return ObjectInputFilter.Status.REJECTED;
});
// JEP 415 (Java 17)
// 컨텍스트별 필터
public class ShipmentDeserializer {
private static final ObjectInputFilter SHIPMENT_FILTER =
ObjectInputFilter.Config.createFilter(
"com.ilic.shipment.*;java.util.*;java.lang.*;!*"
);
public Shipment deserialize(InputStream in) throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(in)) {
ois.setObjectInputFilter(SHIPMENT_FILTER);
return (Shipment) ois.readObject();
}
}
}
public class SecureUser implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private String email;
private int age;
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
ois.defaultReadObject();
// 검증
validate();
}
private void validate() throws InvalidObjectException {
if (username == null || username.isEmpty()) {
throw new InvalidObjectException("Username required");
}
if (email == null || !email.contains("@")) {
throw new InvalidObjectException("Invalid email");
}
if (age < 0 || age > 150) {
throw new InvalidObjectException("Invalid age: " + age);
}
}
}
// ❌ 위험 — 외부 데이터 직접 역직렬화
public Object deserializeUnsafe(byte[] data) throws Exception {
try (ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(data))) {
return ois.readObject(); // ★ 임의 코드 실행 가능
}
}
// ✓ 안전 — 필터링
public Shipment deserializeSafe(byte[] data) throws Exception {
try (ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(data))) {
ois.setObjectInputFilter(filter -> {
Class<?> clazz = filter.serialClass();
if (clazz == null) return ObjectInputFilter.Status.ALLOWED;
String name = clazz.getName();
// 허용 목록 (whitelist)
if (name.equals("com.ilic.Shipment")) return ObjectInputFilter.Status.ALLOWED;
if (name.startsWith("java.lang.")) return ObjectInputFilter.Status.ALLOWED;
if (name.startsWith("java.util.")) return ObjectInputFilter.Status.ALLOWED;
return ObjectInputFilter.Status.REJECTED;
});
return (Shipment) ois.readObject();
}
}
// ✓✓ 더 안전 — 다른 형식
public Shipment deserializeFromJson(String json) {
return mapper.readValue(json, Shipment.class);
// JSON 은 코드 실행 위험 X
}
@Service
public class SecureSerializationService {
// 화이트리스트
private static final Set<String> ALLOWED_CLASSES = Set.of(
"com.ilic.Shipment",
"com.ilic.ShipmentItem",
"java.lang.String",
"java.lang.Long",
"java.math.BigDecimal",
"java.time.LocalDateTime",
"java.util.ArrayList"
);
public Shipment deserialize(byte[] data) throws IOException, ClassNotFoundException {
try (ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(data))) {
ois.setObjectInputFilter(filter -> {
Class<?> clazz = filter.serialClass();
if (clazz == null) return ObjectInputFilter.Status.ALLOWED;
String name = clazz.getName();
if (ALLOWED_CLASSES.contains(name) || name.startsWith("[")) {
return ObjectInputFilter.Status.ALLOWED;
}
log.warn("Rejected class: {}", name);
return ObjectInputFilter.Status.REJECTED;
});
return (Shipment) ois.readObject();
}
}
}
직렬화의 보안 고려는?
답:
1. serialVersionUID 의 한계:
ObjectInputFilter (Java 9+):
Java 17 의 Context-Specific:
readObject 검증:
신뢰 못 할 데이터:
serialVersionUID 권장 전략:
1. 항상 명시
- 자동 계산 의존 X
- private static final long serialVersionUID = 1L;
2. 단순 값
- 1L 부터 시작
- 비호환 시 2L, 3L
- 큰 숫자 (자동 계산 값) 안 좋음
3. 호환 유지
- 가능한 같은 UID
- 필드 추가 등은 OK
4. 비호환 시 변경
- 의도적 비호환
- 옛 데이터 무효화
5. 검증 추가
- readObject 커스텀
- 비즈니스 검증
6. 보안 필터
- ObjectInputFilter
- 화이트리스트
함정 1: 자동 계산 신뢰
// UID 명시 X
// 작은 변경에도 호환 깨짐
→ 항상 명시
함정 2: UID 자주 변경
// 비호환 시마다 1 증가 시 OK
// 단, 모든 옛 데이터 손실
→ 신중히 변경
함정 3: 큰 자동 계산 값
// IDE 가 자동 추가
// 가독성 ↓
→ 단순 1L 권장
함정 4: 외부 데이터 직접 역직렬화
// 보안 취약점
→ ObjectInputFilter 또는 다른 형식
함정 5: UID 만 보고 호환 판단
// UID 같아도 필드 타입 변경 시 비호환
→ 변경 시 체크리스트
함정 6: 부모 클래스 변경
// 자식의 UID 같아도
// 부모 변경으로 비호환 가능
→ 부모도 명시
함정 7: enum 의 직렬화
// enum 은 자동 Serializable
// UID 명시 가능하지만 큰 의미 X
→ 일반적으로 명시 안 함
// 전략 1: 점진적 UID 증가
// V1 → V2: UID 같음 (호환)
// V3: UID 증가 (비호환)
// + 변환 로직
public Shipment loadAny(Path path) {
try {
return (Shipment) ois.readObject(); // 현재 버전
} catch (InvalidClassException e) {
// 옛 버전 처리
return loadLegacy(path, e);
}
}
// 전략 2: JSON 마이그레이션
// 1단계: 자바 직렬화 + JSON 병행 쓰기
// 2단계: JSON 만 쓰기
// 3단계: 옛 자바 직렬화 데이터 일괄 변환
// 4단계: 자바 직렬화 코드 제거
// Phase 9 학습 종합 — 모든 패턴 통합
public class IoStrategy {
// 1. try-with-resources (Unit 9.1)
public void example1() throws IOException {
try (FileInputStream fis = new FileInputStream("file.txt")) {
// 자동 close
}
}
// 2. BufferedStream (Unit 9.2)
public void example2() throws IOException {
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("file.txt"))) {
// 8KB 버퍼
}
}
// 3. DataStream (Unit 9.3)
public void example3() throws IOException {
try (DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream("data.bin")))) {
dos.writeInt(42);
dos.writeUTF("Hello");
}
}
// 4. Serialization (Unit 9.4)
public void example4(Shipment s) throws IOException {
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(
new FileOutputStream("shipment.ser")))) {
oos.writeObject(s);
}
}
// 5. serialVersionUID (Unit 9.5)
static class MyClass implements Serializable {
private static final long serialVersionUID = 1L;
// ...
}
}
public class Shipment implements Serializable {
private static final long serialVersionUID = 1L; // Unit 9.5
private Long id;
private String blNo;
private String consignee;
private BigDecimal weight;
private LocalDateTime createdAt;
private transient byte[] cachedDocs; // Unit 9.4
// 검증
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
ois.defaultReadObject();
validate();
// transient 재초기화
this.cachedDocs = new byte[0];
}
private void validate() throws InvalidObjectException {
if (id == null || blNo == null) {
throw new InvalidObjectException("Required fields missing");
}
}
}
@Service
public class ShipmentSerializationService {
private static final Set<String> ALLOWED = Set.of(
"com.ilic.Shipment",
"java.lang.String",
"java.lang.Long",
"java.math.BigDecimal",
"java.time.LocalDateTime"
);
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())))) {
// 보안 필터
ois.setObjectInputFilter(filter -> {
Class<?> clazz = filter.serialClass();
if (clazz == null) return ObjectInputFilter.Status.ALLOWED;
if (ALLOWED.contains(clazz.getName())) return ObjectInputFilter.Status.ALLOWED;
return ObjectInputFilter.Status.REJECTED;
});
return (Shipment) ois.readObject();
}
}
}
실무 전략과 함정 종합?
답:
1. 권장 전략:
함정 7가지:
마이그레이션:
| Q | 핵심 답변 |
|---|---|
| serialVersionUID 정의? | 클래스의 버전 식별자 (long) |
| 명시 안 하면? | 자동 계산 (작은 변경에도 변경) |
| InvalidClassException 원인? | UID 불일치, 타입 변경 등 |
| 호환 변경? | 필드 추가, 메서드 추가/변경 |
| 비호환 변경? | 필드 삭제/타입 변경 |
| 언제 변경? | 의도적 비호환만 |
| serialver 도구? | 자동 계산 값 출력 |
| 1L vs 자동 계산? | 1L 권장 (단순) |
| ObjectInputFilter? | Java 9+ 보안 필터 |
| transient 와 관계? | UID 와 무관 (직렬화 제외 별도) |
Q1. try-with-resources 등장? → Java 7
Q2. 조건? → AutoCloseable 구현
Q3. close 순서? → LIFO (역순)
Q4. Suppressed Exception? → close 시 예외가 원래 가리는 문제 해결
Q5. Java 9+ 개선? → effectively final 자원
Q6. AutoCloseable vs Closeable? → Exception vs IOException
Q7. 컴파일러 처리? → 자동 finally + close
Q8. catch 와 함께? → 가능
Q9. 다중 자원? → 세미콜론
Q10. close 의 자동 호출? → 정상/예외 모두
Q11. BufferedInputStream 정의? → FilterInputStream, 8KB
Q12. 내부 상태? → buf, count, pos, markpos
Q13. fill 시점? → 버퍼 비면
Q14. 큰 read(byte[])? → 버퍼 우회, 직접 OS
Q15. flush 의 동작? → 버퍼 → OS
Q16. flush vs force? → 자바→OS vs OS→디스크
Q17. close 의 flush? → 자동
Q18. mark/reset 지원? → BufferedInputStream true
Q19. 1바이트 read 성능? → 일반보다 300배+
Q20. Decorator 패턴? → 같은 인터페이스, 기능 추가
Q21. DataInputStream 정의? → 기본 타입 입출력
Q22. 자바 엔디언? → Big-Endian
Q23. readUTF 형식? → 길이 2바이트 + Modified UTF-8
Q24. readUTF 한계? → 65535 바이트
Q25. Modified UTF-8 vs 표준? → null/Supplementary 차이
Q26. readFully? → 정확히 채움, EOFException
Q27. int 의 바이트 수? → 4
Q28. DataInput 인터페이스? → 다형성
Q29. RandomAccessFile? → 양방향 + Random Access
Q30. 바이너리 vs 텍스트? → 빠름 vs 가독성
Q31. 직렬화 정의? → 객체 ↔ 바이트
Q32. Serializable? → 마커 인터페이스
Q33. ObjectOutputStream.writeObject? → 객체 + 그래프
Q34. transient? → 직렬화 제외
Q35. transient 활용? → 비밀번호/캐시/자원
Q36. 객체 그래프? → 자동 추적
Q37. 순환 참조? → 자동 처리
Q38. writeObject 커스텀? → private 필수
Q39. 직렬화의 문제? → 보안/성능/호환/유연/캡슐화/디버깅
Q40. 대안? → JSON, Protobuf
Q41. serialVersionUID 정의? → 버전 식별자
Q42. 자동 계산? → 클래스 시그니처 해시
Q43. 명시 안 하면 위험? → 작은 변경에도 호환 깨짐
Q44. 권장 값? → 1L (단순)
Q45. InvalidClassException 원인? → UID 불일치
Q46. 호환 변경? → 필드 추가
Q47. 비호환 변경? → 필드 삭제/타입 변경
Q48. 언제 UID 변경? → 의도적 비호환
Q49. serialver 도구? → 자동 계산 값
Q50. ObjectInputFilter? → 보안 필터 (Java 9+)
50 / 50 → Phase 9 마스터
45-49 → 거의 마스터
40-44 → 복습
< 40 → Unit 9.1 ~ 9.5 재학습
Phase 9 — I/O 강화
Unit 9.1 — try-with-resources
- 자원 자동 관리
- AutoCloseable 인터페이스
- Suppressed Exception
- Java 9+ effectively final
Unit 9.2 — BufferedInputStream / BufferedOutputStream
- 8KB 버퍼
- Decorator 패턴
- flush 의 두 가지 효과
- mark/reset 지원
Unit 9.3 — DataInputStream / DataOutputStream
- 기본 타입 입출력
- Big-Endian
- Modified UTF-8
- readFully
Unit 9.4 — Serialization (직렬화)
- Serializable 마커
- ObjectInputStream/OutputStream
- 객체 그래프
- transient
- 6가지 문제
Unit 9.5 — serialVersionUID
- 버전 식별자
- 자동 계산 vs 명시
- 호환성 관리
- 보안 필터
1. 자원 안전 관리
- try-with-resources 자유자재
- AutoCloseable 구현
- 자원 누수 회피
2. 효율적 I/O
- BufferedStream 활용
- Decorator 결합
- 성능 최적화
3. 바이너리 형식 처리
- DataStream 활용
- 네트워크 프로토콜
- 엔디언 변환
4. 객체 영속성
- 직렬화 (필요 시)
- 한계와 보안 이해
- 현대 대안 활용
5. 버전 관리
- serialVersionUID 전략
- 호환성 평가
- 마이그레이션
6. 보안 의식
- ObjectInputFilter
- 검증 패턴
- 신뢰 못 할 데이터
✅ Phase 1 — Pass by Value (3 Unit)
✅ Phase 2 — 컬렉션 프레임워크 (6 Unit)
✅ Phase 3 — 해시의 원리 (4 Unit)
✅ Phase 4 — 추상화의 두 도구 (4 Unit)
✅ Phase 5 — 제네릭과 와일드카드 (5 Unit)
✅ Phase 6 — 객체 비교 (4 Unit)
✅ Phase 7 — I/O 시스템 큰 그림 (5 Unit)
✅ Phase 8 — Stream 실전 (6 Unit)
✅ Phase 9 — I/O 강화 (5 Unit) ← 여기, 완주
⏭ Phase 10 — 함수형 프로그래밍 (4 Unit)
총: 42/43 Unit (Phase 9 완주, 약 98%)
1. serialVersionUID 의 본질
2. 자동 vs 명시
3. 변경 vs 유지
🚀 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 9 완주
→ 자원 관리 + 버퍼링 + 바이너리 + 직렬화 + 버전 마스터
→ 자바 I/O 의 모든 측면 종합
3주차 마지막 Phase 는 자바의 함수형 프로그래밍.
Phase 10 — 함수형 프로그래밍 (4 Unit)
Unit 10.1 — 람다 표현식
- 람다의 정의
- 함수형 인터페이스
- 메서드 참조
Unit 10.2 — Stream API
- 중간 연산
- 최종 연산
- 병렬 Stream
Unit 10.3 — Collectors와 reduce (★ 마스터 깊이)
- Collectors 정밀
- reduce 의 종류
- groupingBy, partitioningBy
Unit 10.4 — Optional
- null 안전 처리
- Optional 활용
- 함정과 권장 패턴
✅ Phase 1 ~ 9 완주 (42 Unit)
⏭ Phase 10 — 함수형 프로그래밍 (4 Unit)
총: 42/43 Unit (약 98%)
🏆 Phase 9 완주 — I/O 강화 마스터 달성