자바

이펙티브 자바 -11-

정한_s 2022. 1. 17. 14:16

직렬화

객체 직렬화란 자바가 객체를 바이트스트림으로 인코딩하고 그 바이트스트림으로 다시 객체를 재구성하는 매커니즘이다. 

더보기

직렬화 쓰는 이유 : 기존의 정보를 그대로 영속화 할때 사용한다. 직렬화된 객체는 네트워크로 전달 할 수 도 있으며 역직렬화하여 객체를 바로 사용할 수 있다. 

 

  • 서블릿 세션들은 대부분 세션의 Java 직렬화를 지원하고 있습니다. 단순히 세션을 서블릿 메모리 위에서 운용한다면 직렬화가 필요없지만, 파일로 저장하거나 세션 클러스터링, DB를 저장하는 옵션등을 선택하게 되면 세션 자체가 직렬화되어 저장되어 전달됩니다.
  • 캐시에서도 직렬화는 사용됩니다. 주로 DB를 조회한 후 가져온 데이터 객체 같은 경우 실시간 형태로 요구하는 데이터가 아니라면 메모리, 외부 저장소, 파일 등을 저장소를 이용해서 데이터 객체를 저장한 후 동일한 요청이 오면 DB를 다시 요청하는 것이 아니라 저장된 객체를 찾아서 응답하게 하는 형태를 캐시를 사용한다라고 말합니다. 캐시를 이용하면 DB 리소스를 절약할 수 있기 때문에 많은 시스템에서 자주 활용됩니다. 이렇게 캐시할 부분을 자바 직렬화된 데이터를 저장해서 사용됩니다.

 

자바 직렬화의 대안을 찾으라

직렬화의 근본적인 문제는 공격범위가 넓어 방어하기 어렵다는 것이다. ObjectInputStream의 readObject 메서드를 호출하면서 객체 그래프가 역직렬화 되기 때문이다. readObject 메서드는 Serializable 인터페이스를 구현했다면 해당 객체를 만들어 낼 수 있다. 

 

신뢰할 수 없는 바이트 스트림을 역직렬화 하는 일 자체가 스스로를 공격에 노출하는 행위이다. 따라서 직렬화 위험을 회피하는 가장 좋은 방법은 아무것도 역직렬화하지 않는 것이다.

 

객체와 바이트 시퀀스를 변환해주는 다른 메커니즘이 많이 있다. 그 예로 JSON이나 프로토콜 버퍼(protobuf)가 있다.

 

만약 직렬화를 배제할 수 없을 때는 신뢰할 수 없는 데이터는 절대 역직렬화하지 않아야 한다. 역직렬화한 데이터가 안전한지 완전히 확신할 수 없다면 객체 역직렬화 필터링을 사용하자. 객체 역직렬화 필터링은 역직렬화되기 전에 필터를 설치하는 기능이다.

 

객체 역직렬화 필터

  • 기본 수용 모드 : 블랙리스트에 기록된 잠재적으로 위험한 클래스들을 거부한다
  • 기본 거부 모드 : 화이트리스트에 기록된 안전하다고 알려진 클래스만 수용

블랙리스트 방식보다는 화이트리스트 방식을 추천한다

 

정리

직렬화는 위험하니 피해야 한다. JSON이나 프로토콜 버퍼 같은 대안을 사용하자

 

Serializable을 구현할지는 신중히 결정하라

Serialiable을 구현하면 릴리스한 뒤에는 수정하기 어렵다.

 

Serialiable 문제점

  • 모든 직렬화 클래스는 고유 식별 번호를 부여받는다. serialVersionUID 라는 필드로, 이 번호를 명시하지 않으면 시스템이 런타임에 암호 함수 (SHA-1)를 적용해 자동으로 클래스 안에 넣는다. 암호화를 할때 대부분의 클래스 멤버들이 고려되므로 이들 중 하나라도 수정되면 UID 값도 변한다
  • 버그와 보안에 구멍이 생길 위험이 높다. 객체는 생성자를 사용해 만드는 것이 기본인데, 직렬화는 이 기본을 우회하는 객체 생성 기법이다
  • 해당 클래스 신버전을 릴리스 할때 테스트할 것이 늘어난다.

보통 '값' 클래스와 컬렉션 클래스는 Serializable로 구현하고 스레드 풀처럼 '동작'하는 객체를 표현하는 클래스는 대부분 Serializable을 구현하지 않았다

 

커스텀 직렬화 형태를 고려해보라

클래스를 직렬화하기로 했다면 어떤 직렬화 형태를 사용할지 심사숙고해라. 객체를 적절히 설명하는 직렬화 형태를 고안해라. 

 

* transient는 Serialize하는 과정에 제외하고 싶은 경우 선언하는 키워드이다.

 

readObject 메서드는 방어적으로 작성하라

readObject 메서드를 작성할 때는 언제나 public 생성자를 작성하는 자세로 임해야 한다. readObject는 어떤 바이트 스트림이 넘어오더라도 유효한 인스턴스를 만들어내야한다. 바이트 스트림이 진짜 직렬화된 인스턴스라고 가정해서는 안된다. 

  • private이어야 하는 객체 참조 필드는 각 필드가 가리키는 객체를 방어적으로 복사하라
  • 모든 불변식을 검사하여 어긋나는게 발견되면 invalidObjectException을 던진다
  • 직접적이든 간접적이든, 재정의 할 수 있는 메서드는 호출하지 않는다

인스턴스 수를 통제해야 한다면 readResolve 보다는 열거 타입을 사용하라

readResolve 기능을 이용하면 readObject가 만들어낸 인스턴스를 다른 것으로 대체할 수 있다. 역직렬한 객체의 클래스가 readResolve 메서드를 적절히 정의해뒀다면, 역직렬화 후 새로 생성된 객체를 인수 이 메서드가 호출되고, 이 메서드가 반환한 객체 참조가 새로 생성된 객체를 대신해 반환한다

 

불변식을 지키기 위해 인스턴스를 통제해야 한다면 가능한 열거 타입을 사용하자. 

 

직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라

제3자가 확장할 수 없는 클래스라면 가능한 직렬화 프록시 패턴을 사용하자. 이 패턴은 중요한 불변식을 안정적으로 직렬화 해주는 가장 쉬운 방법이다