본문 바로가기
Java & 스프링

Spring의 Jackson은 어떻게 동작할까? with ObjectMapper

by hjhello423 2020. 12. 27.

Spring에서 api를 개발하다 보면 @RestController를 많이 사용하게 되는데요.
이때 reqeust와 response에서 'json -> 객체', '객체 -> json'의 과정을 처리해주는 녀석이 바로 MessageConverter입니다.

Spring에서 MessageConverter가 json 변환 처리 과정에서 사용하는 것이 바로 Jackson입니다.
json을 serialize, deserialze 하는 과정이 무엇을 기준으로 이루어지는지 정리해보겠습니다.
아래 과정은 Jackson의 ObjectMapper를 이용해보았습니다.

테스트에는 아래의 Animal class를 사용해보도록 하겠습니다.

@ToString
@EqualsAndHashCode
public class Animal {

    public Animal(String name, int weight, Integer age) {
        this.name = name;
        this.weight = weight;
        this.age = age;
    }

    private String name;
    private int weight;
    private Integer age;

}

 


기본 생성자를 이용한다

먼저 아래와 같이 간단한 test 코드를 작성해 보았습니다.

public class ObjectMapperTest {

    private static ObjectMapper om;
    private static final String JSON_STR = "{\"name\":\"tiger\",\"weight\":30,\"age\":2}";
    private static final Animal tiger = new Animal("tiger", 30, 2);

    @BeforeAll
    public static void init() {
        om = new ObjectMapper();
    }

    @Test
    public void 생성자이용() throws Exception {
        //when
        Animal animal = om.readValue(JSON_STR, Animal.class);

        //then
        Assertions.assertTrue(animal.equals(tiger));
    }
}

위 코드를 실행하면 어떤 결과가 나올까요?

 

 

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.github.hjdeepsleep.toy.domain.study.Animal` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (String)"{"name":"tiger","weight":30,"age":2}"; line: 1, column: 2]

기본 생성자가 없어서 에러가 발생되네요.
Json으로 변환할 때는 기본 생성자가 필요합니다. 그런데 이 기본 생성자는 private으로 선언되어 있어도 됩니다.

@NoArgsConstructor(access = AccessLevel.PRIVATE)
@EqualsAndHashCode
public class Animal {

    public Animal(String name, int weight, Integer age) {
        this.name = name;
        this.weight = weight;
        this.age = age;
    }

    private String name;
    private int weight;
    private Integer age;

}

 


property를 이용한다

자 그럼 기본 생성자를 생성해 주었으니 테스트 코드가 성공할까요?
결론부터 말하자면 테스트 코드는 아래와 같이 또다시 실패합니다.

 

 

com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "name" (class com.github.hjdeepsleep.toy.domain.study.Animal), not marked as ignorable (0 known properties: ])
 at [Source: (String)"{"name":"tiger","weight":30,"age":2}"; line: 1, column: 10] (through reference chain: com.github.hjdeepsleep.toy.domain.study.Animal["name"])

UnrecognizedPropertyException이 발생했는데요. ObjectMapper가 serialized, deserialized 과정에서 기본적으로 getter/setter를 이용하기 때분입니다.

자 그럼 @Getter, @Setter를 이용해 테스트를 성공시켜 보겠습니다.

@Test
public void 생성자이용() throws Exception {
	//when
	Animal animal = om.readValue(JSON_STR, Animal.class);

	//then
	Assertions.assertTrue(animal.equals(tiger));
}

 

 

드디어 변환에 성공했습니다!
한 가지 팁을 드리자면 변환 과정에서 getter와 setter 중 1개만 선언되어 있어도 변환이 가능하다는 점입니다.

 


property 이용으로 인한 문제

위에서 getter/setter를 이용한다는 점을 알아보았는데요.
그럼 아래와 같이 field에 선언되지 않은 getter/setter가 있다면 어떻게 될까요?

@Getter
@Setter
@ToString
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@EqualsAndHashCode
public class Animal {

    public Animal(String name, int weight, Integer age) {
        this.name = name;
        this.weight = weight;
        this.age = age;
    }

    private String name;
    private int weight;
    private Integer age;

    public String getColor() {
        return "red";
    }

    public String setColor() {
        return "blue";
    }

}

getColor(), setColor()를 추가한 뒤 아래의 테스트 코드를 실행해 보겠습니다.

@Test
public void 출력() throws Exception {
	//when
	String result = om.writeValueAsString(tiger);

	//then
	Assertions.assertTrue(result.equals(JSON_STR));
}

 

 

결과는 보이는 것처럼 실패합니다.
ObjectMapper가 변환 과정에서 get*,set*으로 시작하는 모든 메서드를 참조해 변환을 시도하기 때문입니다.
(set으로 반환한 blue가 아니라 get으로 반환한 red가 출력되는 것도 확인할 수 있습니다. )

 


변환할 필드가 없을 때

이번엔 json -> 객체로 변환 과정에서 class에 없는 필드 값(height)을 넣어 보겠습니다.

@Test
public void 객체로_변환() throws Exception {
	//given
	String str = "{\"name\":\"tiger\",\"weight\":30,\"age\":2,\"height\":50}";
    
	//when
	Animal result = om.readValue(str, Animal.class);

	//then
	Assertions.assertFalse(result.equals(tiger));
}
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "height" (class com.github.hjdeepsleep.toy.domain.study.Animal), not marked as ignorable (3 known properties: "weight", "name", "age"])
 at [Source: (String)"{"name":"tiger","weight":30,"age":2,"height":50}"; line: 1, column: 48] (through reference chain: com.github.hjdeepsleep.toy.domain.study.Animal["height"])

 

 

이번에는 UnrecognizedPropertyException이 발생하면서 객체로 변환하는데 실패하였습니다.
json에 있는 weight가 Animal calss에는 정의되지 않아서 생긴 문제입니다.
이 문제를 해결하려면 아래와 같이 간단한 설정만 바꿔주면 됩니다.

@Test
public void 객체로_변환2() throws Exception {
    //given
    ObjectMapper om2 = new ObjectMapper();
    om2.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    String str = "{\"name\":\"tiger\",\"weight\":30,\"age\":2,\"height\":50}";

    //when
    Animal result = om2.readValue(str, Animal.class);

    //then
    Assertions.assertTrue(result.equals(tiger));
}

이렇게 DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES을 false로 설정해 주면 deserialize 과정에서 알 수 없는 key를 만나도 무시하게 됩니다.

또 다른 한 가지 방법은 Animal class에 @JsonIgnoreProperties(ignoreUnknown = true)를 선언해 주는 방법입니다.
이 방법 또한 알 수 없는 key를 무시하도록 객체에 설정합니다.


property 이용하지 않고 deserialize/serialize 하기

getter/setter를 마구잡이로 선언하면 유지보수성이 굉장히 낮아지겠죠?
하지만 ObjectMapper를 이용해 변환 처리를 해야 한다면 어떻게 해야 할까요?

Animal class에 @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)를 선언해 주는 방법과 아래와 같이 ObjectMapper를 설정해 주는 방법이 있습니다.

@Test
public void 필드이용() throws Exception {
    //given
    ObjectMapper om2 = new ObjectMapper();
    om2.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);

    //when
    Animal result = om2.readValue(JSON_STR, Animal.class);

    //then
    Assertions.assertTrue(result.equals(tiger));
}

 

serialize 과정도 같은 설정을 사용합니다.

public void 필드이용하여_출력() throws Exception {
    //given
    ObjectMapper om2 = new ObjectMapper();
    om2.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
    om2.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);

    //when
    String result = om2.writeValueAsString(tiger);

    //then
    System.out.println(result);
    System.out.println(tiger);
    Assertions.assertTrue(result.equals(JSON_STR));
}

tiger 객체를 json으로 변환하는 과정에서 getColor()를 무시한 것을 확인할 수 있습니다.

만약 Animal에 해당 설정을 하고 싶다면 아래와 같이 선언하면 됩니다.

@JsonAutoDetect(
	fieldVisibility = JsonAutoDetect.Visibility.ANY,
    getterVisibility = JsonAutoDetect.Visibility.NONE, 
	setterVisibility = JsonAutoDetect.Visibility.NONE)

 


ObjectMapper에 하는 설정은 모두 class에 annotation을 이용하여 동일하게 설정이 가능합니다.
만약 ObjectMapper에 설정한다면 프로젝트에서 공통으로 사용 가능할 것이고, class에 설정하면 특정 class에만 다르게 설정이 가능합니다.

각 설정법을 장단점이 있으니 필요에 맞게 사용하면 됩니다!

반응형

댓글