Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Item 87. 커스텀 직렬화 형태를 고려해보라 #89

Open
yejin9858 opened this issue Sep 24, 2023 · 0 comments
Open

Item 87. 커스텀 직렬화 형태를 고려해보라 #89

yejin9858 opened this issue Sep 24, 2023 · 0 comments
Assignees

Comments

@yejin9858
Copy link

개발 일정에 쫒기는 상황에서는 그냥 동작만 하게 만들어놓는 방식 좋다.

하지만 Serializable을 구현하는 클래스를 시간이 없다고 기본 직렬화 형태를 사용한다면 다음 릴리즈 때 버리려 한 현재의 구현에 영원히 발이 묶이게 된다.

실제로도 BigInteger 같은 클래스는 이 문제에 시달리고 있다.

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

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

적합한 예시

public class Name implements Serializable{
	private final String lastName;
	private final String firstName;
	private final String middleName;
...
}
  • 코드가 논리적 구성 요소를 완벽히 반영함
  • 추가로 불변식 보장과 보안을 위해 readObject 메서드 제공해야 한다. 여기서는 readObject가 lastName, firstName이 Null 이 아님을 보장해야한다.

적합하지 않은 예시

public final class StringList implements Serializable{
	private int size = 0;
	private Entry head = null;
	private static class Entry implements Serializable {
		String data;
		Entry next;
		Entry previous;
	}
}
  • 물리적으로는 문자열들을 이중 연결 리스트로 연결했지만, 논리적으로 이 클래스는 일련의 문자열을 표현한다.

만약 객체의 물리적 표현과 논리적 표현의 차이가 클 경우, 기본 직렬화 형태를 사용하?

  1. 공개 API가 현재의 내부 표현 방식에 영구히 묶인다.

    Entry가 공개가 되어버린다.

    다른 릴리즈에서 내부 표현 방식을 바꾸더라도 StringList는 여전히 연결 리스트로 표현된 입력도 처리할 수 있어야 함

    = 연결리스트를 더는 사용하지 않아도 관련 코드를 지울 수 없음

  2. 너무 많은 공간을 차지할 수 있다.

    Entry와 연결 정보는 사실 내부 구현에 해당하므로 직렬화에 포함할 가치가 없다.

    이는 저장, 통신 속도 저하로 이어진다.

  3. 시간이 너무 많이 걸릴 수 있다.

    객체 그래프의 위상에 대한 정보가 없어 직접 그래프를 순회해볼 수 ㅏㅂㄲ에 없다.

  4. 스택 오버플로우를 일으킬 수 있다.

StringList는 무자열을 나열하는 정도의 직렬화면 충분할 것 같다.

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

private final class StringList implements Serializable{
	private transient int size = 0;
	private transient Entry head = null;

	//직렬화 하지 않음!
	private static class Entry {
		String data;
		Entry next;
		Entry previous;
	}

	//직렬화
	private void writeObject(ObjectOutputStream s) throws IOException {
		s.defaultWriteObject();
		s.writeInt(size);
		
		for(Entry e = head ; e != null ; e = e.next)
			s.writeObject(e.data)
	}
	
	private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
		s.defaultReadObject();
		int numElements = s.readInt();
		
		for(int i = 0 ; i < numElements; i++){
			add((String) s.readObject());
		}
	}

	...
}

transient : 일시적이다. 해당 인스턴스 필드가 기본 직렬화 형태에 포함되지 않는다.

transient여도 writeObject와 readObject로 defaultWriteObject, defaultReadObject를 호출하는 작업은 꼭 필요하다. → 향후 transient가 아닌 인스턴스 필드가 추가되더라도 호환된다.

ex) 신버전 인스턴스를 직렬화 한 후 구버전으로 역직렬화하면 새로 추가된 필드들은 무시될 것이다.

하지만 defaultReadObject가 호출되지 않으면 역직렬화에서 StreamCorruptedException이 발생할 것이다.

이전보다 개선 버전의 StringList는 문자열의 길이가 평균 10일 때 공간 절반, 속도 두 배가 된다.

또한 스택 오버플로가 발생하지 않는다.

StringList보다 더 심한 클래스도 있다.

유연성과 성능이 떨어졌을 뿐이지 객체를 직렬화 한 후 역직렬화 하면 원래 객체르 륵 불변식가지 포함해 제대로 복원해내긴 한다. → 정확한 객체

하지만 그 불변식이 세부 구현에 따라 달라지는 객체가 있다.

ex) 해시테이블

물리적으로는 키-값 엔트리를 나열한 형태

논리적으로는 어떤 엔트리를 어떤 버킷에 담을지 키에서 구한 해시코드가 결정함. 그 계산 방식은 구현에 따라, 계산할 때마다 달라지기도 함

→ 해시테이블에 기본 직렬화 = 심각한 버그

기본 직렬화를 수용하든 하지않든 defaultWriteObject 메서드를 호출하면 transient로 선언하지 않은 모든 인스턴스 필드가 직렬화된다.

따라서 transient로 선언해도 되는 인스턴스 필드에서는 모두 transient 한정자를 붙여야 한다.

  • 계산된 해시 값처럼 다른 필드에서 유도되는 필드
  • JVM을 실행할 때마다 값이 달라지는 필드
    ex) 네이치브 자료구조를 가리키는 long 필드

등이 이에 해당한다.

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

→ 커스텀 직렬화에서는 대부분의 인스턴스 필드를 transient로 선언해야 한다.

기본 직렬화를 사용할 때 transient 필드들은 역직렬화될 때 기본값(null, false, 0)으로 초기화 된다.

기본값을 그대로 사용해서는 안된다면 defaultReadObject에서 원하는 값으로 복원하거나, 처음 사용할 때 초기화 하자(Item 83)

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

모든 메서드를 syncronized로 선언하여 스레드 안전하게 만든 객체(Item 82) 에서 기본 직렬화를 사용하려면 writeObject도 syncronized 선언하라.

private syncronized void writeObject(ObjectOutputStream s) throws IOException {
		s.defaultWriteObject();

	}

writeObject 메서드 안에서 동기화하고 싶다면 클래스의 다른 부분에서 사용하는 락 순서를 똑같이 따라야 한다. → 자원순서교착상태

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

  • 직렬버전 UID의 잠재적인 호환성 문제가 사라짐
  • 런타임에 이 값을 생성하느라 복잡한 연산을 수행하지 않음 → 성능 개선
private static fianl long serialVersionUID = [무작위값];

고유할 필요 없다.

기존 버전 클래스와 호환성을 끊고 싶다면 단순히 직렬 버전 UID의 값을 바꿔줘라

하지만 이런 이유가 아니면 절대 수정하지 마라. → InvaildClassException

@yejin9858 yejin9858 self-assigned this Sep 24, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant