본문 바로가기
Language/Effective java

직렬화

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

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

싱글턴 클래스를 implements Serializable을 추가하는 순간 싱글턴이 아니게 된다. readObject를 통해 클래스가 초기화될 때 별개의 인스턴스를 다시 만들기 때문이다. 

 

readResolve를 사용하게 되면 싱글톤을 유지할 수 있고 새로 생긴 인스턴스는 가비지 컬렉터의 대상이 된다.

  • 싱글턴 필드 외에 다른 필드들은 transient로 선언해야한다.
public class Elvis implements Serializable {
    public static final Elvis INSTANCE = new Elvis();

    private Elvis() { }

    private String[] favoriteSongs =
            {"Hound Dog", "Heartbreak Hotel"};

    public void printFavorites() {
        System.out.println(Arrays.toString(favoriteSongs));
    }

    private Object readResolve() {
        return INSTANCE;
    }
}
  • transient가 없는 필드가 있다면 도둑클래스가 탈취해서 바꿀 수 있다.
public class ElvisStealer implements Serializable {
    static Elvis impersonator;
    private Elvis payload;
    
    private Object readResolve() {
        impersonator = payload;
        
        return new String[] {"A Fool Such as I"};
    }
    
    private static final long serialVersionUID = 0;
    
}
  • 열거타입으로 작성한다면 선언한 상수 외에 다른 객체는 존재 하지 않음을 자바가 보증해준다. AccessibleObject.setAccessible같은 특권 메서드를 악용한다면 방어가 무력해진다.
public enum Elvis {
    INSTANCE;
    private String[] favoriteSongs =
            {"Hound Dog", "Heartbreak Hotel"};
    
    public void printFavorites() {
        System.out.println(Arrays.toString(favoriteSongs));
    }
}

 

readResolve 메서드의 접근성은 매우 중요하다.

  • final클래스에서는 private으로 작성해야 한다.
  • private : 하위 클래스에서 사용할 수 없다.
  • package-private : 같은 패키지에 속한 하위 클래스에서만 사용 할 수 있다.
  • protected나 public : 재정의하지 않은 모든 하위 클래스에서 사용 할 수 있다. 재정의 하지 않았다면, 하위 클래스의 인스턴스를 역직렬화하면 상위 클래스의 인스턴스를 생성하여 ClassCastException을 일으킬 수 있다.

 

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

직렬화 프록시 패턴을 사용하면 버그와 보안 문제의 가능성을 크게 줄여줄 수 있다.

 

직렬화 프록시 패턴 정의하는 방법

  • 바깥 클래스의 논리적 상태를 정밀하게 표현하는 충첩클래스를 private static으로 선언한다.
  • 중첩클래스의 생성자는 단 하나여야 한다.
  • 바깥 클래스의 매개변수로 받아야 한다.
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 = 123456789L; // 아무 값이나 상관없다.
}

 

writeReplace메서드를 추가한다. 범용적이여서 직렬화 프록시를 사용하는 모든 클래스에 그대로 복사해 쓰면 된다.

private Object writeReplace() {
    return new SerializationProxy(this);
}

 

readObject를 통해 writeReplace통하지 않고 직렬화하려고 하면 아래와 같이 막을 수 있다.

private void readObject(ObjectInputStream stream)
    throws InvalidObjectException {
    throw new InvalidObjectException("프록시가 필요합니다.");
}

 

readResolve()는 public 생성자를 사용한다.

  • 일반 인스턴스 만들때와 같은 방식을 따른다.
  • 역직렬화된 인스턴스가 해당 클래스의 불변식을 만족하는지 검사할 수단을 강구하지 않아도 된다.
  • 방어적 복사나 가짜 바이트 스트림공격과 내부 필트 탈취 공격을 차단해준다.
  • final 불변 클래스를 만들 수 있다.
private Object readResolve() {
    return new Period(start, end);
}

 

EnumSet직렬화에서도 적용이 가능하다.

EnumSet은 2가지로 나누어진다. RegularEnumSet과 JumboEnumSet으로 나누어져 있다. 만약 64개짜리 열거 타입을 가진 EnumSet을 직렬화한다음에 5개를 추가하면 문제가 발생할 것이다. 

 

EnumSet을 적용한 직렬화 프록시 코드

private static class SerializationProxy<E extends Enum<E>> 
        implements Serializable {
    private final Class<E> elementType;
    
    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 =
            123456789L;
}

 

직렬화 프록시 패턴에는 한계

  • 멋대로 확장할 수 있는 클래스에는 적용 할 수 없다.
  • 객체 그래프의 순환이 있는 클래스에도 적용 할 수 없다. 직렬화 프록시만 가졌을 뿐 실제 객체는 아직 만들어진게 아니므로 ClassCastException이 발생한다.
  • 안정성의 대가로 성능이 느리다.
728x90

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

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

댓글