Java Serializable
기본 개념
Serializable은 객체를 바이트 스트림으로 변환할 수 있다고 JVM에게 알려주는 마커 인터페이스입니다.
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name; // 직렬화 됨
private int age; // 직렬화 됨
private static String company; // 직렬화 안됨 (static)
private transient String pwd; // 직렬화 안됨 (transient)
}
serialVersionUID 상세
// 1. 명시적 선언 (권장)
private static final long serialVersionUID = 1L;
// 2. 없으면 컴파일러가 자동 생성
// - 클래스명, 필드명, 메소드 시그니처 등을 해시값으로 계산
// - 클래스 구조가 조금만 바뀌어도 값이 달라짐
버전 호환성 예시:
// Version 1
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
}
// Version 2 - 필드 추가해도 serialVersionUID 같으면 호환
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age = 0; // 기본값으로 처리됨
}
상속 관계에서의 직렬화
1. 부모가 Serializable인 경우
public class Parent implements Serializable {
private String parentField;
}
public class Child extends Parent {
private String childField; // 자동으로 직렬화 가능
}
2. 자식만 Serializable인 경우
public class Parent {
private String parentField;
// 기본 생성자 필수! (역직렬화시 부모 객체 생성용)
public Parent() { }
}
public class Child extends Parent implements Serializable {
private String childField;
// parentField는 직렬화되지 않음!
}
직렬화 제외 방법
public class User implements Serializable {
private String name; // 직렬화 O
private transient String password; // 직렬화 X
private static String appVersion; // 직렬화 X (static)
private final transient Logger logger = // 직렬화 X
LoggerFactory.getLogger(User.class);
}
커스텀 직렬화 메소드
public class User implements Serializable {
private String name;
private transient String encryptedPassword;
// 직렬화시 호출
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject(); // 기본 직렬화 먼저
oos.writeObject(encrypt(encryptedPassword)); // 암호화해서 저장
}
// 역직렬화시 호출
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
ois.defaultReadObject(); // 기본 역직렬화 먼저
encryptedPassword = decrypt((String) ois.readObject());
}
}
실제 사용 사례
1. 세션 클러스터링
@Entity
public class UserSession implements Serializable {
private static final long serialVersionUID = 1L;
private String userId;
private LocalDateTime loginTime;
private transient Connection dbConn; // DB 연결은 제외
}
2. Redis 캐싱
@RedisHash("product")
public class ProductCache implements Serializable {
private static final long serialVersionUID = 1L;
private String productId;
private String name;
private BigDecimal price;
}
3. 메시징 시스템
@Component
public class OrderService {
public void processOrder(Order order) {
// Order가 Serializable이어야 메시지 큐에 전송 가능
rabbitTemplate.convertAndSend("order.queue", order);
}
}
성능 및 보안 이슈
성능 문제
// Java 기본 직렬화 - 느리고 용량 큼
User user = new User("김철수", 30);
// 직렬화된 크기: 약 200-300 bytes
// JSON 직렬화 - 빠르고 가벼움
String json = objectMapper.writeValueAsString(user);
// JSON 크기: 약 30-50 bytes
보안 취약점
- 역직렬화 공격: 악의적인 바이트 스트림으로 임의 코드 실행 가능
- 가젯 체인: 클래스패스의 다른 클래스들을 이용한 공격
현재 대안들
1. JSON 직렬화 (주류)
// Jackson 사용
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(user);
User user = mapper.readValue(json, User.class);
2. Protocol Buffers
// protobuf 사용 - 빠르고 효율적
UserProto.User user = UserProto.User.newBuilder()
.setName("김철수")
.setAge(30)
.build();
3. Apache Avro
// 스키마 진화 지원
User user = User.newBuilder()
.setName("김철수")
.setAge(30)
.build();
언제 사용할까?
사용하는 경우 ✅
- 레거시 시스템 연동
- Java RMI 사용
- 세션 클러스터링
- JMS 메시징
- 분산 캐시 (Hazelcast 등)
사용하지 않는 경우 ❌
- REST API (JSON 사용)
- 마이크로서비스 통신
- 새로운 프로젝트
- 성능이 중요한 시스템
- 보안이 중요한 시스템
결론: Serializable은 Java 생태계의 레거시 기술로, 특별한 요구사항이 없다면 JSON이나 다른 직렬화 방식을 사용하는 것이 좋습니다.