본문 바로가기
Language/Effective java

직렬화

by y.j 2022. 9. 3.
728x90

커스텀 직렬화 형태를 고려해보라.

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

  • 기본 직렬화 형태는 유연성, 성능, 정확성 측면에서 신중히 고민한 후 합당할 때만 사용해야한다.
  • 기본 직렬화 형태와 거의 같은 결과가 나올 경우에만 기본 형태를 사용해야 한다.

 

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

  • 성명은 논리적으로 이름, 성, 중간이름이라는 3개의 문자열로 구성되며, 앞 코드의 인스턴스 필드들은 이 논리적 구성요소를 정확히 반영했다.
  • 기본 직렬화 형태가 적합하다고 결정했더라도 불변식 보장과 보안을 위해 readObject메서드를 제공해야 할 때가 많다.
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;
    }
    
    ... // 나머지 코드 생략
    
}

 

객체의 물리적 표현과 논리적 표현의 차이가 클 때 기본 직렬화를 사용하면 안되는 이유

  • 공개 API가 현재의 내부 표현 방식에 영구히 묶인다. 앞의 예에서 private 내부 클래스인 StringList.Entry가 공개 API가 되어 버린다. 
  • 너무 많은 공간을 차지할 수 있다. 내부 구현은 직렬화 형태에 포함할 가치가 없다.
  • 시간이 너무 많이 걸릴 수 있다. 직렬화 로직은 객체 그래프의 위상에 관한 정보가 없어 그래프를 직접 순회해볼 수 밖에 없다.
  • 스택 오버플로를 일으킬 수 있다. 그래프를 순회하는 과정에서 스택오버플로우를 일으킬 수 있다.

 

코드 수정

  • transient사용하기 : 해시값처럼 JVM마다 달라지는 필드들은 transient를 붙여야 한다. 향후 릴리스에서 transient가 아닌 인스턴스 필드가 추가되더라도 상호 호환된다. 해당 객체의 논리적 상태와 무관한 필드라고 확실할 때만 transient 한정사를 생략해야 한다. ( 해시테이블의 경우 value에 대한 해시코드가 달라질 수 있다. )
  • defaultReadObject, defaultWriteObject : 필드가 모두 transient여도 사용하도록 명세에 나와있다. 먼저 필드들을 초기값으로 만든다음 해당필드를 원하는 값으로 복원해야 한다.
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());
    }
    
    ... // 나머지 코드는 생략
}

위의 코드는 속도도 빨라지고 스택오버플로우가 나타나지는 않는다. 유연성을 떨어지더라도 원래 객체를 불변식까지 포함해 제대로 복원해낸다는 점에서 정확하다고 할 수 있다.

 

동기화 메커니즘은 직렬화에도 적용해야 한다.

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

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

 

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

UID를 명시하면 속도가 빨라지고 없으면 이 값을 생성하느라 복잡한 연산을 수행한다. 구버전으로 직렬화된 인스턴스들과의 호환성을 끊으려는 경우를 제외하고는 직렬 버전 UID를 절대 수정하지 말자.

private static final long serialVersionUID = <무작위로 고른 long 값>;

 

readObject 메서드는 방어적으로 작성하라.

물리적 표현과 논리적 표현이 부합하더라도 불변식을 보장하지 못할 수 있다. readObject메서드가 실질적으로 또 다른 public생성자이기 때문이다. 

 

아래코드는 Period클래스의 불변식을 깨뜨리기 때문에 잘못된 결과를 내뱉는다. 

public class BogusPeriod {

    public static final byte[] serializedForm = {
            (byte)0xac, (byte)0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x06,
            0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x40, 0x7e, (byte)0xf8
    };

    public static void main(String[] args) {
        Period p = (Period) deserialize(serializedForm);
        System.out.println(p);
    }

    static Object deserialize(byte[] sf) {
        try {
            return new ObjectInputStream(
                    new ByteArrayInputStream(sf)).readObject();
        } catch (IOException | ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
}

 

readObject를 통해 해결하는 방법

  • 유효성 검사를 통해 잘못된 역직렬화가 일어나는 것을 막을 수 있다.
  • Override을 하면 안된다. 역직렬화 되기 전에 하위 readObject가 실행되어서 오작동으로 이어진다.
  • 객체를 역직렬화할 때는 클라이언트가 소유해서는 안되는 객체 참조를 갖는 필드를 모두 반드시 방어적으로 복사해야 한다.
private void readObject(ObjectInputStream s)
    throws IOException, ClassNotFoundException {
    s.defaultReadObject();
    
    
    // 방어적 복사
    start = new Date(start.getTIme());
    end   = new Date(end.getTime());

    // 유효성 검증
    if(start.compareTo(end) > 0)
        throw new InvalidObjectException(start + " after " + end);
}
728x90

'Language > Effective java' 카테고리의 다른 글

직렬화  (0) 2022.09.04
직렬화  (0) 2022.09.03
동시성  (0) 2022.09.02
동시성  (0) 2022.08.27
동시성  (0) 2022.08.24

댓글