Java

자바 직렬화와 JPA Entity, Spring

팅리엔 2024. 8. 30. 22:54

Serialization

  • 데이터 구조(객체, 리스트 등)을 바이트 스트림으로 변환하는 것
  • 시스템 간 데이터 전송, 저장을 위함
  • types
    • string: csv, xml, json
    • binary: java serialization, apache avro, protocol buffer

 
 

Java Serialization

<이펙티브 자바>를 보면 이런 문장이 있다.
당신이 작성하는 새로운 시스템에서 자바 직렬화를 써야 할 이유는 전혀 없다.
 

구현

  • primitive type이거나 Serializable 인터페이스를 구현해야 한다.
  • 클래스의 모든 멤버가 직렬화 가능해야 한다. (trasient 멤버 제외)
  • serialVersionUID: 객체의 버전. 미입력시 컴파일러가 자동으로 만들어준다. 
  • ObjectOutputStream의 writeObject로 직렬화 하고, ObjectInputStream의 readObject로 역직렬화 한다.

 

단점

  • serialiVersionUID를 임의 지정해주지 않았다면 클래스(이름, 구현 인터페이스, 패키지 등) 정보 변경 시 UID가 변경된다. 역직렬화 시 클래스 정보가 불일치한다면 InvalidClassException이 발생한다. 그러니 직렬화 객체에는 항상 serialVersionUID를 정의해주는 것이 좋다.
  • 외부 자바 시스템에서 역직렬화 하려면 동일한 클래스(패키지까지 같아야 한다!)가 존재해야 한다.
  • 직렬화 시 객체 데이터와 클래스 정보까지 저장하므로 용량을 많이 잡는다. (json의 거의 2배라고 한다.)
  • 직렬화할 때 객체 그래프를 순회하기에 시간이 너무 많이 걸릴 수 있다. 스택오버플로를 일으킬 수 있다.
  • 위험한 데이터가 역직렬화 되어 악성 코드가 실행될 수 있다. ObjectInputStream의 readObject() 메서드는 객체 그래프가 역직렬화 되고, 클래스패스 안의 거의 모든 타입의 객체를 만들어 낼 수 있다. 이 말은 그 타입들의 코드 전체가 공격 대상이 된다는 것이다. 자신의 코드 뿐만 아니라 서드파티 라이브러리까지 공격 범위에 포함된다.
  • Serializable을 구현하면 릴리스한 뒤에는 수정하기 어렵다. 직렬화 형태는 적용 당시 클래스의 내부 구현 방식에 영원히 묶여버린다. 메서드를 추가하면 바뀐다… 클래스의 private과 package-private 필드들마저도 결국 외부에 노출되어 버리는 꼴이다…
  • 상속용으로 설계된 클래스, 인터페이스는 대부분 Serializable을 구현하면 안 된다. 확장하는 쪽에 커다란 부담을 지우게 된다. (Serializable을 구현한 클래스만 지원하는 프레임워크를 쓴다면 뭐 어쩔 수 없다.)

 

결론

  • json이나 protobuf와 같은 다른 방식을 쓰자. 클래스가 직렬화를 지원하도록 만들지 말자. cost가 들겠지만 자바 직렬화를 json 등으로 마이그레이션 하자.
  • 역직렬화를 해야만 한다면 자바 9에서 소개된 ObjectInputFilter를 이용해 블랙리스트 클래스들이 필터링 되도록 하자. (블랙리스트 방식보다는 허용된 클래스만 수용하는 화이트리스트 방식을 추천한다.)

 

 



 

JPA Entity

Q. JPA Entity는 Serializable을 구현해야 하나요?
A. 아니요.
 

JPA, Hibernete and Spring Data JPA

JPA(Jakarta Persistence API)는 Java ORM 표준 스펙이다. JPA 구현체로 Hibernate, EclipseLink 등이 있다. Spring Data JPA는 JPA를 편리하게 사용하도록 지원하는 프로젝트다. Spring Data를 사용하면 리파지토리 인터페이스만 작성하면 구현 객체를 동적으로 생성해서 주입해준다. Spring Data는 default JPA 구현체로 Hibernate를 쓴다. 
 

JPA Entity and Serialization

우선 JPA spec 3.1(spring-data-jpa 3.3.3)에서는 객체를 외부로 전송할 경우에 직렬화 가능하도록 Serializable을 구현하라고 한다. 

If an entity instance is to be passed by value as a detached object (e.g., through a remote interface), the entity class must implement the Serializable interface.

 
ORM은 객체를 데이터베이스 쿼리로, 쿼리 결과를 객체로 매핑해준다. 개념만 생각해보면 자바 직렬화를 하는 게 아니다. 하지만 JPA 구현체가 어딘가에서 자바 직렬화를 사용할 가능성도 있다. Hibernate에서는 Entity가 Serializable을 구현할 필요는 없다.
 
나는 데이터 공유 시 DTO가 아닌 Entity를 사용하여 Entity를 노출하는 것은 적절치 않다고 생각하며, 굳이 그래야 한다고 해도 json 타입 등으로 전달할 것이므로 Entity를 Serializable 하도록 만들 일이 없을 것 같다.
 

JPA Key class and Serialization

The primary key class must be serializable.

 
그런데 JPA 표준 스펙에서 식별자 클래스는 Serializable을 구현하라고 한다. 왜 이렇게 정의했는지 정확한 의도를 모르겠다. 아무튼 영속성컨텍스트는 객체 관리를 위해 내부 Map 구조에 식별자와 객체 참조를 사용하고, Hibernate는 이 때 식별자를 직렬화 하므로 식별자 클래스는 Serializable 해야 한다.

 

 


 

 

spring에서의 json 직렬화도 슬쩍 봐보자면,

Spring 에서의 직렬화

스프링 @RestController에서 객체를 반환하면 json으로 변환을 해준다. 스프링에서는 요청/응답 메시지를 어디에서 객체/json으로 변환 하는가? 바로 HttpMessageConverter이다. 스프링부트에서 default json mapping 라이브러리는 Jackson이다. MappingJackson2HttpMessageConverter라는 구현체를 사용한다.

 

그렇다면 Jackson 라이브러리의 직렬화/역직렬화 방법은 어떻게 되는가? Json은 Reflection API를 활용해 클래스 정보를 동적으로 읽어서 직렬화/역직렬화를 한다. 

  • 직렬화 하려면, 필드의 접근 제어자를 public으로 한다. OR, 표준 getter(getXXX)를 선언한다.
  • 역직렬화 하려면, 표준 setter/getter를 선언한다. OR, all args 생성자가 하나만 존재한다. OR, 기본 생성자가 존재한다.

알아두면 유용한 Jackson 어노테이션

  • @JsonIgnore: Jackson은 boolean 타입 반환 시 예외적으로 isXXX()도 getter로 인식하여 직렬화 하는데 이를 무시시킬 수 있다.
  • @JsonCreateor: 객체 역직렬화 시 생성자, 팩토리 메서드 선택 방식을 결정할 수 있다.
  • @JsonProperty: json 필드와 java 필드 이름을 다르게 할 수 있다.
  • @JsonFormat: json 응답의 날짜/시간 형식, 타임존을 지정할 수 있다.