티스토리 뷰
- 아이템 85. 자바 직렬화의 대안을 찾으라
- 아이템 86. Serializable을 구현할지는 신중히 결정하라
- 아이템 87. 커스텀 직렬화 형태를 고려해보라
- 아이템 88. readObject 메서드는 방어적으로 작성하라
- 아이템 89. 인스턴스 수를 통제해야 한다면 readResolve보다는 열거 타입을 사용하라
- 아이템 90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라
아이템 85. 자바 직렬화의 대안을 찾으라
- 직렬화의 근본적인 문제는 공격 범위가 너무 넓고 지속적으로 더 넓어져 방어하기 어렵다는 점이다. readObject 메서드는 바이트 스트림을 역직렬화하는 과정을 통해 클래스패스 안의 거의 모든 타입의 객체를 만들어낼 수 있다. 이 말은즉슨, 그 타입들의 코드 전체가 공격 범위에 들어간다는 뜻이다.
- 가젯(gadget)은 자바 라이브러리와 널리 쓰이는 서드파티 라이브러리에서 직렬화 가능 타입들을 연구하여 역직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 수행하는 메서드들을 의미한다.
- 역직렬화 폭탄(deserialization bomb)은 서비스 거부 공격에 노출되기 쉬운, 역직렬화에 시간이 오래 걸리는 짧은 스트림을 의미한다.
- 직렬화 위험을 회피하는 가장 좋은 방법은 아무것도 역직렬화하지 않는 것이다. 여러분이 작성하는 새로운 시스템에서 자바 직렬화를 써야 할 이유는 전혀 없다. 객체와 바이트 시퀀스를 변환해주는 다른 메커니즘이 많이 있다.
- 크로스-플랫폼 구조화된 데이터 표현(cross-platform structured-data representation)은 자바 직렬화의 여러 위험을 회피하면서 다양한 플랫폼 지원, 우수한 성능, 풍부한 지원 도구, 활발한 커뮤니티와 전문가 집단 등 수많은 이점까지 제공한다. ex) JSON, 프로토콜 버퍼
아이템 86. Serializable을 구현할지는 신중히 결정하라
- Serializable을 구현하면 릴리스한 뒤에는 수정하기 어렵다. 클래스가 Serializable을 구현하면 직렬화된 바이트 스트림 인코딩(직렬화 형태)도 하나의 공개 API가 되기 때문이다.
- 모든 직렬화된 클래스는 고유 식별 번호(serialVersionUID)를 부여받는다. 이를 자동 생성되는 값에 의존하면 쉽게 호환성이 깨져버려 InvalidClassException이 발생할 수 있다.
- 기본 역직렬화를 사용하면 불변식 깨짐과 허가되지 않는 접근에 쉽게 노출되어 버그와 보안 구멍이 생길 위험이 높아진다.
- 직렬화 가능 클래스가 수정되면 신버전 인스턴스를 직렬화한 후 구버전으로 역직렬화할 수 있는지, 그리고 그 반대도 가능한지를 검새해야 하므로 테스트할 것이 늘어난다.
- 상속용으로 설계된 클래스는 대부분 Serializable을 구현하면 안 되며, 인터페이스도 대부분 Serializable을 확장해서는 안 된다.
- 내부 클래스는 직렬화를 구현하지 말아야 한다. 단, 정적 멤버 클래스는 구현해도 된다.
아이템 87. 커스텀 직렬화 형태를 고려해보라
- 기본 직렬화 형태는 유연성, 성능, 정확성 측면에서 신중히 고민한 후 합당할 때만 사용해야 한다.
- 객체의 물리적 표현과 논리적 내용이 같다면 기본 직렬화 형태라도 무방하다.
- 기본 직렬화 형태가 적합하다고 결정했더라도 불변식 보장과 보안을 위해 readObject 메서드를 제공해야 할 때가 많다.
- 일시적이란 뜻의 transient 한정자는 해당 인스턴스 필드가 기본 직렬화 형태에 포함되지 않는다는 표시다. 해당 객체의 논리적 상태와 무관한 필드라고 확신할 때만 transient 한정자를 생략해야 한다.
- 기본 직렬화 사용 여부와 상관없이 객체의 전체 상태를 읽는 메서드에 적용해야 하는 동기화 메커니즘을 직렬화에도 적용해야 한다.
- 어떤 직렬화 형태를 택하든 직렬화 가능 클래스 모두에 직렬 버전 UID를 명시적으로 부여하자. (직렬 버전 UID가 꼭 고유할 필요는 없다.)
아이템 88. readObject 메서드는 방어적으로 작성하라
- readObject 메서드는 실질적으로 또 다른 public 생성자이므로 다른 생성자와 똑같은 수준으로 주의를 기울여야 한다. 인수가 유효한지 검사해야 하고 필요하다면 매개변수를 방어적으로 복사해야 한다. (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 InvaldObjectException(start + "가 " + end + "보다 늦다.");
}
- 방어적 복사를 유효성 검사보다 앞서 수행하며, Date의 clone 메서드는 사용하지 않았음에 주목하자. 또한 final 필드는 방어적 복사가 불가능하니 주의하자. 그래서 이 readObject 메서드를 사용하려면 start와 end 필드에서 final 한정자를 제거해야 한다.
- transient 필드를 제외한 모든 필드의 값을 매개변수로 받아 유효성 검사 없이 필드에 대입하는 public 사용자를 추가해도 괜찮은가? 답이 "아니오"라면 기본 readObject 메서드를 사용하는 대신 커스텀 readObject 메서드를 만들어 (생성자에서 수행했어야 할) 모든 유효성 검사와 방어적 복사를 수행해야 한다. 혹은 직렬화 프록시 패턴을 사용하는 방법도 있다.
아이템 89. 인스턴스 수를 통제해야 한다면 readResolve보다는 열거 타입을 사용하라
- 클래스에 implements Serializable을 선언하는 순간 더 이상 싱글턴이 아니다. 어떤 readObject를 사용하든 이 클래스가 초기화될 때 만들어진 인스턴스와는 별개인 인스턴스를 반환하게 되기 때문이다.
- 역직렬화한 객체의 클래스가 readResolve 메서드를 적절히 정의해뒀다면, 역직렬화 후 새로 생성된 객체를 인수로 이 메서드가 호출되고, 이 메서드가 반환한 객체 참조가 새로 생성된 객체를 대신해 반환된다.
- readResolve를 인스턴스 통제 목적으로 사용한다면 객체 참조 타입 인스턴스 필드는 모두 transient로 선언해야 한다. 싱글턴이 transient가 아닌 참조 필드를 가지고 있다면, 그 필드의 내용은 readResolve 메서드가 실행되기 전에 역직렬화되기 때문이다.
- readResolve 메서드를 사용해 '순간적으로' 만들어진 역직렬화된 인스턴스에 접근하지 못하게 하는 방법은 깨지기 쉽고 신경을 많이 써야 하는 작업이므로 가능한 한 열거 타입을 사용하자.
아이템 90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라
- 직렬화 프록시 패턴(serialization proxy pattern)을 사용하는 방법은 다음과 같다.
- 먼저, 바깥 클래스의 논리적 상태를 정밀하게 표현하는 중첩 클래스를 설계해 private static으로 선언한다. 이 중첩 클래스가 바로 바깥 클래스의 직렬화 프록시다.. 중첩 클래스의 생성자는 단 하나여야 하며, 바깥 클래스를 매개변수로 받아야 한다. 이 생성자는 단순히 인수로 넘어온 인스턴스의 데이터를 복사한다. 그리고 바깥 클래스와 직렬화 프록시 모두 Serializable을 구현한다고 선언해야 한다.
- 다음으로, 바깥 클래스에 writeReplace 메서드를 추가한다. 이 메서드는 자바의 직렬화 시스템이 바깥 클래스의 인스턴스 대신 SerializationProxy의 인스턴스를 반환하게 하는 역할을 한다. 이 메서드 덕분에 직렬화 시스템은 결코 바깥 클래스의 직렬화된 인스턴스를 생성해낼 수 없다. (단, readObject 메서드를 통해 역직렬화할 수 없도록 하는 예외 로직을 추가해야 한다.)
- 마지막으로, 바깥 클래스와 논리적으로 동일한 인스턴스를 반환하는 readResolve 메서드를 SerializationProxy 클래스에 추가한다.
- 직렬화는 생성자를 이용하지 않고도 인스턴스를 생성하는 기능을 제공하는데, 이 패턴은 직렬화의 이런 언어도단적 특성을 상당 부분 제거한다. 즉, 일반 인스턴스를 만들 때와 똑같은 생성자, 정적 팩터리, 혹은 다른 메서드를 사용해 역직렬화된 인스턴스를 생성하는 것이다. 따라서 역직렬화된 인스턴스가 해당 클래스의 불변식을 만족하는지 검사할 또 다른 수단을 강구하지 않아도 된다.
'공부 > 이펙티브 자바' 카테고리의 다른 글
11장 동시성 (아이템 78 ~ 84) (0) | 2020.03.24 |
---|---|
10장 예외 (아이템 69 ~ 77) (0) | 2020.03.17 |
9장 일반적인 프로그래밍 원칙 (아이템 57 ~ 68) (0) | 2020.03.08 |
8장 메서드 (아이템 49 ~ 56) (0) | 2020.03.08 |
이펙티브 자바 스터디 (0) | 2020.03.07 |
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크