업데이트:

이번 포스트에선 저번 @RequestBody 매핑 포스트에서 언급했던 ObjectMapper 변환에 대한 내용을 다뤄보겠습니다.

ObjectMapper란?


JSON 컨텐츠를 Java Object로 역직렬화하거나 Java ObjectJSON으로 직렬화할 때 사용하는 Jackson 라이브러리안에 클래스입니다.

보통 Spring 프로젝트(특히 Web)를 진행하시는 분들이 많이들 접하시게 될텐데요.

다음과 같이 spring-boot-starter-web를 추가하게 되면

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
}

아래 캡처와 같이 jackson-databind가 있는 것을 확인할 수 있습니다.

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_BEANSdisable 처리 해보겠습니다.

private ObjectMapper getObjectMapper() {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
    return objectMapper;
}

ObjectMapperSerializationFeature.FAIL_ON_EMPTY_BEANSfalse로 처리했습니다.

💡SerializationFeature.FAIL_ON_EMPTY_BEANSjavadoc에 나와있는걸 보면

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으로 변환된 내용을 보면

empty_json

orderName, amount 값이 바인딩이 안된 것을 확인할 수 있습니다.

public으로 멤버변수 전환

이번엔 Order의 멤버 변수의 접근제어자를 privatepublic으로 바꿔서 테스트 해보겠습니다.

public class Order {

    public String orderName;
    public long amount;

    public Order(String orderName, long amount) {
        this.orderName = orderName;
        this.amount = amount;
    }
}

json1

정상적으로 변환이 됐습니다!

하지만 이는 캡슐화에 위배되기 때문에 좋은 해결방안이 아니라고 생각합니다.

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;
    }
}

json2

이번에도 정상적으로 Json으로 변환되고 값도 바인딩 된것을 확인 할 수 있습니다

결론

Object → Json으로 직렬화하기 위해선 Getter를 통해 멤버 변수에 접근할 수 있도록 해주는 것이 가장 좋은 방법이라고 할 수 있겠습니다.

역직렬화 (Json → Java Object)


이번엔 반대로 Json → Java Object로 역직렬화 하는 법을 보겠습니다.

Order는 위에서 테스트한 그대로를 사용했고, 테스트 코드는 ObjectMapperreadValue()를 사용했습니다.

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;
    }
}

java_object1

테스트, 역직렬화가 성공한 것을 확인할 수 있습니다.

의문점

Setter가 없이 값의 바인딩이 되는 것이 orderName, amount를 받는 생성자가 있기 때문에 가능한 것이라 생각하여 단순 궁금함에 파라미터를 받는 생성자를 제거하고 테스트를 해봤습니다.

public class Order {

    private String orderName;
    private long amount;

    public Order() {
    }

    public String getOrderName() {
        return orderName;
    }

    public long getAmount() {
        return amount;
    }
}

java_object2

❓❓❓

Setter도 없고, 파라미터를 받는 생성자도 없는데 어떻게 바인딩이 되는걸까요??

한번 ObjectMapperreadValue 메소드를 따라 들어가봤습니다.

결론부터 말씀드리면 필드의 접근 제어를 ReflectsetAccessible메소드를 사용하여 접근가능하도록 하고, 필드에 값을 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개입니다.

  1. 기본 생성자가 필요하다.
  2. *Getter를 통해 멤버 변수에 접근할 수 있도록 한다.*

특히 Spring에서 MessageConverter에서 ObjectMapper가 사용되기 때문에 이 내용을 아시면 도움이 많이 되실겁니다.

(더이상 setter가 있어야 변환이 된다는 말을 그만)

오늘도 긴 제 글을 봐주셔서 감사합니다. 😄

📌참고


[jackson, issue] 이슈 해결 - No serializer found for class

Jackson ObjectMapper 정리

댓글남기기