티스토리 뷰

반응형

고민 후 괜찮다고 판단할 때만 기본 직렬화 형태 사용하기

  • 유연성 , 성능, 정확성 측면에서 신중히 고민 후 합당할 때만 사용
  • 이상적인 직렬화 상태 : 객체의 물리적 표현(코드로 어떻게 구현했는지)과 논리적 내용(실제로 의미하는 것)이 같다면 기본 직렬화 형태여도 무방함
//기본 직렬화 형태에 적합
public class Name implements Serializable {
    private final Stirng lastName;
    private final String firstName;
    private final String middleName;
}
  • 기본 직렬화 형태가 적합하더라도 불변식 보장과 보안을 위해 readObject 메서드를 제공해야 할 때가 많음. 위의 예시에서 lastName, firstName이 null이 아님을 readObject 메서드로 보장해야함

 

 

기본 직렬화 형태에 적합하지 않은 경우

//문자열 리스트를 표현
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가 현재의 내부 표현 방식에 영구히 묶임

위 예에서 StringList.Entry가 공개 API가 되어버린다.

나중에 내부 표현 방식을 바꾸어도 StringList 클래스는 계속해서 연결리스트로 표현된 입력도 처리할 수 있어야한다. 즉, 관련 코드를 사용하지 않더라도 절대 제거할 수 없다.

 

2) 너무 많은 공간 차지

엔트리와 연결 정보는 내부 구현에 해당하니 직렬화 형태에 포함할 가치가 없다.

이처럼 직렬화 형태가 너무 커져 많은 공간을 차지하고 전송 속도가 느려진다.

 

3) 시간이 너무 많이 걸림

직렬화 로직은 객체 그래프의 위상에 관한 정보가 없어 그래프를 직접 순회해봐야만함

 

4) 스택 오버플로우 위험

기본 직렬화 과정은 객체 그래프를 재귀 순회하므로 스택 오버플로의 위험이 있다.

 

위 예시를 합리적인 직렬화 형태로 바꾸면?

public final class StringList implements Serializable {
    // transient: 직렬화 대상에서 제외한다.
    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());
        }
    }
    // ... 생략
}
  • StringList의 물리적 상세 표현은 배제한 채 논리적인 구성만 담는다. (단순히 리스트가 포함된 문자열의 개수를 적은 다음에 그 뒤로 문자열들을 나열하는 수준만)
  • 모든 인스턴스 필드가 transient여도 defaultWriteObject()와 defaultReadObject()를 호출해야 향후 릴리스에서 transient가 아닌 인스턴스 필드가 새롭게 추가되더라도 상호호환이 가능
  • 해당 객체의 논리적 상태와 무관한 필드라고 확신할 때만 transient 한정자 생략

 

 

동기화 메커니즘을 직렬화에도 적용

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

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

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

// 무작위로 고른 long 값
private static final long serialVersionUID = 0204L;

UID는 직렬화와 역직렬화 과정에서 값이 서로 맞는지 확인한 후에 처리를 하기 때문에 이 값이 맞지 않다면 InvalidClassException 예외 발생 (구버전으로 직렬화된 인스턴스들과의 호환성을 끊으려는 것이 아니라면 직렬 버전 UID를 절대 수정하지 말자)

 

 

결론

  • 클래스를 직렬화하기로 했다면 어떤 직렬화 형태를 사용할지 신중해라.
  • 자바의 기본 직렬화 형태는 직렬화 결과가 해당 객체의 논리적 표현에 부합할때만 사용, 아니라면 커스텀 직렬화 형태 고안
  • 직렬화 형태 공개 메서드를 설계할 때에 준하는 시간을 들여 설계 (한번 공개되었으면 향후 릴리스에서 제거할 수 없어 호환성 유지를 위해 영원히 지원해야하기 때문)
반응형
댓글
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday