cleanUrl: /posts/write-readObject-methods-defensively-effective-java-item-88
share: true
하나의 클래스를 직접 직렬화 하면서 무엇을 주의해야 하는지, Serializable 이 보안적인 측면에서 무엇을 주의해야 할지 다뤄본다. Serializable 을 지양하는 이유중에 하나가 보안인데 이 item 은 특히 이 부분을 대응하기 위해 어떻게 방어적인 패턴을 적용할지 살핀다.
item 50 에서 Date
객체를 방어적으로 복사하느라 코드가 상당히 길어졌다. 이 클래스는 직렬화 해보자
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) {
if (start.compareTo(end) > 0)
throw new IllegalArgumentException(start + "가 " + end + "보다 늦다.");
this.start = start;
this.end = end;
}
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
}
논리적 구현과 물리적 구현이 일치하여 기본 implements Serializable
만 붙여도 되겠다고 생각할 수 있지만 그렇지 않다.
이유는 이 클래스의 핵심적인 불변식을 보장하지 못하기 때문이다.
readObject
는 public API 이다.private
이지만 역직렬화 과정에서 이것은 생성자와 동일하다.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);
}
// Returns the object with the specified serialized form
static Object deserialize(byte[] sf) {
try {
return new ObjectInputStream(new ByteArrayInputStream(sf)).readObject();
} catch (IOException | ClassNotFoundException e) {
throw new IllegalArgumentException(e);
}
}
}
위 역직렬화가 내 컴퓨터에선 진행이 되지 않는데 아마도 package 이름이 일치하지 않아서 그런것 같다.
하지만 결과는 end < start 로 불변식이 깨지게 된다.
즉, 클래스가 지켜야하는 규칙을 무시하고 Period
가 생성된다.
이 문제를 고치기 위해 다음과 같은 코드가 추가 되어야 한다.
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
if (start.compareTo(end) > 0) {
throw new InvalidObjectException(start + "가 " + end + "보다 늦다.");
}
}
start, end 는 final 변수이다. 하지만 이 참조에 접근하여 Date
인스턴스들을 수정할 수 있다.