Jackson - ObjectMapper에 대해
업데이트:
이번 포스트에선 저번 @RequestBody
매핑 포스트에서 언급했던 ObjectMapper
변환에 대한 내용을 다뤄보겠습니다.
ObjectMapper란?
JSON 컨텐츠를 Java Object로 역직렬화하거나 Java Object를 JSON으로 직렬화할 때 사용하는 Jackson 라이브러리안에 클래스입니다.
보통 Spring 프로젝트(특히 Web)를 진행하시는 분들이 많이들 접하시게 될텐데요.
다음과 같이 spring-boot-starter-web
를 추가하게 되면
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
}
아래 캡처와 같이 jackson-databind
가 있는 것을 확인할 수 있습니다.
이 jackson-databind
안에 ObjectMapper
가 존재합니다.
spring-boot-starter-web
을 사용하지 않는다면 다음과 같이 Jackson 라이브러리를 직접 추가해주면 됩니다.
dependencies {
implementation 'com.fasterxml.jackson.core:jackson-databind:{version}',
}
직렬화 (Java Object→ Json)
우선 상대적으로 간단한 Java Object → Json으로 직렬화하는 것부터 알아보겠습니다.
테스트
public class Order {
private String orderName;
private long amount;
public Order(String orderName, long amount) {
this.orderName = orderName;
this.amount = amount;
}
}
단순한 Order
라는 객체로 orderName
, amount
를 멤버 필드로 가지고 있고, 멤버 필드를 파라미터로 받는 생성자가 있습니다.
다음은 테스트 코드입니다.
@Test
void objectToJsonWithoutSetter() throws Exception {
/** Given **/
Order order = new Order("goodName", 1_000L);
/** When **/
String json = objectMapper.writeValueAsString(order);
/** Then **/
System.out.println("json = " + json);
}
위의 테스트 코드를 실행시켜보면 다음과 같은 에러를 보실 수 있습니다.
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
No serializer found for class com.citizen.board.domain.Order
and no properties discovered to create BeanSerializer
(to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
간단하게 말하면 Serializer(번역이 어렵네요. 직렬화기?)를 찾을 수 없다는 것인데, Order의 멤버 변수는 private
하게 되어있고 접근할 수 없기 때문입니다.
해결 방법
configure 세팅
해결 방법 첫 번째로 에러 메시지에 나왔듯이 SerializationFeature.FAIL_ON_EMPTY_BEANS
를 disable 처리 해보겠습니다.
private ObjectMapper getObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
return objectMapper;
}
ObjectMapper
에 SerializationFeature.FAIL_ON_EMPTY_BEANS
를 false
로 처리했습니다.
💡
SerializationFeature.FAIL_ON_EMPTY_BEANS
는 javadoc에 나와있는걸 보면
Feature that determines what happens when no accessors are found for a type (and there are no annotations to indicate it is meant to be serialized).
”접근자를 찾을 수 없고,직렬화되었음을 나타내는 주석이없을 때 수행되는 작업을 결정하는 기능입니다.” 라고 기재되어있습니다.
위에서 생성된 ObjectMapper
로 테스트 코드를 돌려보면 테스트는 성공되지만 json으로 변환된 내용을 보면
orderName
, amount
값이 바인딩이 안된 것을 확인할 수 있습니다.
public으로 멤버변수 전환
이번엔 Order
의 멤버 변수의 접근제어자를 private
→ public
으로 바꿔서 테스트 해보겠습니다.
public class Order {
public String orderName;
public long amount;
public Order(String orderName, long amount) {
this.orderName = orderName;
this.amount = amount;
}
}
정상적으로 변환이 됐습니다!
하지만 이는 캡슐화에 위배되기 때문에 좋은 해결방안이 아니라고 생각합니다.
Getter 추가
이번엔 필드에 접근하기 위해 Getter
를 추가해보도록 하겠습니다.
public class Order {
private String orderName;
private long amount;
public Order(String orderName, long amount) {
this.orderName = orderName;
this.amount = amount;
}
public String getOrderName() {
return orderName;
}
public long getAmount() {
return amount;
}
}
이번에도 정상적으로 Json으로 변환되고 값도 바인딩 된것을 확인 할 수 있습니다
결론
Object → Json으로 직렬화하기 위해선 Getter
를 통해 멤버 변수에 접근할 수 있도록 해주는 것이 가장 좋은 방법이라고 할 수 있겠습니다.
역직렬화 (Json → Java Object)
이번엔 반대로 Json → Java Object로 역직렬화 하는 법을 보겠습니다.
Order
는 위에서 테스트한 그대로를 사용했고, 테스트 코드는 ObjectMapper
의 readValue()
를 사용했습니다.
public class Order {
private String orderName;
private long amount;
public Order(String orderName, long amount) {
this.orderName = orderName;
this.amount = amount;
}
public String getOrderName() {
return orderName;
}
public long getAmount() {
return amount;
}
}
@Test
void jsonToObject() throws Exception {
/** Given **/
String json = "{\"orderName\":\"goodName\",\"amount\":1000}";
/** When **/
Order order = objectMapper.readValue(json, Order.class);
/** Then **/
System.out.println("order = " + order);
}
테스트 결과는 다음과 같은 에러 메시지가 발생했네요.
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of `com.citizen.board.domain.Order`
(no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (String)"{"orderName":"goodName","amount":1000}"; line: 1, column: 2]
no Creators, like default constructor
메시지에서 알 수 있듯이 기본 생성자가 없어서 발생했음을 알 수 있습니다.
해결방법
기본 생성자 추가
에러 메시지에서 알려준대로 기본 생성자를 추가하여 테스트를 진행해보겠습니다.
public class Order {
private String orderName;
private long amount;
/** 기본 생성자 추가 **/
public Order() {
}
public Order(String orderName, long amount) {
this.orderName = orderName;
this.amount = amount;
}
public String getOrderName() {
return orderName;
}
public long getAmount() {
return amount;
}
}
테스트, 역직렬화가 성공한 것을 확인할 수 있습니다.
의문점
Setter
가 없이 값의 바인딩이 되는 것이 orderName
, amount
를 받는 생성자가 있기 때문에 가능한 것이라 생각하여 단순 궁금함에 파라미터를 받는 생성자를 제거하고 테스트를 해봤습니다.
public class Order {
private String orderName;
private long amount;
public Order() {
}
public String getOrderName() {
return orderName;
}
public long getAmount() {
return amount;
}
}
❓❓❓
Setter
도 없고, 파라미터를 받는 생성자도 없는데 어떻게 바인딩이 되는걸까요??
한번 ObjectMapper
의 readValue
메소드를 따라 들어가봤습니다.
결론부터 말씀드리면 필드의 접근 제어를 Reflect의 setAccessible
메소드를 사용하여 접근가능하도록 하고, 필드에 값을 set
하는 방식으로 값이 바인딩되도록 하고 있었습니다.
해당 메소드는 각각 FieldProperty.fixAccess
> ClassUtil.*checkAndFixAccess
,* FieldProperty.deserializeAndSet
입니다.
FieldProperty.fixAccess
@Override
public void fixAccess(DeserializationConfig config) {
ClassUtil.checkAndFixAccess(_field,
config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS));
}
ClassUtil.*checkAndFixAccess*
public static void checkAndFixAccess(Member member, boolean force) {
// We know all members are also accessible objects...
AccessibleObject ao = (AccessibleObject) member;
/* 14-Jan-2009, tatu: It seems safe and potentially beneficial to
* always to make it accessible (latter because it will force
* skipping checks we have no use for...), so let's always call it.
*/
try {
if (force ||
(!Modifier.isPublic(member.getModifiers())
|| !Modifier.isPublic(member.getDeclaringClass().getModifiers()))) {
/** 필드 접근을 가능하도록 true로 세팅 **/
ao.setAccessible(true);
}
} catch (SecurityException se) {
// 17-Apr-2009, tatu: Related to [JACKSON-101]: this can fail on platforms like
// Google App Engine); so let's only fail if we really needed it...
if (!ao.isAccessible()) {
Class<?> declClass = member.getDeclaringClass();
throw new IllegalArgumentException("Cannot access "+member+" (from class "+declClass.getName()+"; failed to set access: "+se.getMessage());
}
}
}
FieldProperty.deserializeAndSet
@Override
public void deserializeAndSet(JsonParser p,
DeserializationContext ctxt, Object instance) throws IOException
{
Object value;
if (p.hasToken(JsonToken.VALUE_NULL)) {
if (_skipNulls) {
return;
}
value = _nullProvider.getNullValue(ctxt);
} else if (_valueTypeDeserializer == null) {
value = _valueDeserializer.deserialize(p, ctxt);
// 04-May-2018, tatu: [databind#2023] Coercion from String (mostly) can give null
if (value == null) {
if (_skipNulls) {
return;
}
value = _nullProvider.getNullValue(ctxt);
}
} else {
value = _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer);
}
try {
/** 필드에 값 세팅 **/
_field.set(instance, value);
} catch (Exception e) {
_throwAsIOE(p, e, value);
}
}
마무리
ObjectMapper
를 주로 사용하는 건 Dto
, VO
의 변환 시에 많이들 사용하실 텐데요.
중요한 포인트는 2개입니다.
- 기본 생성자가 필요하다.
*Getter
를 통해 멤버 변수에 접근할 수 있도록 한다.*
특히 Spring에서 MessageConverter
에서 ObjectMapper
가 사용되기 때문에 이 내용을 아시면 도움이 많이 되실겁니다.
(더이상 setter
가 있어야 변환이 된다는 말을 그만)
오늘도 긴 제 글을 봐주셔서 감사합니다. 😄
댓글남기기