Open yejin9858 opened 1 year ago
어떤 클래스의 인스턴스 직렬화 → 클래스 선언에 implements Serializable 붙이기
직렬화는 손쉬워보인다. 하지만 그렇지 않다.
클래스가 Serializable을 구현함
→ 직렬화된 바이트 스트림 인코딩(직렬화 형태)도 하나의 공개 API가 된다.
→ 클래스가 널리 퍼짐
→ 직렬화 형태도 영원히 지원해야 함
→ 커스텀 직렬화 형태를 설계하지 않고 자바의 기본 방식을 사용하면 직렬화 형태는 최ch 적용 당시 클래스의 내부 구현 방식에 영원히 묶여버린다.
커스텀 직렬화와 자바의 기본 방식 직렬화
직렬화를 커스텀 하면 일부 필드만 직렬화 할 수 있어 더 유연하고, 빠르다
기본 직렬화 형태에서는 클래스의 private와 package-private 인스턴스 필드들을 다 공개하게 된다. = 캡슐화가 깨진다. = Item 15 필드들의 접근을 최대한 막아 정보를 은닉하라 어김
문제를 깨닫고 뒤늦게 클래스 내부 구현을 손보면 원래의 직렬화 형태와 달라지게 된다.
ex) 한 쪽은 구버전 인스턴스를 직렬화하고 다른 쪽은 신버전 클래스로 역직렬화 ⇒ 실패
원래의 직렬화 형태를 유지하면서 내부 표현을 바꿀 수도 있지만, 어렵고 코드 더러워짐
⇒ 직렬화 가능 클래스를 만들고자 한다면 길게 보고 감당할 수 있을 만큼 고품질의 직렬화 형태도 주의해서 함께 설계해야 한다. (Item 87의 커스텀 직렬화 방법, Item 90의 인스턴스 직렬화 대신 프록시 직렬화 참고)
직렬화가 클래스 개선을 방해한다.
스트림 고유 식별자 UID
모든 직렬화된 클래스는 고유 식별 변호를 부여받는다. : serialVersionUID 필드(명시하지 않았다면 자동생성)
이 값을 생성하는 데에는 클래스 이름, 구현한 인터페이스들, 컴파일러가 자동을 생성해 넣은 것을 포함한 대부분의 클래스 멤버들이 고려됨
⇒ 메서드가 추가되거나 이들 중 하나라도 수정되면 UID 값이 변경됨
⇒ 직렬 버전의 UID 값도 변함 → 자동 생성되는 값에 의존하여 호환성이 깨진다.(InvaildClassException)
Item 85에서도 언급됨 - 직렬화를 조심하라
객체는 생성자를 사용해 만드는 게 기본인데, 직렬화는 언어의 매커니즘을 우회하는 객체 생성 기법이다.
직렬화는 생성자를 전면에 드러내지 않음으로써
을 위반하기 쉬움
⇒ 기본 역직렬화는 불변식 깨짐과 허가되지 않은 접근에 쉽게 노출되게 만든다.
를 추가적으로 검사해야함
커스텀 직렬화 형태를 잘 설계함으로써 테스트 부담을 줄일 수는 있음
하지만
라면 선택의 여지가 없다.
Serializable은 구현에 따르는 비용이 적지 않으니, 클래스 설계시마다 그 이득과 비용을 잘 저울질해야한다.
ex) BigInteger와 Instant 같은 ‘값’ 클래스와 컬렉션 클래스들은 Serializable로 구현되어있다.
스레드 풀처럼 ‘동작’하는 객체를 표현하는 클래스들은 대부분 Serializable을 구현하지 않았다.
인터페이스도 대부분 Serializable을 확장해서는 안된다.
(Item 19 상속을 고려해 설계하고 문서화하라, 그렇지 않으면 상속을 금지하라)
Serializable을 구현한 클래스만 지원하는 프레임워크를 사용하는 상황같은 어쩔 수 없는 상황이 아니라면 지키자
ex) 상속용으로 설계된 클래스 중 Throwable과 Component가 Serializable을 구현함
하지만 Swing과 AWT가 널리 쓰이던 시절에도 이런 용도로 거의 쓰이지 않았다.
인스턴스 필드 값 중 불변식을 보장해야 할 게 있다면 반드시 하위 클래스에서 finalize 메서드를 재정의하지 못하게 해야한다.
finalize 메서드를 자신이 재정의하면서 final로 선언해야한다. 이렇게 해두지 않으면 finalizer 공격을 당할 수 있다
인스턴스 필드 중 기본값으로 초기화하면 위배되는 불변식이 있다면
private void readObjectNoData() throws InvalidObjectException{ throw new InvalidObjectException("스트림 데이터가 필요합니다."); }
이 메서드를 추가해주자.
기존의 직렬화 가능 클래스에 직렬화 가능 상위 클래스를 추가하는 드문 경우를 위한 메서드다.
상속용 클래스인데 직렬화를 지원하지 않으면 그 하위 클래스에서 직렬화를 지원하려 할 때 부담이 늘어난다.
보통은 이런 클래스를 역직렬화 하려면 그 상위 클래스는 매개변수가 없는 생성자를 제공해야 함
→ 제공하지 않으면 어쩔 수 없이 직렬화 프록시 패턴을 사용해야 함
내부 클래스에는 바깥 인스턴스의 참조와 유효 범위 안의 지역변수 값들을 저장하기 위해 컴파일ㄹ러가 생성한 필드들이 자동으로 추라된다.
익명 클래스와 지역 클래스의 이름을 짓는 규칙이 언어 명세에 나와있지 않듯, 이 플드들이 클래스 정의에 어떻게 추가되는지도 정의되지 않았다.
즉, 내부 클래스에 대한 기본 직렬화 형태는 분명하지 않다
↔ 정적 멤버 클래스는 Serializable 구현해도 됨
Item 86 Serializable을 구현할지는 신중히 결정하라
어떤 클래스의 인스턴스 직렬화 → 클래스 선언에 implements Serializable 붙이기
직렬화는 손쉬워보인다. 하지만 그렇지 않다.
1. Serializable을 구현하면 릴리스한 뒤에는 수정하기 어렵다.
클래스가 Serializable을 구현함
→ 직렬화된 바이트 스트림 인코딩(직렬화 형태)도 하나의 공개 API가 된다.
→ 클래스가 널리 퍼짐
→ 직렬화 형태도 영원히 지원해야 함
→ 커스텀 직렬화 형태를 설계하지 않고 자바의 기본 방식을 사용하면 직렬화 형태는 최ch 적용 당시 클래스의 내부 구현 방식에 영원히 묶여버린다.
커스텀 직렬화와 자바의 기본 방식 직렬화
직렬화를 커스텀 하면 일부 필드만 직렬화 할 수 있어 더 유연하고, 빠르다
기본 직렬화 형태에서는 클래스의 private와 package-private 인스턴스 필드들을 다 공개하게 된다. = 캡슐화가 깨진다. = Item 15 필드들의 접근을 최대한 막아 정보를 은닉하라 어김
문제를 깨닫고 뒤늦게 클래스 내부 구현을 손보면 원래의 직렬화 형태와 달라지게 된다.
ex) 한 쪽은 구버전 인스턴스를 직렬화하고 다른 쪽은 신버전 클래스로 역직렬화 ⇒ 실패
원래의 직렬화 형태를 유지하면서 내부 표현을 바꿀 수도 있지만, 어렵고 코드 더러워짐
⇒ 직렬화 가능 클래스를 만들고자 한다면 길게 보고 감당할 수 있을 만큼 고품질의 직렬화 형태도 주의해서 함께 설계해야 한다. (Item 87의 커스텀 직렬화 방법, Item 90의 인스턴스 직렬화 대신 프록시 직렬화 참고)
직렬화가 클래스 개선을 방해한다.
스트림 고유 식별자 UID
모든 직렬화된 클래스는 고유 식별 변호를 부여받는다. : serialVersionUID 필드(명시하지 않았다면 자동생성)
이 값을 생성하는 데에는 클래스 이름, 구현한 인터페이스들, 컴파일러가 자동을 생성해 넣은 것을 포함한 대부분의 클래스 멤버들이 고려됨
⇒ 메서드가 추가되거나 이들 중 하나라도 수정되면 UID 값이 변경됨
⇒ 직렬 버전의 UID 값도 변함 → 자동 생성되는 값에 의존하여 호환성이 깨진다.(InvaildClassException)
2. 버그와 보안 구멍이 생길 위험이 높아진다.
Item 85에서도 언급됨 - 직렬화를 조심하라
객체는 생성자를 사용해 만드는 게 기본인데, 직렬화는 언어의 매커니즘을 우회하는 객체 생성 기법이다.
직렬화는 생성자를 전면에 드러내지 않음으로써
을 위반하기 쉬움
⇒ 기본 역직렬화는 불변식 깨짐과 허가되지 않은 접근에 쉽게 노출되게 만든다.
3. 해당 클래스의 신버전을 릴리즈할 때 테스트 할 것이 늘어난다.
를 추가적으로 검사해야함
커스텀 직렬화 형태를 잘 설계함으로써 테스트 부담을 줄일 수는 있음
⇒ Serializable 구현 여부는 가볍게 결정할 문제가 아니다
하지만
라면 선택의 여지가 없다.
Serializable은 구현에 따르는 비용이 적지 않으니, 클래스 설계시마다 그 이득과 비용을 잘 저울질해야한다.
ex) BigInteger와 Instant 같은 ‘값’ 클래스와 컬렉션 클래스들은 Serializable로 구현되어있다.
스레드 풀처럼 ‘동작’하는 객체를 표현하는 클래스들은 대부분 Serializable을 구현하지 않았다.
상속용으로 설계된 클래스는 대부분 Serializable을 구현해서 안되며
인터페이스도 대부분 Serializable을 확장해서는 안된다.
(Item 19 상속을 고려해 설계하고 문서화하라, 그렇지 않으면 상속을 금지하라)
Serializable을 구현한 클래스만 지원하는 프레임워크를 사용하는 상황같은 어쩔 수 없는 상황이 아니라면 지키자
ex) 상속용으로 설계된 클래스 중 Throwable과 Component가 Serializable을 구현함
하지만 Swing과 AWT가 널리 쓰이던 시절에도 이런 용도로 거의 쓰이지 않았다.
직렬화와 확장이 모두 가능한 클래스나 인스턴스 필드 사용시 주의할 점
인스턴스 필드 값 중 불변식을 보장해야 할 게 있다면 반드시 하위 클래스에서 finalize 메서드를 재정의하지 못하게 해야한다.
finalize 메서드를 자신이 재정의하면서 final로 선언해야한다. 이렇게 해두지 않으면 finalizer 공격을 당할 수 있다
인스턴스 필드 중 기본값으로 초기화하면 위배되는 불변식이 있다면
이 메서드를 추가해주자.
기존의 직렬화 가능 클래스에 직렬화 가능 상위 클래스를 추가하는 드문 경우를 위한 메서드다.
Serializable을 구현하지 않기로 했을 때 주의할 점
상속용 클래스인데 직렬화를 지원하지 않으면 그 하위 클래스에서 직렬화를 지원하려 할 때 부담이 늘어난다.
보통은 이런 클래스를 역직렬화 하려면 그 상위 클래스는 매개변수가 없는 생성자를 제공해야 함
→ 제공하지 않으면 어쩔 수 없이 직렬화 프록시 패턴을 사용해야 함
내부 클래스는 직렬화를 구현하지 말아야 한다.
내부 클래스에는 바깥 인스턴스의 참조와 유효 범위 안의 지역변수 값들을 저장하기 위해 컴파일ㄹ러가 생성한 필드들이 자동으로 추라된다.
익명 클래스와 지역 클래스의 이름을 짓는 규칙이 언어 명세에 나와있지 않듯, 이 플드들이 클래스 정의에 어떻게 추가되는지도 정의되지 않았다.
즉, 내부 클래스에 대한 기본 직렬화 형태는 분명하지 않다
↔ 정적 멤버 클래스는 Serializable 구현해도 됨