본문 바로가기

STUDY REVIEW/이펙티브 자바 독서스터디

[EFFECTIVE JAVA] 이펙티브 자바 독서스터디 - 12장 직렬화

12장 직렬화
아이템 85. 자바 직렬화의 대안을 찾으라
아이템 86. Serializable을 구현할지는 신중히 결정하라
아이템 87. 커스텀 직렬화 형태를 고려해보라
아이템 88. readObject 메서드는 방어적으로 작성하라
아이템 89. 인스턴스 수를 통제해야 한다면 readResolve보다는 열거 타입을 사용하라
아이템 90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라

 

 

읽고 느낀 점

그 동안 Serializable 에 대해 많이 말했는데 본론부터 알려주지 마지막에 어려운걸로 마무리 하다니

아직 공부해야할 산이 높고도 높다는 걸 깨달았다.

다들 정말 이렇게 코드를 짜고 있는건가?

내가 온전하게 자바에 대해 논의할 수 있을때 다시 한번 읽어보면 좋은 책이다.


아이템 85. 자바 직렬화의 대안을 찾으라

 

글을 읽기 전 직렬화가 무엇인지부터 알아보자(스스로)

직렬화를 회피하는 가장 좋은 방법은 아무것도 역직렬화하지 않는 것이다.

대안으로는 JSON, 프로토콜이 있다.


아이템 86. Serializable을 구현할지는 신중히 결정하라

 

Serializable을 구현하면 릴리스한 뒤에는 수정하기 어렵다.

버그와 보안 구멍이 생길 위험이 높아진다.

해당 클래스의 신버전을 릴리스할 때 테스트 할 것이 늘어난다.

Serializable 구현 여부는 가볍게 결정할 사안이 아니다.

 

상속용으로 설계된 클래스는 대부분 Serializable을 구현하면 안되며, 인터페이스도 대부분 Serializable을 확장해서는 안된다. 또한 내부클래스는 직렬화를 구현하지 말아야 한다.


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

 

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

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

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

 

  • 기본 직렬화 형태에 적합하지 않은 클래스
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가 현재의 내부 표현 방식에 영구히 묶인다.
  2. 너무 많은 공간을 차지할 수 있다.
  3. 시간이 너무 많이 걸릴 수 있다.
  4. 스택 오버플로를 일으킬 수 있다.

 

  • 합리적인 커스텀 직렬화 형태를 갖춘 StringList
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) {...}
    
    /**
     * 이 {@code StringList} 인스턴스를 직렬화한다.
     * 
     * @serialData 이 리스트의 크기(포함된 문자열의 개수)를 기록한 후
     * ({@code int}), 이어서 모든 원소를(각각은 {@code String})
     * 순서대로 기록한다.
     */
    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());
        }
    }
}

아이템 88. readObject 메서드는 방어적으로 작성하라

 

  • private 이어야 하는 객체 참조 필드는 각 필드가 가리키는 객체를 방어적으로 복사하라 
  • 모든 불변식을 검사하여 어긋나는 게 발견되면 InvalidObjectException을 던진다. 방어적 복사 다음에는 반드시 불변식 검사가 뒤따라야 한다.
  • 역직렬화 후 객체 그래프 전체의 유효성을 검사해야 한다면 ObjectInputValidation 인터페이스를 사용하라
  • 직접적이든 간접적이든 readObject메서드에서 재정의 가능한 메서드를 호출해서는 안된다.

아이템 89. 인스턴스 수를 통제해야 한다면 readResolve보다는 열거 타입을 사용하라

 

해결책은 enum을 사용하자.

public enum Elvis {
    INSTANCE;
    
    ...필요한 데이터...
}

아이템 90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라

 

  • 직렬화 프록시
public class Period implements Serializable{
    private static class SerializationProxy implements Serializable {
        private final Date start ;
        private final Date end ;
      
        SerializationProxy(Period p) {
            this.end = p.end ; 
            this.start = p.start ;
        }
        private static final long serialVersionUID = 123123L ;

        private Object readResolve() {
            return new Period(start,end) ;
        }

    }
    private final Date start ;
    private final Date end ;

    public Period(Date start, Date end) {
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());
    }

    // 직렬화 프록시 패턴용 wrtiePlace 메서드
    private Object writeReplace() {
        return new SerializationProxy(this) ;
    }

    // 직렬화 프록시 패턴용 readObject 메서드
    private void readObject(ObjectInputStream s) throws IOException {
        throw new InvalidObjectException("Proxy required !") ;
    }
}
  • EnumSet의 직렬화 프록시
private static class SerializationProxy <E extends Enum<E>>
        implements Serializable {
   //이 EnumSet의 원소 타입
    private final Class<E> elementType;

   //이 EnumSet 안의 원소들
    private final Enum<?>[] elements;

    SerializationProxy(EnumSet<E> set) {
        elementType = set.elementType;
        elements = set.toArray(new Enum<?>[0]);
    }

    private Object readResolve() {
        EnumSet<E> result = EnumSet.noneOf(elementType);
        for (Enum<?> e : elements)
            result.add((E)e);
        return result;
    }

    private static final long serialVersionUID =
        362491234563181265L;
}