Serializable 인터페이스를 구현하면 버그나 보안 결함이 생길 가능성이 높아진다.
· 일반 생성자 대신 언어 외적인 메커니즘을 통해 객체를 생성할 수 있게 되기 때문이다.
ㄴ 직렬화 프록시 패턴을 통해 개선!
직렬화 프록시 : 바깥 클래스 객체의 논리적 상태를 간결하게 표현하는 직렬화 가능 클래스를 private static 중첩클래스
· 바깥 클래스를 인자 자료형으로 사용하는 생성자를 하나만 가진다.
· 이 생성자는 인자에서 데이터를 복사하기만 한다.
· 일관성 검사를 할 필요도 없고 방어적 복사를 할 필요도 없다.
· 설계산 직렬화 프록시의 기본 직렬화 형식은 바깥 클래스의 완벽한 직렬화 형태다.
· 바깥 클래스 뿐만 아니라 직렬화 프록시도 반드시 Serializable인터페이스를 구현한다고 선언되어야 한다.
// Priod 클래스의 직렬화 프록시
private static class SerializationProxy implements Serializable{
private final Date start;
private final Date end;
SerializationProxy(Proiod p){
this.start = p.start;
this.end = p.end;
}
private static fianl long serialVersionUID =
213214124214L;
}
이 프락시를 추가한다음, 바깥 클래스에 아래 writeReplace메서드를 구현한다.
이 메서드는 직렬화 프록시가 있는 클래스라면 아무 수정없이 그대로 사용할 수 있다.
// 직렬화 프록시 패턴을 구현하기 위한 writeReplace메서드
private Object writeReplace(){
return new SerializationProxy(this);
}
위의 메서드가 있으면 직렬화 시스템은 바깥 클래스 객체 대신 Serialization Proxy객체를 직렬화 한다.
· writeReplace메서드는 직렬화가 이루어지기 전에 바깥 클래스 객체를 직렬화 프록시 객체로 변환한다.
writeReplace메서드를 갖추게 되면 직렬화 시스템은 바깥 클래스로 직렬화된 객체는 절대로 만들지 않는다. 하지만 공격자는 클래스의 불변식을 훼손하고자 그런 객체를 만들려 할 수도 있다. 그런 공격을 막으려면 아래의 readObject메서드를 바깥 클래스에 추가하면 된다.
//직렬화 프록시 패턴 구현을 위한 readObject메서드
private void readObject(ObjectInpuStream stream)
throws InvalidObjectException{
throw new InvalidObjectException("Proxy required");
}
SerializationProxy클래스에 자기와 논리적으로 동일한 바깥 클래스 객체를 반환하는 readResolve메서드를 추가해야 한다.
직렬화시스템은 역직렬화를 끝내자 마자 직렬화 프락시 객체를 다시 바깥클래스 객체로 변환하게 된다.
이 readResolve메서드는 그 public API만 사용해서 바깥 클래스 객체를 만드는데, 이 패턴이 아름다운것은 그래서이다.
역직렬화된 객체도 일반 객체도 마찬가지로 생성자, 정적 팰터리, 일반 메서드를 통해 만드므로, 직렬화의 언어 외적인 특성이 대부분제거되는 것이다. 그 덕에 역직렬화된 객체가 클래스 불변식을 준수하도록 하기 위해 별도의 수단을 동원할 필요가 없다.
// Period.SerializationProxy의 readResolve메서드
private Object readResolve(){
return new Period(start, end); // public생성자 이용
}
방어적 복사 접근법과 마찬가지로 직렬화 프락시 접근법도 가짜 바이트 스트림을 통한 공격을 방지하며 내부 필드 탈취 공격도 저절로 중단되도록 만든다.
앞선 두가지 접근법과는 달리 직렬화 프록시 접근법을 이용하면 Period필드를 final로 선언할 수 있어서 Period를 진정한 변경불가능 클래스로 만들 수 있다.
방어적 복사보다 직렬화 프록시 패턴이 더 위력적일 때 가 있다.
역직렬화된 객체가 애초에 직렬화된 객체와 다른 클래스가 되도록 만들 수 있다.
// EnumSet의 직렬화 프록시
private static class SerializationProxy <E extends Enum<E>> implements Serialiizable{
// 이 enum 집합의 원소 자료형
private final Class<E> elementType;
// 이 enum 집합에 담긴 원소들
private final Enum[] elements;
SerializationProxy(EnumSet<E> set){
elementType = set.elementType;
elements = set.toArray(EMPTY_ENUM_ARRAY);//규칙 43
}
private Object readResolve(){
EnumSet<E> result = EnumSet.noneOf(elementType);
for(Enum e : elements){
result.add((E)e);
return result;
}
private static final long serialVersionUID =
123122142141L;
}
직렬화 프록시 패턴에는 두가지 제약이 있다.
1. 확장할 수 있는 클래스에는 적용할 수 없는 기법 객체 그래프에 순환되는 부분이 있는 클래스에는 적용할 수 없다
· 어떤 객체의 메서드를 그 객체의 직렬화 프록시의 readResolve안에서 호출하려 하면 ClassCastException이 발생할 것인데, 직렬화 프락시는 가졌으나 아직 실제 객체를 가진것은 아니기 때문이다.
2. 직렬화 프록시 패턴이 주는 힘과 안전성을 거저 얻는것이 아니다.
· 직렬화 프록시 패턴을 써서 Period객체를 직렬화 하고 역직렬화하는데 방어적 복사 기법에 비해 14%가량 비용이 더 들었다.
결론
· 클라이언트가 확장할 수 없는 클래스에 readObject나 writeObject를 구현해야 할 때는 직렬화 프록시 패턴 도입을 고려해 보라.
· 단순하지 않은 불변식을 만족해야 하는 객체를 안정적으로 직렬화 하는 가장 쉬운 방법이다.
'IT > Programming' 카테고리의 다른 글
<Effective Java> RULE 76 readObject메서드는 방어적으로 구현하라 (0) | 2023.04.25 |
---|---|
<Effective Java> RULE 77 개체 통제가 필요하다면 readResolve 대신 enum자료형을 이용하라. (0) | 2023.04.25 |
[JAVA] helloworld print 예제 (0) | 2023.04.25 |
[JAVA] println, print 예제 (0) | 2023.04.25 |
[JAVA] 원의 넓이 출력 예제 (0) | 2023.04.25 |