본문 바로가기

JAVA/Effective Java

item 87) 커스텀 직렬화 형태를 고려해보라

728x90

기본 직렬화 : Serializable 인터페이스 구현

커스럼 직렬화 : 커스텀 writeObject, readObject 작성

 

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

- 객체의 물리적 표현과 논리적 내용이 같다면 사용해도 무방하다.

public class Name implements Serializable {
    
    /**
     * 성. null이 아니어야함
     * @serial
     */
    private final String lastName;
    
    /**
     * 이름. null이 아니어야 함.
     * @serial
     */
    private final String firstName;
    
    /**
     * 중간이름. 중간이름이 없다면 null.
     * @serial
     */
    private final String middleName;
}

사람의 이름은 논리적으로 성, 중간이름, 이름으로 구성되므로 적합하다.

 

기본직렬화가 적합하다고 결정했더라도 불변식 보장과 보안을 위해 readObject 메서드가 필요할때가 있다.

- 위의 예제에서 lastName , middleName이 NotNull임이 보장해야한다.

 

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;
    }
}

객체의 물리적 표현과 논리적 표현의 차이가 클 때 기본 직렬화를 썼을떄의 문제점

 

- 공개 API가 내부 표현 방식에 종속된다.

예를 들어, 향후 버전에서는 연결 리스트를 사용하지 않게 바꾸더라도 관련 처리는 필요해진다

 

- 공간이 커진다.

엔트리와 연결 정보는 내부 구현이므로 직렬화에 포함 시킬 필요가 없다. 이로 인해 직렬화 형태가 커져 속도가 느려진다.

 

- 시간이 오래 걸린다.

기본 직렬화 로직은 객체 그래프를 직접 재귀 순회하므로 느려진다.

 

- 스택 오버플로우를 일으킨다.

책에는 StringList 1000~1800의 원소를 직렬화하는데 StackOverFlowError를 일으킨다고 한다.

합리적인 직렬화 형태

물리적인 상세 표현은 배제하고 논리적인 구성만을 담으면 된다

{문자열의 개수, 문자열 원소}

public 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;
    }

    // 문자열을 리스트에 추가한다.
    public final void add(String s) { ... }

    /**
     * StringList 인스턴스를 직렬화한다.
     */
    private void writeObject(ObjectOutputStream stream)
            throws IOException {
        stream.defaultWriteObject();
        stream.writeInt(size);

        // 모든 원소를 순서대로 기록한다.
        for (Entry e = head; e != null; e = e.next) {
            s.writeObject(e.data);
        }
    }

    private void readObject(ObjectInputStream stream)
            throws IOException, ClassNotFoundException {
        stream.defaultReadObject();
        int numElements = stream.readInt();

        for (int i = 0; i < numElements; i++) {
            add((String) stream.readObject());
        }
    }
    // ... 생략
}

 

transient 키워드가 붙은 필드는 기본 직렬화 형태에 포함되지 않는다(그리고 기본값으로 초기화된다. null, 0 , flse)

 

직렬화 명세 : writeObject readObject 메서드는 각각 defaultWriteObject defaultReadObject 메서드를 호출한다.

 이렇게 해야 향후 릴리즈에서 transient가 아닌 필드가 추가되더라도 상위와 하위 모두 호환이 가능하기 때문이다.

신버전의 인스턴스를 직렬화한 후에 구버전으로 역직렬화하면 새로 추가된 필드는 무시될 것이다. 또 구버전 readObject 메서드에서 defaultReadObject를 호출하지 않는다면 역직렬화 과정에서 StreamCorruptedException이 발생한다.

 

개선된 버전

- 직렬화 형태가 절반정도의 메모리와 수행속도로 개선된다.

- 스택 오버플로가 전혀 발생하지 않는다.

 

직렬화는 객체의 전체 상태를 읽은 메서드이므로 다른 객체의 전체 상태를 읽는 상태와 동일한 직렬화를 적용해야한다.

(synchronized 면 똑같이 synchronized 적용)

 

SVUID(serivalVersionUID)를 명시적으로 부여하라

 

명시적으로 부여하지 않으면 내부적으로 클래스의 기본 해쉬값을 사용한다.

 public class Member implements Serializable {
        private String name;
        private String email;
        private int age;
      // 생략
    }
    
    public class Member implements Serializable { 
        private String name;
        private String email;
        private int age;
        // phone 속성을 추가
        private String phone;
      // 생략
    }
    
    //멤버 변수추가 후 구버전을 다시 역직렬화 하면 java.io.InvalidClassException 예외 발생
    
    

 

“조금이라도 역직렬화 대상 클래스 구조가 바뀌면 에러 발생해야 된다.” 정도의 민감한 시스템이 아닌 이상은 클래스를 변경할 때에
직접 serialVersionUID 값을 관리해주어야 클래스 변경 시 혼란을 줄일 수 있다.