아이템 85. 자바 직렬화의 대안을 찾으라
- 직렬화는 공격 범위가 넓어 방어하기 어렵다.
ObjectInputStream
의 readObject
메서드를 호출하면서 객체 그래프가 역직렬화된다. 이는 클래스패스 안의 모든 타입의 객체를 만들어 낼 수 있다. → 코드 전체가 공격 범위
- 직렬화 위험을 피하는 방법은 아무것도 역직렬화하지 않는 것이다.
- 자바 직렬화 대신 JSON, 프로토콜 버퍼와 같은 방식을 사용하자.
JSON : 텍스트 기반
프로토콜 버퍼 : 이진 표현 (효율 훨씬 높음)
- 신뢰할 수 없는 데이터는 절대 역직렬화하지 말자.
직렬화를 피할 수 없고, 역직렬화에 대한 확신이 없다면 객체 역직렬화 필터링(ObjectInputFilter)를 사용하자.
블랙리스트 방식 < 화이트리스트 방식
- 어쩔 수 없다면 최대한 주의하자
아이템 86. Serializable을 구현할지는 신중히 결정하라
-
Serializable
구현 문제점
1. Serializable
을 구현하면 릴리스 후에 수정하기 어렵다.
직렬화된 바이트 스트림 인코딩도 하나의 공개 API가 되기 때문.
기본 직렬화 형태에서는 private까지 API로 공개되버림.
내부 구현 수정하면 원래의 직렬화 형태와 달라짐.
2. 버그, 보안 위험
직렬화는 생성자를 이용해서 만드는 것이 아니라, 우회해서 생성 → "숨은 생성자"
3. 신버전 릴리스 시, 테스트할 것이 늘어난다.
-
그럼에도 불구하고, 객체 전송, 저장에 자바 직렬화를 이용하는 프레임워크용으로 만든 클래스라면 어쩔 수 없다.
Serializable
을 반드시 구현해야하는 클래스의 컴포넌트로 쓰일 때도.
-
상속용으로 설계된 클래스, 인터페이스는 Serializable
를 확장하면 안된다.
-
내부 클래스는 Serializable
를 구현하면 안된다. (정적 멤버 클래스는 가능)
아이템 87. 커스텀 직렬화 형태를 고려해보라
- 객체의 물리적 표현과 논리적 표현이 같을 때는 기본 직렬화를 써도 괜찮다.
- 물리적 표현 != 논리적 표현 인데 기본 직렬화를 썼을 때의 문제
1. 공개 API가 현재 내부 구현 방식에 영원히 묶임
2. 너무 많은 공간 차지
모든 내부 구현까지 포함하기 때문
3. 시간 오래 걸림
모든 객체 그래프 순회하기 때문
4. 스택 오버플로우 위험
- 해당 객체의 논리적 상태와 무관한 필드라고 확신할 때만
transient
를 생략할 수 있다.
transient
붙이면 기본 직렬화 형태에 포함 X
기본 직렬화 사용하면 transient
붙은 필드는 기본값으로 초기화됨
- 직렬화 형태에 상관없이 직렬화 가능 클래스 모두에 UID를 명시적으로 부여하자
아이템 88. readObject 메서드는 방어적으로 작성하라
readObject
메서드는 public 생성자를 다룰 때처럼 주의를 기울이자. (readObject
는 매개변수로 바이트 스트림을 받는 생성자)
- 해당 클래스의 불변식을 깨뜨리지 않도록 주의하자.
객체를 역직렬화할 때는 클라이언트가 소유해서는 안되는 객체 참조를 갖는 필드를 모두 방어적으로 복사해야 함
→ readObject
에서 불변 클래스 안의 모든 private 가변 요소를 방어적으로 복사하자
- 방어적 복사를 유효성 검사보다 먼저 수행하자. (final 필드는 불가능)
- 기본
readObject
를 사용해도 좋을지 판단하는 방법
transient
제외한 모든 필드의 값을 매개변수로 받아 유효성 검사 없이 필드에 대입하는 public 생성자를 추가해도 괜찮은가?에 대한 답이 "예" 일때만 사용해라
readObject
에서 재정의 가능 메서드를 호출해서는 안된다
생성자와의 공통점
아이템 89. 인스턴스 수를 통제해야 한다면 readResolve보다는 열거 타입을 사용하라
Serializable
을 구현하는 순간 더이상 싱글턴이 아니게 된다.
역직렬화 시, readObject
를 사용하면 새로운 인스턴스를 생성하기 때문.
→ readResolve
를 이용하면 readObject
가 만든 인스턴스를 다른 것으로 대체할 수 있음
readResolve
를 인스턴스 통제 목적으로 사용한다면 객체 참조 타입 인스턴스 필드는 모두 transient
로 선언해야 한다.
싱글턴이라면 직렬화 형태는 아무런 실 데이터를 가질 이유가 없기 때문
- 열거 타입을 사용하면 싱글턴임을 자바가 보장해준다.
하지만, 컴파일 타임에 어떤 인스턴스들이 있는지 알 수 없는 상황에는 열거 타입으로 표현하는 것이 불가능하므로 readResolve
방식을 써야 한다.
아이템 90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라
- 직렬화 프록시 : 직렬화를 위해 해당 클래스 안에 만든 private static 중첩 클래스 (생성자는 1개만 있어야 함. 바깥 클래스를 매개변수로 받는)
- 바깥 클래스에
writeReplace
메서드 추가
직렬화가 이뤄지기 전, 바깥 클래스의 인스턴스를 직렬화 프록시로 변환
- 직렬화 프록시에
readResolve
메서드 추가
역직렬화 시 직렬화 시스템이 직렬화 프록시를 다시 바깥 클래스의 인스턴스로 변환하게 해줌 (일반 인스턴스 만들 때와 같은 생성자, 정적 팩터리 메서드 이용할 수 있음 → 생성 시에 검사 잘해주고 있다면 따로 더 해줄 일 없어서 아름다움)
- 직렬화 프록시 한계
1. 클라이언트가 확장할 수 있는 클래스에는 적용 불가
2. 객체 그래프 순환이 있는 클래스에는 적용 불가
3. 방어적 복사보다 느림
프록시 테스트 코드가 있는 링크
https://madplay.github.io/post/consider-serialization-proxies-instead-of-serialized-instances