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이나 다른 직렬화 방식을 사용하는 것이 좋습니다.