꾸준한 스터디
article thumbnail

클래스가 Serializable을 구현하고 기본 직렬화 형태를 사용하면 다음 릴리스 때 버리려 한 현재의 구현에 종속된다.

즉, 기본 직렬화 형태를 버릴 수 없게 된다.

실제로 BigInteger 같은 일부 자바 클래스가 이 문제에 시달리고 있다.

 

먼저 고민해보고 괜찮다고 판단될 때만 기본 직렬화 형태를 사용하라

기본 직렬화형태는 유연성, 성능, 정확성 측면에서 신중히 고민한 후 합당할 때만 사용해야 한다.

직접 설계하더라도 기본 직렬화 형태와 거의 같은 결과가 나올 경우에만 기본 형태를 써야 한다.

어떤 객체의 기본 직렬화 형태는 그 객체를 루트로 하는 객체그래프의 물리적 모습을 나름 효율적으로 인코딩한다.

객체가 포함한 데이터들과 그 객체에서 시작해 접근할 수 있는 모든 객체를 담아내며, 심지어 이 객체들이 연결된 위상(topology)까지 기술한다.

그러나 이상적인 직렬화 형태라면 물리적이 모습과 독립된 논리적인 모습만을 표현해야 한다.

객체의 물리적 표현과 논리적 내용이 같다면 기본 직렬화 형태라도 무방하다.

 

 

기본 직렬화 형태에 적합한 후보

성명은 논리적으로 이름, 성, 중간이름이라는 3개의 문자열로 구성되며, 앞 코드의 인스턴스 필드들은 이 논리적 구성요소를 정확히 반영했다.

기본 직렬화 형태가 적합하다고 결정했더라도 불변식 보장과 보안을 위해 readObject 메서드를 제공해야 할 때가 많다.

위의 Name 클래스의 경우 readObject 메서드가 lastName과 firstName 필드가 null이 아님을 보장해야 한다.

Name 클래스의 모든 필드는 private임에도 문서화 주석이 달려있는데 이 필드들은 결국 클래스의 직렬화 형태에 포함되는 공개 API에 속하며 공개 API는 모두 문서화해야하기 때문이다.

private 필드의 설명을 API 문서에 포함하라고 자바독에 알려주는 역할은 @serial 태그가 한다.

@serial 태그로 기술한 내용은 API 문서에서 직렬화 형태를 설명하는 특별한 페이지에 기록된다.

 

 

 

기본 직렬화 형태에 적합하지 않은 클래스

이 클래스는 직렬화 형태에 적합하지 않은 예로 문자열 리스트를 표현하고 있다.

논리적으로 이 클래스는 일련의 문자열을 표현한다.

물리적으로는 문자열들을 이중 연결 리스트로 연결했다.

이 클래스에 기본 직렬화 형태를 사용하면 각 노드의 양방향 연결정보를 포함해 모든 Entry를 철두철미하게 기록한다.

 

객체의 물리적 표현과 논리적 표현의 차이가 클 때 기본 직렬화 형태를 사용하면 생기는 문제

  1. 공개 API가 현재의 내부 표현 방식에 종속된다
    • private 클래스인 StringList.Entry가 공개 API가 되어 다음 릴리스에서 내부 표현 방식이 바뀌더라도 StringList 클래스는 여전히 연결 리스트로 표현된 입력저리도 할 수 있어야 한다.
      즉, 연결리스트를 더 사용하지 않더라도 관련 코드를 절대 제거할 수 없다.
  2. 너무 많은 공간을 차지할 수 있다.
    • StringList의 직렬화 형태는 연결 리스트의 모든 Entry와 연결 정보까지 기록했지만 Entry와 연결 정보는 내부 구현에 해당하니 직렬화 형태에 포함할 가치가 없다.
    • 직렬화 형태가 너무 커져서 디스크에 저장하거나 네트워크로 전송하는 속도가 느려진다.
  3. 시간이 너무 많이 걸릴 수 있다.
    • 직렬화 로직은 객체 그래프의 위상에 관한 정보가 없으니 그래프를 직접 순회해볼 수밖에 없다.
  4. 스택 오버플로를 일으킬 수 있다.
    • 기본 직렬화 과정은 객체 그래프를 재귀 순회하는데, 이 작업은 중간 정도 크기의 객체 그래프에서도 스택 오버플로를 일으킬 수 있다.

 

 

합리적인 커스텀 직렬화 형태를 갖춘 StringList

StringList를 위한 합리적인 직렬화 형태는 단순히 리스트가 포함한 문자열의 개수를 적은 다음, 그 뒤로 문자열들을 나열하는 수준이면 될 것이다.

StringList의 물리적인 상세 표현은 배제한 채 논리적인 구성만 담는 것이다.

writeObject와 readObject가 직렬화 형태를 처리한다.

transient 한정자는 해당 인스턴스 필드가 기본 직렬화 형태에 포함되지 않는다는 표시다.

 

StringList의 필드 모두가 transient더라도 writeObject와 readObject는 각각 가장 먼저 defaultWriteObject와 defaultReadObject를 호출하는데 클래스 인트턴스 필드 모두가 transient 더라도 향후 릴리스에서 transient가 아닌 인스턴스 필드가 추가되었을 대 상위와 하위 모두 호환이 가능해지기 때문에 직렬화 명세는 이 작업을 무조건 하라고 요구한다.

신버전 인스턴스를 직렬화한 후 구버전으로 역직렬화하면 새로 추가되니 필드는 무시된다.

구버전 readObject 메서드에서 defaultReadObject를 호출하지 않는다면 역직렬화시 StreamCorruptedException이 발생한다.

 

writeObject 메서드는 private이지만 주석이 달린 이유는 앞서 private 필드가 공개 API된 이유와 같아서 이다.

메서드에 달린 @serialData 태그는 자바독 유틸리트에게 이 내용을 직렬화 형태 페이지에 추가하도록 요청한다.

개선한 StringList의 직렬화 형태는 원래 버전의 절반 정도의 공간을 차지한다.

그리고 스택 오버플로가 전혀 발생하지 않아 실질적으로 직렬화할 수 있는 크기 제한이 없어졌다.

 

StringList의 기본 직렬화 형태는 비록 유연성과 성능이 떨어지더라도 객체를 직렬화한 후 역직렬화 하면 원래 객체를 그 불변식까지 포함해 제대로 복원해내 정확하도고 할 수 있지만 그 불변식이 세부 구현에 따라 달라지는 객체는 이 정확성마저 깨질 수 있다.

해시테이블을 예로 물리적으로 키-값 엔트리들을 담은 해시 버킷을 나열한 형태인데

어떤 엔트리를 어떤 버킷에 담을지는 키에서 구한 해시코드가 결정하는데 그 계산 방식은 구현에 따라 달라질 수 있다.

계산 할 때마다 달라지기도 하여 해시테이블에 기본 직렬화를 사용하면 심각한 버그로 이어질 수 있다.

 

  • 기본 직렬화 여부에 상관없이 defaultWriteObject 메서드를 호출하면 transient로 선언하지 않은 모든 인스턴스 필드가 직렬화 된다.
  • transient로 선언해도 되는 인스턴스 필드에는 모두 transient 한정자를 붙여야 한다.
  • 캐시된 해시 값처럼 다른 필드에서 유도되는 필드나 JVM을 실행할 때 마다 값이 달라지는 필드처럼 객체의 논리적 상태와 무관한 필드라고 확신하면 transient 한정자를 생략해야한다.
  • 기본 직렬화를 사용한다면 transient 필드들은 역직렬화될 때 기본값으로 초기화 된다.
  • 기본값을 그대로 사용 안되면 readObject 메서드에서 defaultReadObject를 호출한 다음 해당 필드를 원하는 값으로 복원해라
  • 아니면 초기화시 값을 설정하는 방법도 있다.

 

책에서는 

해당 객체의 논리적 상태와 무관한 필드라고 확신할 때만 transient 한정자를 생략해야 한다.

 

라고 나와 있는데 논리적 상태와 무관한 필드라면 오히려 transient 한정자를 사용하여 직렬화되지 않도록 해야하는게 맞는게 아닌가 싶다.

객체의 논리적 상태가 확실한 필드라면 transient 한정자를 사용하지 말것을 신중히 생각해봐야 한다가 더 맞는 말인 것 같다. 원서를 봐야 알겠지만..

 

스터디 팀원이 책 회사에 문의한 결과

오역이 맞았다.

 

 

기본 직렬화 사용 여부와 상관없이 객체의 전체 상태를 읽는 메서드에 적용하는 동기화 메커니즘을 직렬화에도 적용해야한다.

모든 메서드를 synchronized로 선언하여 스레드 안전하게 만든 객체에서 기본 직렬화를 사용하려면 writeObject도 synchronized로 선언해야 한다.

기본 직렬화를 사용하는 동기화된 클래스를 위한 writeObject 메서드

 

 

SerialVersionUID

어떤 직렬화 형태를 택하든 직렬화 가능 클래스 모두에게 직렬 버전 UID를 명시적으로 부여하자

직렬 버전 UID를 명시하지 않으면 런 타임에 이 값을 생성하느라 복잡한 연산을 수행하지 때문이다.

 

직렬 버전 UID가 없는 기존 클래스를 구버전으로 직렬화된 인스턴스와 호환성을 유지한채 수정하고 싶다면 구버전에서 사용한 자동 생성도니 값을 그대로 사용하여야 한다.

이 값은 직렬화된 인스턴스가 존재하는 구버전 클래스를 serialver 유틸리티에 입력으로 주어 실행하면 얻을 수 있다.

기본 버전 클래스와의 호환성을 끊고 싶다면 단순히 직렬 버전 UID의 값을 바꿔주면 된다.

구버전으로 직렬화된 인스턴스들과 호환성을 끊으려는 경우를 제외하고 직렬 버전 UID를 절대 수정하지 말자.

 

 

핵심 정리

클래스를 직렬화하기로 했다면 어떤 직렬화 형태를 사용할지 심사숙고하기 바란다. 자바의 기본 직렬화 형태는 객체를 직렬화한 결과가 해당 객체의 논리적 표현에 부합할 때만 사용하고, 그렇지 않으면 객체를 적절히 설명하는 커스텀 직렬화 형태를 고안하라. 직렬화 형태도 공개 메서드를 설계할 때에 준하는 시간을 들여 설계해야 한다.

한번 공개된 메서드는 향후 릴리스에서 제거할 수 없듯이, 직렬화 형태에 포함된 필드도 마음대로 제거할 수 없다.

직렬화 호환성을 유지하기 위해 영원히 지원해야 하는 것이다. 잘못된 직렬화 형태를 선택하면 해당 클래스의 복잡성과 성능에 영구히 부정적인 영향을 남긴다.

 

 

https://devfunny.tistory.com/861

 

[교재 EffectiveJava] 아이템 87. 커스텀 직렬화 형태를 고려해보라

커스텀 직렬화 형태를 고려 Serializable을 구현하고 기본 직렬화 형태를 사용한다면 다음 릴리스 때 버리려한 현재의 구현에 영원히 발이 묶이게된다. 기본 직렬화 형태를 버릴 수 없게된다. 실제

devfunny.tistory.com

https://velog.io/@oyeon/%EC%95%84%EC%9D%B4%ED%85%9C87.-%EC%BB%A4%EC%8A%A4%ED%85%80-%EC%A7%81%EB%A0%AC%ED%99%94-%ED%98%95%ED%83%9C%EB%A5%BC-%EA%B3%A0%EB%A0%A4%ED%95%B4%EB%B3%B4%EB%9D%BC

 

아이템87. 커스텀 직렬화 형태를 고려해보라

클래스가 Serializable을 구현하고 기본 직렬화 형태를 사용한다면 현재의 구현에 종속적이게 된다. 즉, 기본 직렬화 형태를 버릴 수 없게 된다. 따라서 유연성, 성능, 정확성과 같은 측면을 고민한

velog.io

https://jaehun2841.github.io/2019/03/17/effective-java-item87/#%ED%95%A9%EB%A6%AC%EC%A0%81%EC%9D%B8-%EC%BB%A4%EC%8A%A4%ED%85%80-%EC%A7%81%EB%A0%AC%ED%99%94-%ED%98%95%ED%83%9C%EB%A5%BC-%EA%B0%96%EC%B6%98-StringList

 

Item 87. 커스텀 직렬화 형태를 고려해보라 | Carrey`s 기술블로그

서론 개발 일정에 쫓기는 상황에서는 API 설계에 노력을 집중하는 편이 낫다. 다음 릴리스에서 세부적인 기능을 제대로 구현하고 이번 릴리즈는 대충 동작만하게 하면 된다는 뜻이다. 하지만 클

jaehun2841.github.io

https://blog.yevgnenll.me/posts/consider-using-a-custom-serialized-form-effective-java-item-87

 

커스텀 직렬화 형태를 고려해보라 Effective Java 87

Serializable 을 implement 하기만 하면 직렬화를 맞출 수 있다. 하지만, 기본 직렬화가 커버하지 못하는 것이 있어 직접 private readObject, private writeObject 를 구현해야 할 경우가 있다. 언제 이러한 custom

blog.yevgnenll.me

 

profile

꾸준한 스터디

@StudyRecord

포스팅이 유익하셨다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!