ObjectMapper 불변 객체 직렬화/역직렬화

비딴·2023년 12월 17일
2
post-thumbnail

ObjectMapper를 사용하면서 이해 없이 사용한다는 생각이 들어 공부하고 정리한 내용입니다.

사전 지식

직렬화

객체를 데이터의 형태로 변환하는 것

역직렬화

데이터를 원래 객체의 형태로 변환하는 것

알고자 하는 것

  • ObjectMapper를 통해 직렬화/역직렬화가 어떤 방식으로 진행되는가
  • 불변 객체를 직렬화/역직렬화 하기 위해서는 어떻게 작성하여야 하는가

공식 문서

jackson-databind 깃허브 링크

ObjectMapper를 사용하기 위한 외부 의존성 주입

ObjectMapper를 사용하기 위해서는 com.fasterxml.jackson.core:jackson-databind:2.15.3 외부 패키지를 주입받아야 합니다.
하지만 대부분 직접 주입 설정을 해주지 않아도 사용할 수 있습니다. 많은 라이브러리들이 의존성 설정으로 databind를 주입받고 있습니다.
제 프로젝트에서는 org.springframework.boot:spring-boot-starter-web 라이브러리가 databind를 주입하고 있었습니다.

+--- org.springframework.boot:spring-boot-starter-web -> 3.1.5
|    +--- org.springframework.boot:spring-boot-starter-json:3.1.5
|    |    +--- com.fasterxml.jackson.core:jackson-databind:2.15.3

intellij -> gradle -> task -> help -> dependencies 동작을 통해 패키지 의존성을 트리 구조로 확인할 수 있습니다.

기본적인 직렬화/역직렬화

jackson-databind 공식문서에서는 기본적인 직렬화시 Getter가 필요하고, 역직렬화시 NoArgsConstructorSetter가 필요합니다.

@ToString
@NoArgsConstructor
@Setter
@Getter
public class Student {

    private String name;
}
class ObjectMapperTest {

    private ObjectMapper objectMapper;

    @BeforeEach
    void setup() {
        objectMapper = new ObjectMapper();
    }

    @DisplayName("객체를 직렬화한다.")
    @Test
    void serialize() throws JsonProcessingException {
        Student student = new Student();
        student.setName("biddan");

        String writtenValueAsString = objectMapper.writeValueAsString(student);
        System.out.println(writtenValueAsString);

        Student deserializedStudent = objectMapper.readValue(writtenValueAsString, Student.class);
        System.out.println(deserializedStudent);
    }
}

기본적인 직렬화/역직렬화

Getter, NoArgsConstructor, Setter가 있을 때 직렬화/역직렬화를 잘 수행하는 것을 볼 수 있습니다.

불변으로 만들 수 있을까?

NoArgsConstructor, Setter는 불변을 보장하지 않기 때문에 다른 방법으로 작성할 수 있을지 조금 더 알아보겠습니다.

@ToString
@Getter
public class Student {

    private final String name;

    public Student(String name) {
        this.name = name;
    }
}
class ObjectMapperTest {

    private ObjectMapper objectMapper;

    @BeforeEach
    void setup() {
        objectMapper = new ObjectMapper();
    }

    @DisplayName("객체를 직렬화한다.")
    @Test
    void serialize() throws JsonProcessingException {
        Student student = new Student("bidddan");

        String writtenValueAsString = objectMapper.writeValueAsString(student);
        System.out.println(writtenValueAsString);

        Student deserializedStudent = objectMapper.readValue(writtenValueAsString, Student.class);
        System.out.println(deserializedStudent);
    }
}

NoArgsConstructor, Setter를 제거하고 테스트 코드를 수정하여 돌려보면 실패합니다.

역직렬화 실패

Cannot construct instance of com.example.objectmappertest.Student (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (String)"{"name":"biddan"}"; line: 1, column: 2]
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of com.example.objectmappertest.Student (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (String)"{"name":"biddan"}"; line: 1, column: 2]

실패 지점 전까지 디버깅을 진행하여 역직렬화를 어떻게 하는지 알아보고 불변 객체를 보장하게 코드로 변경해보겠습니다.

역직렬화 과정

에러 지점

위 지점에서 예외가 발생하였고, 인자는 잘 받아오는 것을 확인할 수 있습니다.
_readMapAndClose 메소드에서 데이터를 객체로 변환하다가 예외가 발생하는 것 같습니다.
더 들어가보겠습니다.

역직렬화 초기 설정과 변환

초기 설정과 데이터를 객체로 변환하는 부분으로 나누어져 있습니다.
초기 설정은 하지 않았으므로, 데이터를 객체로 변환하는 부분으로 넘어가겠습니다.
데이터를 객체로 변환하는 부분은 ctxt.readRootValue 메소드입니다.

역직렬화 방법 선택

역직렬화할 문자열

역직렬화할 방법을 선택하는 메소드입니다.
클래스의 이름이 포함된 JSON일 경우 설정을 통해 _unwrapAndDeserialize()로 역직렬화를 선택할 수 있는 것 같습니다.
아무 설정을 하지 않았으므로 deser.deserialize() 통해 역직렬화를 진행합니다.

NoArgsConstructor 설정을 통한 주입

NoArgsConstructor를 설정해주었을 때는 vanillaDeserialize()에서 setter를 통해 넣어주거나 setter가 없을 경우 리플렉션을 통해 직접 넣어줍니다.
저는 NoArgsConstructor를 사용하지 않을 예정이므로 자세한 설명을 생략하겠습니다.

여기서 예외가 발생합니다. 역직렬화 방식이 정의되어 있지 않아 발생하는 문제입니다.
_valueInstantiator.createUsingDelegate(), _deserializeUsingPropertyBased() 두 방식 중 하나를 할 수 있어야 합니다.
_valueInstantiator.createUsingDelegate()는 커스텀한 변환 방식을 등록해놓아야 하기 때문에 복잡합니다.
간단한 _deserializeUsingPropertyBased()(속성 기반 생성자)를 사용하여 변환해보도록 하겠습니다.

사용자 지정 생성자 리드미

공식 문서 README에도 속성 기반 생성자를 사용하는 방법이 잘 설명되어 있습니다.

@ToString
@Getter
public class Student {

    private final String name;

    public Student(@JsonProperty("name") String name) {
        this.name = name;
    }
}

속성 기반 생성자 테스트

생성자에 JsonProperty 어노테이션을 추가하여 실패했던 직렬화/역직렬화 테스트를 다시 돌려보면 성공합니다.

레코드도 가능할까?

레코드는 자바 14부터 DTO를 간단한 불변 객체로 사용할 수 있게 도입된 기능입니다.
Student 클래스를 레코드를 변경하여 테스트를 돌려보겠습니다.

public record Student(
        String name
) {

}

레크드 테스트

성공하는 것을 볼 수 있습니다.

레코드는 JsonProperty을 사용하지 않아도 될까?

역직렬화를 할 때, PropertyBasedCreator 생성시에 매핑 필드들을 가지고 있는 _propertyLookup를 설정합니다.
레코드에 어노테이션을 달지 않아도 추가되는 것을 볼 수 있습니다.

[ObjectMapper 2.12 버전] 레코드에 @JsonProperty 생략 지원 글

2.12 버전부터 레코드에 @JsonProperty를 생략하는 것을 지원한다는 내용을 글을 통해서도 확인할 수 있습니다.

약간의 문제

레코드 지원은 최근에 이루어졌습니다.(2.12 버전 기준)
간혹 역직렬화에 실패하는 문제들이 있어 이러한 문제들이 일어날 수 있음을 인지하고, 테스트를 통해 확인해보는 것이 좋을 것 같습니다.

자바 16 레코드 역직렬화 실패 케이스
jackson 2.12 JDK 15 레코드에서 JsonNaming이 작동하지 않는 문제

profile
비 온 뒤 딴딴

0개의 댓글