Post

serialVersionUID와 InvalidClassException

serialVersionUID와 InvalidClassException

[!info] 직렬화, 역직렬화는 보통 json을 이용해서 처리하게 되고, 그렇게 하는 것이 좋아보인다.
json이라는 명확한 표준이 있어 이식성도 좋고, 변환 로직도 심플해서 아래와 같은 잠재적인 문제를 피할 수 있기 때문이다.

Java Serialization의 단점

  • 어떤 사정으로 인해 json serialization 하지 못하는 경우, java에서는 Serializable 구현하고 이를 이용해서 직렬화 하게 되는데…
  • 직렬화/역직렬화 시, 클래스와 객체의 동일성 판단이 json에 비해 매우 민감하기 때문에 주의해야 한다.
1
2
3
4
5
6
7
8
import dev.umbum.Address;

// 변경 전 User
public class User implements Serializable {
    private String name;
    private String email;
    private Address address;
}

User 클래스의 객체를 직렬화 해서 redis에 올린 다음, Address의 위치가 변경되어(내용은 변경되지 않았다) import 주소를 아래와 같이 변경하면, 역직렬화 시 InvalidClassException 발생한다.

1
2
3
4
5
6
7
8
import dev.umbum.model.Address;

// 변경 후 User
public class User implements Serializable {
    private String name;
    private String email;
    private Address address;
}
  • 필드가 아무것도 변경되지 않았기 때문에 역직렬화 성공할 것이라고 예상하지만, import 구문만 달라져도 역직렬화에 실패한다.
  • int -> long으로 변경하는 것도 json이라면 호환이 되지만, java 직렬화는 InvalidClassException 발생한다.

호환이 된다/안된다 판단은? serialVersionUID 참고

  • 호환 가능 판단은 serialVersionUID를 보고 판단하게 되는데, 명시적으로 지정하지 않더라도 Serializable 인터페이스가 있으면 컴파일러가 알아서 계산하게 된다.
    • 클래스, 필드, 메서드 등 종합적으로 보고 컴파일러가 계산하며 컴파일러 세부 구현에 따라 값이 또 다를 수 있다는게 단점이다.
  • import 구분을 변경하거나, type을 변경하는 등 Incompatible changes가 있었다면, 생성되는 serialVersionUID 값이 달라지게 된다.
    • e.g. -2649918656647101333 -> 3656463606950018647 로 변경
  • 어떤 변경이 serialVersionUID 값을 달라지게 만드는지는, compatible, Incompatible 변경 spec 참고
  • 그래서 docs 에서는 아예, serialVersionUID를 지정해서 처리하는 것을 권장하고 있다.

It is strongly recommended that all serializable classes explicitly declare serialVersionUID values, since the default serialVersionUID computation is highly sensitive to class details that may vary depending on compiler implementations, and can thus result in unexpected serialVersionUID conflicts during deserialization, causing deserialization to fail.

직접 serialVersionUID를 생성 할 수 있는 방법이 있을까?

Java Serialization으로 인한 장애 경험

  1. 단지 정보 조회가 너무 무거워서 단지 키를 cache 키로 써서 cache 적재하고 있었다.
  2. 우선 단지DTO가 import만 달라졌는데, 캐시 데이터와 형식 불일치가 날 것이라고 예상하지 못했다.
  3. 배포 중 달라진 import로 인해 역직렬화 실패가 지속 발생했다.
  4. 배포 전략은 rolling이 아니라 blue/green이었는데, 때문에 여기까지는 큰 문제가 아니다.
    • 배포 직후 cache에 적재되어 있는 데이터의 역직렬화가 실패하고 나면, 새로운 캐시를 적재하기 때문에 잠시 뒤 문제가 해결되기 때문이다. (서비스 장애 까지는 가지 않는다.)
    • 배포 시 잠시 동안 발생하는 cache miss는 미리 감안하고 배포하는 경우가 종종 있다.
  5. 하지만 클러스터가 이중화 되어 있었다는 점 때문에 실제로 서비스 지연이 발생했었는데, P 클러스터와 C 클러스터가 각각 blue/green 배포였기 때문에, 사실상 서버군 2개의 rolling 배포였기 때문이다.
    • P 클러스터가 배포 완료되면 역직렬화 실패하고, 새로운 serialVersionUID로 cache 적재한다.
    • C 클러스터는 아직 배포 전이기 때문에, 여기서 다시 역직렬화 실패하고 기존의 serialVersionUID로 다시 cache를 적재한다.
    • 이 과정이 반복된다.

물론 C 클러스터까지 완전히 배포가 완료되면, 곧 신규 데이터로 캐시가 모두 갱신이 될 것이므로 에러는 사라지게 되겠지만, 그 동안 캐시 삭제/삽입이 지속 발생하여 서비스 지연 및 에러 알림 받게 된다.

참고

  • https://kwonnam.pe.kr/wiki/web/%EC%8B%A0%EA%B7%9C%EC%84%9C%EB%B9%84%EC%8A%A4#backend_%EC%BA%90%EC%8B%9C
  • https://techblog.woowahan.com/2645/
This post is licensed under CC BY 4.0 by the author.