EffectiveJava

[EffectiveJava] 12장 직렬화

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

// 85-1 역질렬화 폭탄 - 이 스트림의 역직렬화는 영원히 계속된다.
static byte[] bomb() {
    Set<Object> root = new HashSetM<>();
    Set<Object> s1 = root;
    Set<Obejct> s2 = new HashSet<>();
    for (int i = 0; i < 100; i++) {
        Set<Object> t1 = new HashSet<>();
        Set<Object> t2 = new HashSet<>();
        t1.add("foo"); // t1을 t2와 다르게 만든다.
        s1.add(t1); s1.add(t2);
        s2.add(t1); s2.add(t2);
        s1 = t1;
        s2 = t2;
    }
    return serialize(root); // 간결하게 하기 위해 이 메서드의 코드는 생략함
}

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

// 86-1 상태가 있고, 확장 가능하고, 직렬화 가능한 클래스용 readObjectNoData 메서드
private void readObjrctNoData() throws InvalidObjectException {
    throw new InvalidObjectException("스트림 데이터가 필요합니다.");
}

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

// 87-1 기본 직렬화 형태에 적합한 후보
public class Name implements Serializable {
	
	  /** 
	  * 성. null이 아니어야 함.
	  * @serial
	  */
	  private final String lastName;
	
	  /**
	  * 이름. null이 아니어야 함.
	  * @serial
	  */
	  private final String firstName;
	
	  /**
	  * 중간 이름. 중간이름이 없다면 null.
	  * @serial
	  */
	  private final String middleName;
	
		... // 나머지 코드는 생략
}
// 87-2 기본 직렬화 형태에 적합하지 않은 클래스
public final class StringList implements Seializable {
    private int size = 0;
    private Entry head = null;

    private static class Entry implements Serializable {
        String data;
        Entry next;
        Entry previous;
    }

		... // 나머지 코드는 생략
}
// 87-3 합리적인 커스텀 직렬화 형태를 갖춘 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.delfualtWriteObject();
		    s.writeInt(size);
		
		    // 모든 원소를 올바른 순서로 기록한다.
		    for (Entry e = head; e != null; e = e.next) 
		        s.writeObject(e.data);
		}

		private void readObject(ObjectInputStream s) throws IOException, ClassNotFundException {
		    s.defaultReadObject();
		    int numElements = s.readint();
		
		    // 모든 원소를 읽어 이 리스트에 삽입한다.
		    for (int i = 0; i < numElements; i++) 
		        add((String) s.readObject());
		}
		
		... // 나머지 코드는 생략
}
// 87-4 기본 직렬화를 사용하는 동기화된 클래스를 위한 writeObject메서드
private synchronized void writeObject(ObjectOutputStream s) throws IOExecption {
  s.defaultWirteObject();
}
private static final long serialVersionUID = <무작위로 고른 long 값>;

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

// 88-1 방어적 복사를 사용하는 불변 클래스
public final class Period {
    private final Date start;
    private final Date end;
  
    /**
     * @param  start 시작 시각
     * @param  end 종료 시각. 시작 시각보다 뒤여야 한다.
     * @throws IllegalArgumentException 시작 시각이 종료 시각보다 늦을 때 발생한다.
     * @throws NullPointerException start나 end가 null이면 발생한다.
     */
    public Period(Date start, Date end) {
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());
        if (this.start.compareTo(this.end) > 0)
            throw new IllegalArgumentException(start + " after " + end);
    }
  
    public Date start() {
        return new Date(start.getTime());
    }
  
    public Date end() {
        return new Date(end.getTime());
    }
  
    public String toString() {
        return start + " - " + end;
    }

		... // 나머지 코드는 생략
}
// 88-2 허용되지 않는 Period 인스턴스를 생성할 수 있다
public class BogusPeriod {
    // 진짜 Period 인스턴스에서는 만들어질 수 없는 바이트 스트림
    private static final byte[] serializedForm = {
            (byte)0xac, (byte)0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x06,
            0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x40, 0x7e, (byte)0xf8,
            0x2b, 0x4f, 0x46, (byte)0xc0, (byte)0xf4, 0x02, 0x00, 0x02,
            0x4c, 0x00, 0x03, 0x65, 0x6e, 0x64, 0x74, 0x00, 0x10, 0x4c,
            0x6a, 0x61, 0x76, 0x61, 0x2f, 0x75, 0x74, 0x69, 0x6c, 0x2f,
            0x44, 0x61, 0x74, 0x65, 0x3b, 0x4c, 0x00, 0x05, 0x73, 0x74,
            0x61, 0x72, 0x74, 0x71, 0x00, 0x7e, 0x00, 0x01, 0x78, 0x70,
            0x73, 0x72, 0x00, 0x0e, 0x6a, 0x61, 0x76, 0x61, 0x2e, 0x75,
            0x74, 0x69, 0x6c, 0x2e, 0x44, 0x61, 0x74, 0x65, 0x68, 0x6a,
            (byte)0x81, 0x01, 0x4b, 0x59, 0x74, 0x19, 0x03, 0x00, 0x00,
            0x78, 0x70, 0x77, 0x08, 0x00, 0x00, 0x00, 0x66, (byte)0xdf,
            0x6e, 0x1e, 0x00, 0x78, 0x73, 0x71, 0x00, 0x7e, 0x00, 0x03,
            0x77, 0x08, 0x00, 0x00, 0x00, (byte)0xd5, 0x17, 0x69, 0x22,
            0x00, 0x78};
  
		
    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 IllegalArgumentException(e);
        }
    }
}
  
/*
    실행결과 
    Sat Jan 02 05:00:00 KST 1999 - Mon Jan 02 05:00:00 KST 1984
*/
// 88-3 유효성 검사를 수행하는 readObject 메서드 - 아직 부족하다!
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
    s.defaultReadObject();

    // 불변식을 만족하는지 검사
    if (start.compareTo(end) > 0) {
        throw new InvalidObjectException(start + " after " + end);
    }
}
// 88-4 가변 공격의 예
public class MutablePeriod {
    //Period 인스턴스
    private final Period period;

    // 시작 시각 필드 - 외부에서 접근할 수 없어야 한다.
    private final Date start;

    // 종료 시각 필드 - 외부에서 접근할 수 없어야 한다.
    private final Date end;
  
    public MutablePeriod() {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(bos);
  
            // 유효한 Period 인스턴스를 직렬화 한다.
            out.writeObject(new Period(new Date(), new Date()));
  
            /*
            * 악의적인 '이전 객체 참조', 즉 내부 Date 필드로의 참조를 추가한다.
            * 상세 내용은 자바 객체 직렬화 명세의 6.4절을 참고하자.
            */
            byte[] ref = {0x71, 0, 0x7e, 0, 5}; // 참조 #5
            bos.write(ref); // 시작(start) 필드
            ref[4] = 4; // 참조 # 4
            bos.write(ref); // 종료(end) 필드
  
            // Period 역직렬화 후 Date 참조를 '훔친다'.
            ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
            period = (Period) in.readObject();
            start = (Date) in.readObject();
            end = (Date) in.readObject();
        } catch (IOException | ClassNotFoundException e) {
            throw new AssertionError(e);
        }
    }
public static void main(String[] args) {
    MutablePeriod mp = new MutablePeriod();
    Period p = mp.period;
    Date pEnd = mp.end;

		// 시간을 되돌리자!
    pEnd.setYear(78);
    System.out.println(p);

		// 60년대로 회귀!
    pStart.setYear(69);
    System.out.println(p);
}
// 88-5 방어적 복사와 유효성 검사를 수행하는 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(this.start + "가 " + this.end + "보다 늦다.");
    }
}

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

public class Elvis{
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() { ... }
    private Object leaveTheBuilding() {...}
}
// 인스턴스 통제를 위한 readResolve - 개선의 여지가 있다!
pivate Object readResolve(){
    // 진짜 Elvis를 반환하고, 가짜 Elvis는 가비지 컬렉터에 맡긴다.
    return INSTANCE;
}
// 89-1 잘못된 싱글턴 - 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;
    }
}
// 89-2 도둑 클래스
public class ElvisStealer implements Serializable {
    static Elvis impersonator;
    private Elvis payload;
  
    private Object readResolve() {
        // resolve되기 전의 Elvis 인스턴스의 참조를 저장한다.
        impersonator = payload;

        // favoriteSongs 필드에 맞는 타입의 객체를 반환한다.
        return new String[] {"A Fool Such as I"};
    }
		private static final long serialVersionUID = 0;
}
// 89-3 직렬화의 허점을 이용해 싱글턴 객체를 2개 생성한다.

public class ElvisImpersonator {
        // 진짜 Elvis 인스턴스로는 만들어질 수 없는 바이트 스트림!
        private static final byte[] serializedForm ={
                (byte) 0xac,(byte) 0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x05, 
                0x45, 0x6c, 0x76, 0x69, 0x73, (byte) 0x84, (byte) 0xe6, 
                (byte) 0x93, 0x33, (byte) 0xc3, (byte) 0xf4, (byte) 0x8b, 
                0x32, 0x02, 0x00, 0x01,0x4c, 0x00, 0x0d, 0x66, 0x61, 0x76, 
                0x6f, 0x72, 0x69, 0x74, 0x65,0x53, 0x6f, 0x6e, 0x67, 0x73, 
                0x74, 0x00, 0x12, 0x4c, 0x6a, 0x61,0x76, 0x61, 0x2f, 0x6c, 
                0x61, 0x6e, 0x67, 0x2f, 0x4f, 0x62, 0x6a,0x65, 0x63, 0x74, 
                0x3b, 0x78, 0x70, 0x73, 0x72, 0x00, 0x0c, 0x45,0x6c, 0x76, 
                0x69, 0x73, 0x53, 0x74, 0x65, 0x61, 0x6c, 0x65, 0x72,0x00, 
                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01,
                0x4c, 0x00, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 
                0x74,0x00, 0x07, 0x4c, 0x45, 0x6c, 0x76, 0x69, 0x73, 0x3b, 
                0x78, 0x70,0x71, 0x00, 0x7e, 0x00, 0x02 };
        
        public static void main(String[] args) {
	        // ElvisStealer.impersonator 를 초기화한 다음,
	        // 진짜 Elvis(즉, Elvis.INSTANCE)를 반환
          **Elvis elvis = (Elvis) deserialize(serializedForm);**
          Elvis impersonator = ElvisStealer.impersonator;
        
          elvis.printFavorites();
          impersonator.printFavorites();
        }
}
// 89-4 열거 타입 싱글턴 - 전통적인 싱글턴보다 우수하다
public enum Elvis {
    INSTANCE;
    private String[] favoriteSongs = {"Hound Dog", "Heartbreak Hotel"};
    public void printFavorites() {
        System.out.println(Arrays.toString(favoriteSongs));
    }
}

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

// 90-1 Priod 클래스용 직렬화 프록시
 private static class SerializationProxy implements Serializable {
     private final Date start;
     private final Date end;

     SerializtionProxy(Period p) {
         this.start = p.start;
         this.end = p.end;
     }

     private static final long serialVersionUID = 2342352345098L; // 아무 값이나 상관없다.
 }
// 직렬화 프록시 패턴용 writeReplace 메서드 (480쪽)
  private Object writeReplace() {
    return new SerializationProxy(this);
  }
// 직렬화 프록시 패턴용 readObject 메서드 (480쪽)
  private void readObject(ObjectInputStream stream)
      throws InvalidObjectException {
    throw new InvalidObjectException("프록시가 필요합니다.");
  }
// Period.SerializationProxy 용 readResolve 메서드
private Object readResolve() {
    return new Period(start, end);  // public 사용자를 사용
}
// 90-2 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;
}

Leave a Reply