[Spring] ObjectMapper 커스텀해서 사용해보기 (feat. serialization)

들어가기

스프링부트는 ObjecctMapper를 통해서 JSON과 객체 간의 데이터를 변환(직렬화와 비직렬화)한다.

이번엔 직렬화와 비직렬화가 무엇인지 그리고 ObjectMapper에 대해서 알아보고 직접 커스텀해서 사용하고자 한다. 

 

우리가 네트워크 통신을 할 때 어떤 형태의 데이터를 주고 받을까?

네트워크 통신에서 데이터는 최종적으로 바이트 코드 형태로 전송된다. 

다른 형태의 데이터(ex. 텍스트, 이미지, 비디오 등)도 결국은 전송을 위해선 바이트 스트림으로 구성되어 전송된다.

 

JSON, XML과 같은 데이터 형식도 결국에는 바이트 스트림으로 변환되어 네트워크를 통해 송수신된다. 

그렇기에 네트워크의 각 계층에서 추가되는 패킷들도 결국은 바이트 배열로 구성되어 있다.

이유는 네트워크 하드웨어와 프로토콜이 바이트 기반으로 동작하기 때문이다. 

 

즉, 네트워크 통신에선 궁극적으로 모든 데이터가 바이트 스트림 형태로 전송된다

HTTP 포함한 모든 네트워크 통신은 바이트 단위로 이뤄진다.

HTTP(Hypertext Transfer Protocol)는 하이퍼텍스트 문서를 전송하기 위해 사용되는 프로토콜이다. 

 

HTTP도 네트워크 통신이기에 결국은 바이트 코드 형태로 전송된다.

하이퍼텍스트나 다른 형태의 데이터가 최종적으로 바이트로 변환되어 전송된다. 

이는 앞서 말했듯 HTTP를 포함한 모든 네트워크 통신이 바이트 단위로 이루어지기 때문이다. 

 

따라서 HTTP 통신 과정에서도 데이터는 바이트 형태로 인코딩되어 전송되며, 수신 측에서 이를 다시 적절한 형태로 디코딩합니다.

 

그러면 JSON은 뭐야?

JSON은 {key:value} 형태의 데이터 표맷이다.

키와 값의 쌍으로 구성되며, {객체}, [배열], 문자열, 숫자, boolean(true/false), null 값 중 하나를 가질 수 있다.

 

이러한 JSON도 앞서 말햇듯, 결국엔 네트워크를 통해 전송될 때에는 바이트로 변환된다. 

JSON 문자열도 결국은 텍스트이므로, 네트워크를 통해 전송하기 전엔 바이트 스트림으로 인코딩된다.

 

수신 측에서는 이 바이트 스트림을 다시 문자열로 디코딩하고, 필요한 경우 JSON 파싱을 통해 원래의 데이터 구조로 변환할 수 있다. 

 

따라서, JSON을 포함한 모든 데이터 포맷은 결국 네트워크 통신을 위해 바이트 스트림으로 변환 과정을 거친다. 

 

직렬화(Serialization)와 역직렬화(Deserialization)

앞서 말했던 네트워크 통신에서 바이트 스트림으로 변환되는 과정을 

흔히, 직렬화라고 부른다. 

 

이제부터는 직렬화에 대해 좀 더 살펴보자!

직렬화(Serialization)

일반적으로 직렬화는 객체의 상태를 저장하거나 다른 환경으로 전송 가능한 형태로 변환하는 과정을 의미한다.

이를 통해 메모리에 있는 객체를 파일 시스템, 데이터베이스, 또는 네트워크를 통해 다른 시스템으로 전송할 수 있게 된다.

 

직렬화된 형태는 바이트 스트림, JSON, XML 등이 될 수 있으며,

이 과정은 데이터의 영속성을 제공하고, 다양한 시스템 간의 데이터 교환을 가능하게 한다.

더보기

직렬화를 통해 데이터 영속성을 제공한다는 말은, 메모리 상에 있는 객체의 상태를 파일 시스템, 데이터베이스 등의 다른 영구적인 저장소에 저장 가능한 형식으로 변환하여 저장할 수 있게 해준다는 것을 의미한다.

 

역직렬화(Deserialization)

일반적으로 역직렬화는 직렬화된 데이터(바이트 스트림, 문자열 형식 등)를 원래의 데이터 형태(객체, 데이터 구조 등)로 복원하는 과정을 의미한다.

이 과정은 데이터를 전송하거나 저장한 후, 그 데이터를 다시 사용 가능한 형태로 되돌리기 위해 필요하다.

 

예를 들어, 네트워크를 통해 전송받은 바이트 스트림을 객체로 복원하거나, 파일에 저장된 JSON 문자열을 자바 객체로 변환하는 것이 역직렬화에 해당됩니다.

중요한 것은 데이터의 형식(바이트, 텍스트 등)이 아니라, 그 데이터를 원래의 데이터 구조나 객체 등으로 복원한다는 점입니다.

 
 

자바에서의 직렬화와 역직렬화

Java의 직렬화:

  • 어떤 객체(Class)를 바이트 형태로 변환하는 과정을 의미한다.
  • 자바에서는 클래스의 인스턴스를 바이트 스트림으로 변환한다. 

Java의 역직렬화:

  • 직렬화한 바이트 스트림을 다시 객체(Class)로 변환하여 JVM 메모리에 저장하는 과정을 의미한다. 

즉, 자바에서는 객체를 파일 시스템에 저장하거나 다른 컴퓨터로 전송하려면 바이트 형태로 변환하는 과정이 필요한데, 이를 직렬화라고 한다. 그 반대의 과정을 역직렬화라고 한다. 


스프링부트에서는 직렬화하면 주로 떠오르는 키워드는 ObjectMapper를 가지고 있는 Jackson 라이브러리다.

지금부터는 Jackson 라이브러리를 살펴보고자 한다.

 

스프링부트에서의 직렬화/역직렬화

스프링부트가 선택한 Jackson 라이브러리 (ObjectMapper)

Jackson 라이브러리는 자바에서 JSON 데이터를 처리하기 위한 라이브러리다.

ObjectMapper는 자바 객체와 JSON 데이터 사이의 직렬화와 역직렬화를 담당하는 클래스다.

 

여기서 한 가지 알아야 부분이 있다. 

앞서 네트워크 통신은 바이트 스트림 기반이라고 언급했다.

 

ObjectMapper가 자바 객체를 JSON 데이터로 변환(직렬화)했더라도,

추가로 JSON 데이터를 바이트 스트림으로 변환하는 작업를 거쳐야만 네트워크 통신이 가능하다. 

이 작업은 JsonGenerator 의해 처리된다. (반대로 읽을 때는 JsonParser가 처리한다.)

 

ObjectMapper - JsonGenerator - 바이트 스트림 - JsonParser - ObjectMapper

ObjectMapper를 사용하여 Java 객체를 JSON으로 직렬화하면,

JsonGenerator를 사용하여 JSON 데이터를 바이트 스트림으로 쓴다.(wirte) 

 

JsonParser를 통해 바이트 스트림을 JSON 데이터로 읽고(read),

ObjectMapper는 읽은 JSON 데이터를 Java 객체로 역직렬화한다. 

 

스프링부트에서 사용하는 Jackson 라이브러리의 직렬화/역직렬화는 위의 내용대로 진행된다.


지금까지는 ObjectMapper를 사용하기 위해 알아야 하는 내용들을 적어보았다. 후우~ 

이제 프로젝트에 맞게 커스텀해보고자 한다. 

믈론 스프링부트에서 default된 걸 사용해도 상관은 없다. 

 

ObjectMapper를 내 입맛대로 설정해보자 (config)

기본적으로 스프링부트는 실행이 될 때 ObjectMapper가 설정되어 있는지 확인을 하고 없다면 default 설정으로 ObjectMapper를 만들어준다.

 

하지만 프로젝트의 상황에 따라 언제든지 ObjectMapper의 설정을 바꿔줘야 하는 경우가 발생할 수 있다.

따라서 몇 가지 상황을 가정하고 직접 설정을 해보자.

커스터마이징할 상황

우선, ObjectMapper를 직접 설정하기 위해 임의의 몇 가지 상황을 가정해보자.

  • 날짜(ex. Local Date) 혹은 자바 8 버전 이후에 나온 것(ex, Optional)들을 처리하다보면 에러가 발생할 수 있다. 이러한 문제를 없애고자 한다.
  • 특정 회사와 JSON 통신할 때는 camelCase인데, 내부적으로 snake_case를 쓰고 있다. (ObjectMapper에서는 default로 camelCase를 사용한다. 매번 모든 DTO에 JsonNaming으로 snake_case를 설정하는 건 매우 번거롭다.이를 공통화 처리하고자 한다.

 

ObjectMapper를 우선 @Bean으로 직접 등록

그러면 ObjectMapper를 커스터마이징하기 위해 config 하위에 클래스를 만들고 ObjectMapper를 @Bean으로 등록한다.

@Configuration
public class ObjectMapperConfig {

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
			
			...
    }
}

 

 

registerModule (JDK8Module & JavaTimeModule)

registerModule 메서드는 ObjectMapper에 특정 모듈을 등록하기 위해 사용된다.

objectMapper.registerModule(new Jdk8Module());
objectMapper.registerModule(new JavaTimeModule());

 

JDK8Module을 등록하면 JDK 8에서 추가된 클래스(ex. Optional 등)들을 JSON 직렬화 및 역직렬화에 사용할 수 있다.

따라서 Java 8 또는 그 이후 버전에서 사용하는 것이 가능하며, Java 8의 특징들을 효과적으로 JSON 처리에 활용할 수 있게 해준다.

 

JavaTimeModule을 등록하면 Java 8의 날짜 및 시간 API, 즉 java.time 패키지에 속하는 클래스들을 JSON 직렬화 및 역직렬화에 사용할 수 있다.

JavaTimeModule에는

LocalDateTime, LocalDate, LocalTime, ZonedDateTime, OffsetDateTime 등이 포함되어 있다.

 

ObjectMapper를 커스터마이징할 경우에 이렇게 registerModule로 특정 모듈을 명시적으로 등록하지 않는다면 사용할 수 없다고 한다. 그렇기에 명시적으로 작성했다. 

 

다만, 커스터마이징하지 않은 경우에는 즉, 스프링부트의 기본 설정으로 ObjectMapper를 쓴다면 위의 모듈들은 기본적으로 등록되어있다고 한다. 

 

ObjectMapper.configure

objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);

 

 

FAIL_ON_UNKNOWN_PROPERTIESfalse로 설정하면, 알려지지 않은 JSON 필드가 있어도 오류를 발생시키지 않고 무시한다. 

 

FAIL_ON_EMPTY_BEANS를 false로 설정하면, 속성이 없는 빈 객체에 대해 오류를 발생시키지 않는다.

 

이렇게 설정하면 보다 유연하게 JSON 처리가 가능하지만, 예상치 못한 데이터가 포함되어 있을 때 이를 무시하기에 예상치 못한 데이터 문제가 발생할 가능성이 생길 수 있다.

 

따라서 JSON 데이터를 개선하고 불완전한 JSON 데이터를 받을 수 있는 개발 초기 단계에서는 유용하지만, 데이터 무결성과 보안이 중요한 프로덕션 환경에서는 데이터의 안정성을 보장하기 위해 true가 적합하다.

 

ObjectMapper 추가 설정(disable, setPropertyNamingStrategy)

objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.setPropertyNamingStrategy(new PropertyNamingStrategies.SnakeCaseStrategy());

 

objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)를 설정하면

날짜를 타임스탬프로 쓰는 직렬화 기능을 비활성화한다.

기본적으로 Jackson은 날짜를 타임스탬프로 직렬화한다.

이 기능을 비활성화하면 날짜가 ISO-8601형식(예: "2024-03-21T01:30:00Z")으로 직렬화된다.

 

objectMapper.setPropertyNamingStrategy(new PropertyNamingStrategies.SnakeCaseStrategy())를 설정하면

JSON 속성 이름에 대해 snake_case를 사용하도록 설정한다.

즉, 객체를 JSON으로 직렬화할 때 속성 이름을 자바의 camelCase에서 snake_case로 변환해준다.

 

마무리

이렇게 ObjectMapper를 상황에 맞게 커스터마이징을 해보았다.

 

앞서 말했지만,

스프링부트에서는 ObjectMapper에 대한 @Bean이 정의가 되어 있지 않다면, 자동으로 Default로 하나를 만들고,

커스터마이징한 경우에는 즉, @Bean으로 정의한 경우에는 해당 설정을 바탕으로 ObjectMapper을 만들어서 등록한다. 

 

 


참고자료

https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-methods/jackson.html

 

Jackson JSON :: Spring Framework

Spring offers support for the Jackson JSON library.

docs.spring.io

 

https://umanking.github.io/2021/07/24/jackson-localdatetime-serialization/

 

Jackson, LocalDateTime Serialization, Deserialization 이슈

스프링에서 자바 객체를 직렬화/역직렬화를 할때, 내부적으로 Jackson을 사용하는데, 자바8에 도입된 LocalDateTime 타입으로 직렬화, 역직렬화할때 이슈를 정리해보았다.

umanking.github.io