📌 12장 : 직렬화

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

  • 1997년, 자바에 직렬화 도입
  • 직렬화의 근본적인 문제
    • 공격 범위가 넓고, 지속적으로 넓어져 방어가 어렵다.
    • ObjectInputStream의 readObject 메서드를 호출하면 객체 그래프가 역직렬화 되기 떄문
  • 가젯(gadget)
    • 역직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 수행하는 메서드
  • 역직렬화 폭탄(deserialization bomb)
    • 역직렬화에 시간이 오래 걸리는 짧은 스트림을 역직렬화하다 서비스 거부 공격에 쉽게 노출될 수 있다.
  • 직렬화 문제 회피 방법
    • 아무것도 역직렬화하지 않는다.
    • 객체와 바이트 시퀀스를 변환해주는 다른 메커니즘을 사용한다.
    • 크로스-플랫폼 구조화된 데이터 표현(cross-platform structured-data representation)
  • 크로스-플랫폼 구조화된 데이터 표현
    • 자바 직렬화보다 간단하다.
    • 임의 객체 그래프를 자동으로 직렬화/역직렬화 하지 않는다.
    • 속성-값 쌍의 집합으로 구성된 간단하고 구조화된 데이터 객체를 사용한다.
    • ex. JSON, 프로토콜 버퍼(protocol buffers)
      • JSON : 텍스트 기반이라 사람이 읽을 수 있다.
      • 프로토콜 버퍼 : 이진 표현이라 효율이 좋다.
      • JSON은 데이터를 표현하는 데 사용, 프로토콜 버퍼는 문서를 위한 스키마(타입)을 제공한다.
      • 프로토콜 버퍼는 사람이 읽을 수 있는 텍스트 표현(pbtxt)도 지원한다.

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

  • 클래스의 인스턴스를 직렬화 하기 위해서는 클래스 선언에 implements Serializable 붙이기
  • 선언은 쉬워보이지만 Serializable 을 구현하면 릴리즈한 뒤에 수정이 어렵다.
    • Serializable 구현 후, 직렬화된 바이트 스트림 인코딩은 공개 API가 된다.
  • Serializable 구현은 버그와 보안 구멍이 생길 위험이 높아진다.
    • 객체는 생성자를 사용해 만드는데, 직렬화는 기본 메커니즘을 우회하는 객체 생성 기법
    • 역직렬화를 사용하면 불변식 깨짐과 허가되지 않은 접근에 노출된다.
  • Serializable 구현은 신버전 릴리즈할 때 테스트할 것이 늘어난다.
    • 직렬화 수정 후, 구버전으로 역직렬화 할 수 있는지 반대도 가능한지 확인해야 한다.
  • 상속용으로 설계된 클래스는 대부분 Serializable을 구현하면 안된다. 인스턴스도 대부분 안된다.
  • 내부 클래스는 직렬화를 구현하면 안된다.

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

  • 수정이 어렵기 떄문에 고민한 후에 괜찮다고 판단될 때만 기본 직렬화 형태 사용하기
  • 객체의 물리적 표현과 논리적 내용이 같으면 기본 직렬화 형태도 무방하다.
  • 차이가 클 때 사용시 문제점
    • 공개 API가 현재의 내부 표현 방식에 영구히 묶인다.
    • 너무 많은 공간을 차지할 수 있다.
    • 시간이 너무 많이 걸릴 수 있다.
    • 스택 오버플로를 일으킬 수 있다.

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

  • readObject는 어떤 바이트 스트림이 넘어와도 유효한 인스턴스를 만들어야 한다.
  • 안전한 readObject 메서드를 작성하는 방법
    • private 이어야 하는 객체 참조 필드는 각 필드가 가리키는 객체를 방어적으로 복사해야 한다.
    • 모든 불변식을 검사해서 어긋나는 게 발견되면, InvalidObjectException 을 던진다.
    • 역직렬화 후 객체 그래프 전체의 유효성을 검사해아 하면, ObjectInputValidation 인터페이스를 사용해라
    • 재정의할 수 있는 메서드를 호출하지 않는다.

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

  • readResolve 를 인스턴스 통제 목적으로 사용하려면 객체 참조 타입 인스턴스 필드는 모두 transient 으로 선언해야 한다.
    • 그렇게 하지 않으면, readResolve 메서드가 수행되기 전에 역직렬화된 객체의 참조를 공격할 수도 있다.
  • 직렬화 가능한 인스턴스 통제 클래스를 열거 타입을 이용해 구현하면 선언한 상수 외의 다른 객체는 존재하지 않는 것을 보장해준다.
  • 열거 타입 싱글턴 예시
public enum Elvis { 
    INSTANCE;
    private String[] favoriteSongs =
        { "Hound Dog", "Heartbreak Hotel" };
    public void printFavorites() {
        System. out.println(Arrays.toString (favoriteSongs));
    } 
}

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

  • Serializable 으로 구현하면, 생성자 이외의 방법으로 인스턴스를 생성
    • 버그와 보안 문제가 일어날 수 있다.
    • 직렬화 프록시 패턴(serialization proxy pattern) 으로 해결
  • 직렬화 프록시 패턴
    • 바깥 클래스의 논리적 상태를 정밀하게 표현하는 중첩 클래스를 설계해서 private static 으로 선언한다.
    • 중첩 클래스의 생성자는 1개
    • 바깥 클래스를 매개변수로 받아야 한다.
    • 생성자는 단순히 인수로 넘어온 인스턴스 데이터를 복사
    • 일관성 검사, 방어적 복사도 필요 없다.
    • 바깥 클래스, 직렬화 프록시 모두 Serializable 를 구현한다고 선언해야 한다.
  • 직렬화한 Period 클래스 예시
private static class SerializationProxy implements Serializable { 
    private final Date start;
    private final Date end;
    
    Serializationproxy(Period p) { 
        this.start = p.start; 
        this.end = p.end;
    }
    private static final long serialVersionUID = 
            234098243823485285L; // 아무 값이나 상관없다. (아이템 87)
}

// 바깥 클래스에 추가 : 직렬화 프록시 패턴용 writeReplace 메서드
// 직렬화가 이뤄지기 전에 바깥 클래스 인스턴스를 직렬화 프록시로 변환
private Object writeReplace() {
    return new SerializationProxy(this); 
}

// 직렬화 프록시 패턴용 readObject 메서드
private void readObject(ObjectlnputStream stream) 
        throws InvalidObjectException {
    throw new InvalidObjectException("프록시가 필요합니다.");
}

// Period.SerializationProxy용 readResolve 메서드
// 역직렬화 시에 직렬화 시스템이 직렬화 프록시를 다시 바깥 클래스의 인스턴스로 변환
private Object readResolve() {
  return new Period(start, end); // public 생성자를 사용한다.
}

+ Recent posts