ObjectMapper
를 사용하면서 이해 없이 사용한다는 생각이 들어 공부하고 정리한 내용입니다.
객체를 데이터의 형태로 변환하는 것
데이터를 원래 객체의 형태로 변환하는 것
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
가 필요하고, 역직렬화시 NoArgsConstructor
와 Setter
가 필요합니다.
@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 ofcom.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
를 설정해주었을 때는 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
) {
}
성공하는 것을 볼 수 있습니다.
역직렬화를 할 때, PropertyBasedCreator
생성시에 매핑 필드들을 가지고 있는 _propertyLookup
를 설정합니다.
레코드에 어노테이션을 달지 않아도 추가되는 것을 볼 수 있습니다.
[ObjectMapper 2.12 버전] 레코드에 @JsonProperty 생략 지원 글
2.12 버전부터 레코드에 @JsonProperty
를 생략하는 것을 지원한다는 내용을 글을 통해서도 확인할 수 있습니다.
레코드 지원은 최근에 이루어졌습니다.(2.12 버전 기준)
간혹 역직렬화에 실패하는 문제들이 있어 이러한 문제들이 일어날 수 있음을 인지하고, 테스트를 통해 확인해보는 것이 좋을 것 같습니다.
자바 16 레코드 역직렬화 실패 케이스
jackson 2.12 JDK 15 레코드에서 JsonNaming이 작동하지 않는 문제