-
[Java] Effective Java 3/E 정리 - 12장 직렬화Study/이펙티브 자바 2021. 8. 16. 20:54반응형
객체 직렬화란 자바가 객체를 바이트 스트림으로 인코딩하고 (직렬화)
그 바이트 스트림으로부터 다시 객체를 재구성하는 (역직렬화) 메커니즘이다.
직렬화된 객체는 다른 VM에 전송하거나 디스크에 저장 후 나중에 역직렬화할 수 있다.
이번 장은 직렬화의 위험과 그 위험을 최소화하는 방법에 대해 알아보자.
Item 85. 자바 직렬화의 대안을 찾으라
직렬화는 생각보다 장점보다 위험성이 크다.
특히 역직렬화 과정에서 호출되는 readObject(Serializable 인터페이스 구현시)는
클래스 패스안의 거의 모든 타입 객체를 만들어내고 모든 코드를 수행할 수 있기 때문에 모든 코드가 공격 대상이 될 수 있다.
가젯(gadget)
역직렬화 과정에서 호출되어 잠재적으로 위험한 동작을 하는 메서드를 가젯(gadget)이라고 부른다.
여러 가젯을 함께 묶어 가젯 체인을 구성할 수도 있는데, 공격자가 기반 하드웨어의 네이티브 코드를 맘대로 실행할 수 있는 강력한 가젯 체인도 발견된다.
역직렬화 폭탄(deserialization bomb)
역직렬화에 시간이 오래 걸리는 짧은 스트림을 역직렬화하는 것만으로도 서비스 거부 공격에 노출되는데,
이러한 슽림을 역직렬화 폭탄이라고 한다.
다음은 HashSet과 문자열만 사용해 만든 예다. 이 스트림의 역직렬화는 영원히 계속된다.
static byte[] bomb() { Set<Object> root = new HashSet<>(); Set<Object> s1 = root; Set<Object> 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); // 간결하게 하기 위해 이 메서드의 코드는 생략함 }
이러한 위험을 회피하는 가장 좋은 방법은 아무것도 역직렬화하지 않으면 된다.
객체-바이트 시퀀스를 변환해주는 다른 메커니즘은 많고,
이를 사용하면 자바 직렬화보다 간단하고 자바 직렬화의 위험을 회피할 수 있다.
그 중에서 대표적인 것은 JSON과 프로토콜 버퍼(Protocol Buffers 혹은 protobuf)다.
JSON은 텍스트 기반 표현에 효과적이고, 프로토콜 버퍼는 이진 표현이라 효율이 좋다.
그러나 직렬화를 피할 수 없고, 역직렬화한 데이터가 안전한지 확신할 수 없다면
객체 역직렬화 필터링을 사용하여 위험한 클래스를 배제하거나 안전한 클래스만 수용하자.
Item86. Serializable을 구현할지는 신중히 결정하라
어떤 클래스의 인스턴스를 직렬화하려면 implements Serializable을 붙여 구현해주면 된다.
손쉽게 구현할 수 있다고 생각되지만, 아래와 같은 문제점이 발생한다.
Serializable을 구현하면 릴리스한 뒤에는 수정하기 어렵다.
클래스가 Serializable를 구현하면 직렬화된 바이트 스트림 인코딩(직렬화 형태)도 하나의 공개 API가 된다. 직렬화 형태도 다른 공개 API와 마찬가지로 계속 지원해야 되고,
자바의 기본 방식을 사용한 직렬화 형태는 private, package-private 필드를 공개하여 캡슐화를 깨뜨린다.
그러나 클래스 내부 구현을 수정하면 원래의 직렬화 형태와 달라지기 떄문에 이를 주의하면서 설계한다는 것은 굉장히 까다롭다.
Serializable을 구현하면 버그와 보안 구멍이 생길 위험이 높아진다.
객체는 기본적으로 생성자를 사용해 만들어야 하지만, 직렬화는 기본 메커니즘을 우회하는 생성 기법이다.
따라서 역직렬화는 일반 생성자의 문제가 그대로 적용되는 '숨은 생성자'고, 불변식 깨짐과 허가되지 않은 접근에 쉽게 노출된다.
Serializable을 구현하면 해당 클래스의 신버전을 릴리스할 때 테스트할 것이 늘어난다.
직렬화 가능 클래스가 수정되면 신버전 인스턴스를 직렬화한 후 구버전으로 역직렬화할 수 있는지, 그리고 그 반대도 가능한지를 검사해야 한다.
양방향 직렬화/역직렬화가 모두 성공하고, 원래의 객체를 충실히 복제해내는지를 반드시 확인해야 한다.
따라서 테스트 할 양이 직렬화 가능 클래스의 수와 릴리스 횟수에 비례하여 늘어난다.
되도록 Serializable를 구현하지 않는 것이 좋지만, 자바 역직렬화를 사용하는 프레임워크라면 선택지가 없다.
역사적으로 BigInteger와 Instant와 같은 '값' 클래스와 컬렉션 클래스들은 구현했고,
스레드 풀처럼 '동작'하는 캑체를 표현하는 클래스들은 구현하지 않았다.
만약 Serializable를 구현해야 한다면 몇가지 주의사항을 지키자.
- 상속용으로 설계된 클래스는 구현하면 안되며, 인터페이스도 대부분 Serializable을 확장하면 안된다.
- 직렬화, 확장이 모두 가능할 때 인스턴스 필드 값 중 불변식을 보장해야 한다면, 반드시 하위 클래스에서 finalize 메서드를 재정의하지 못하게 해야한다.
- 인스턴스 필드 중 기본값으로 초기화되면 위배되는 불변식이 있다면 클래스에 다음의 readObjectNoData 메서드를 반드시 추가해야 한다.private void readObjectNoData() throws InvalidObjectException { throw new InvalidObjectException("스트림 데이터가 필요합니다"); }
- 내부 클래스는 직렬화를 구현하지 않는다.
Item87. 커스텀 직렬화 형태를 고려해보라
Serializable를 구현하고 기본 직렬화 형태를 사용한다면,
기본 직렬화 형태를 유지해야하기 때문에 다음 릴리스때 구현 상태를 수정하기가 힘들다.
따라서 기본 직렬화 형태는 유연성, 성능, 정확성 측면에서 모두 고민해보고 사용하자.
다음은 기본 직렬화를 사용해도 될지 고민할 때 참고하면 좋다.
- 객체의 물리적 표현과 논리적 내용이 같다면 기본 직렬화 형태라도 무방하다.
- 기본 직렬화 형태가 적합하더라도 불변식 보장과 보안을 위해 readObject메서드를 제공해야 할 떄가 많다.
Item88. readObject 메서드는 방어적으로 작성하라
readObject는 매개변수로 바이트 스트림을 받는 생성자라고 할 수 있다.
그런데 바이트 스트림을 의도적으로 수정하여 비정상적인 객체를 만들어 낼 수 있다.
예를 들어 클래스의 불변식을 깨뜨리거나 불변 필드를 수정하고자 참조를 추가하여 가변 필드로 만들 수 있다.
이런 문제를 방지하기 위해서는 모든 불변식을 검사하고, 객체 참조를 갖는 필드를 모두 방어적으로 복사해야한다.
Item89. 인스턴스 수를 통제해야 한다면 readResolve보다는 열거 타입을 사용하라
싱글톤으로 구현한 클래스에 Serializable를 구현하면 더 이상 싱글톤이 아니게 된다.
어떤 readObject를 사용하든 이 클래스가 초기화될 때 만들어진 인스턴스와는 별개인 인스턴스를 반환하게 된다.
readResolve는 readObject가 만들어낸 인스턴스를 대체하여 싱글톤을 유지할 수 있다.
(readResolve가 기존의 인스턴스를 반환하여 대체하고, 새로 만들어낸 인스턴스는 가비지 컬렉션 대상이 됨.)
그리고 readResolve를 인스턴스 통제 목적으로 사용한다면 객체 참조 타입 인스턴스 필드는 모두 transient로 선언하자.
그렇지 않으면 readResolve 수행 전에 역직렬화된 객체의 참조를 공격할 여지가 있다.
가장 좋은 방법은 transient보다는 왼소 하나짜리 열거 타입으로 바꾸는 것이다.
직렬화 가능한 인스턴스 통제 클래스를 열거 타입으로 구현하면 선언한 상수 외에 다른 객체는 존재하지 않음을 자바가 보장해 준다.
Item90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라
아이템마다 계속 언급되었지만 Serializable를 구현한다면 정상 메커니즘 생성자 외에 인스턴스를 생성할 수 있다.
이 때, 직렬화 프록시 패턴을 사용하면 버그와 보안 문제가 일어날 가능성을 줄일 수 있다.
직렬화 프록시 패턴은 아래와 같은 방법으로 만들 수 있다.
- 논리적 상태를 표현하는 중첩 클래스를 private static으로 선언한다. (직렬화 프록시)
- 중첩 클래스의 생성자는 단 하나, 바깥 클래스를 매개변수로 받는다.
- 생성자는 인수로 넘어온 인스턴스의 데이터를 복사한다.
참고로, 바깥 클래스와 중첩 클래스 모두 serializable 구현한다고 선언해야한다.
직렬화 프록시를 사용하면 직렬화시에 바깥 클래스의 직렬화된 인스턴스 대신 직렬화 프록시로 변환해준다.
역직렬화시에는 직렬화 프록시를 다시 바깥클래스로 변환하게 되고,
바깥 클래스의 생성자 혹은 정적 팩터리를 거치기 때문에 불변식을 검사하지 않아도 된다.
'Study > 이펙티브 자바' 카테고리의 다른 글
[Java] Effective Java 3/E 정리 - 11장 동시성 (0) 2021.08.11 [Java] Effective Java 3/E 정리 - 10장 예외 (0) 2021.07.31 [Java] Effective Java 3/E 정리 - 9장 일반적인 프로그래밍 원칙 (0) 2021.07.28 [Java] Effective Java 3/E 정리 - 8장 메서드 (2) 2021.07.18 [Java] Effective Java 3/E 정리 - 6장 열거 타입과 어노테이션 (0) 2021.07.07