Post

(Effective Java) 12장 직렬화

아이템 85. 자바 직렬화의 대안을 찾으라 : JSON, protobuf

  • 신뢰할 수 없는 스트림을 역직렬화하면 원격 코드 실행(RCE) 취약점으로 이어질 수 있다.
  • 샌프란시스코 교통국이 공격당했던게 gadget들 연결해서 gadget chain을 만드는 테크닉을 썼었다고 하는데,
    • 아마 ROP였을 듯? 생각해보면 자바에서도 ROP가 안될거 없지.
    • 실행흐름 돌리는 시작은 역직렬화로 한거고…
  • 아무튼 그래서, JSON이나 protobuf를 사용하자!

아이템 86. Serializable을 구현할지는 신중히 결정하라

  • Serializable을 구현하여 릴리즈한 뒤에는 수정하기 어렵다. 수정할거면 버전 호환성을 고려해야 함.
    • 모든 직렬화된 클래스는 serialVersionUID을 부여받는다.
      • static final long으로 직접 명시할 수 있다.
      • 명시되지 않은 경우 시스템이 런타임에 SHA1 적용해서 자동으로 클래스에 생성해서 넣는다.
      • 그래서 클래스 이름, 멤버, 메서드 등이 변경되면 serialVersionUID도 같이 변경되어 같은 클래스여도 InvalidClassException이 발생한다.
  • 버그와 보안 홀이 생길 위험이 높아진다. 역직렬화는 생성자를 거치지 않고 객체를 생성하는 것이나 다름 없으니까.
  • static이 아닌 내부 클래스는 직렬화를 구현하지 말아야 한다.
    • 바깥 인스턴스의 참조와 유효 범위 안의 지역변수 값들을 저장하기 위해 컴파일러가 생성한 필드들이 자동으로 추가되기 때문
    • 단, 정적 멤버 클래스는 구현해도 됨. 그래서 내부 클래스는 웬만하면 static으로 하는 것이 좋다.

아이템 87. 커스텀 직렬화 형태를 고려해보라

  • StringList 클래스가 있고, 현재는 내부의 원소들을 LinkedList로 연결해서 가지고 있다고 가정하자.
  • 적당한 직렬화 형태는 무엇일까?
    • {"문자열 개수":n, "문자열 리스트": []} 정도면 충분할 것이다.
  • 그러나 기본 직렬화 형태를 사용하면 각 노드의 연결 정보를 비롯한 전체 노드에 대한 정보를 모두 기록한다. 이는 다음과 같은 문제가 있다.
    • 현재의 내부 표현 방식에 묶여버린다.
      • 내부 구현을 LinkedList가 아니라 ArrayList로 변경하고 싶다면, 하위 호환성을 고려해서 둘 다 지원할 수 있도록 만들어야만 한다.
    • 너무 많은 공간을 차지한다. 엔트리와 연결 정보는 내부 구현이니 직렬화 형태에는 포함할 가치가 없다.
    • 직렬화 로직은 객체 그래프의 위상에 관한 정보가 없으니 그래프를 직접 순회해보아야 하는데, 이 과정에서 시간이 오래 걸릴 수 있다.
    • 객체 그래프를 순회하는 과정에서StackOverflow가 발생할 수 있다.

아이템 88. readObject 메서드는 방어적으로 작성하라

  • 사실상 역직렬화를 통한 생성자라고 볼 수 있기 때문에, 인수 유효성 체크나 매개변수의 방어적 복사 등등을 수행해야함.

아이템 89. 인스턴스 수를 통제해야 한다면 readResolve 보다는 enum 타입을 사용하라

  • 인스턴스 수를 통제한다는건, 싱글턴 같은걸 말한다.
  • readResolve()는 역직렬화 후 새로 생성된 객체를 인수로 이 메서드가 호출되고, 이 메서드가 반환한 객체 참조가 새로 생성된 객체를 대신해 반환된다.
    • 대부분의 경우 이때 새로 생성된 객체 참조는 유지하지 않으므로 바로 가비지 컬렉션 대상이 된다.
  • 근데, 만약 transient로 선언되지 않은 객체 참조 타입 인스턴스 필드가 있다면 readResolve가 수행되기 전, 역직렬화된 객체의 참조를 공격할 여지가 있다.
  • 그래서 enum 싱글턴 을 쓰자!

아이템 90. 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라

  • 내부에 private static으로 프록시용 클래스를 하나 만들고, 프록시 클래스를 거쳐서 직렬화/역직렬화 하는 패턴을 사용하면 앞서 얘기했던 직렬화의 위험성을 크게 줄일 수 있다.
    • 바깥인스턴스 -> 프록시인스턴스 -> 직렬화 / 역직렬화 -> 프록시인스턴스 -> 바깥인스턴스
    • effectivejava/chapter12/item90/Period.java
    • 실제로 EnumSet은 직렬화 프록시 패턴을 사용하고 있다.
  • 단, 직렬화 프록시는 다음과 같은 한계가 있다.
    • 클라이언트가 확장할 수 있는 클래스에는 적용할 수 없다.
    • 객체 그래프에 순환이 있는 클래스에는 적용할 수 없다.
      • ClassCastException이 발생한다. 실제 객체가 아직 만들어지지 않았기 때문.
    • 조금 느리다.
This post is licensed under CC BY 4.0 by the author.