[SpringBoot] Json Deserialize 하기

울상냥·2023년 5월 13일
1

SpringBoot

목록 보기
10/11

Velog API로 HTTP request를 보낸후 json응답을 객체로 받아와야 한다.

간단한 binding은 필드명만 맞게 작성한다면 문제가 없었지만..
받아오는 json이 중첩되어있었기 때문에 직접 binding를 해주어야했다.

(귀찮)

Response Json

UserTags에 대한 요청에 대한 응답의 구조는 이러하다.

{
    "data": {
        "userTags": {
            "tags": [
                {
                    "id": "dc268ec3-074c-4429-82ca-6683be81e9d3:80fb46c1-cfbd-11e8-b93f-579a7dec4e42",
                    "name": "python",
                    "posts_count": 26
                },
                {
                    "id": "dc268ec3-074c-4429-82ca-6683be81e9d3:7d4eb9d0-0410-11e9-a090-7da5cd66404e",
                    "name": "Springboot",
                    "posts_count": 23
                },
                
                (...)
                
                {
                    "id": "dc268ec3-074c-4429-82ca-6683be81e9d3:a3167ea0-eb33-11e8-b115-5df0fc60ff3a",
                    "name": "Nginx",
                    "posts_count": 1
                }
            ],
            "posts_count": 67
        }
    }
}               
  • depth 1 : data
  • depth 2 : userTags
  • depth 3 : tags(배열형태) , posts_count (totalPostsCount)
  • depth 4 : id, name, posts_count(태그별 posts_count)

로 중첩이 되어있다.


ResponseDto

응답을 mapping 시킬 클래스를 작성해주었다.

UserTags

@NoArgsConstructor
@Getter
public class UserTags{

    private int totalPostsCount;
    private List<Tags> tags;
}

Tags

@NoArgsConstructor
@Getter
public class Tags {

    private String id;
    private String name;
    @JsonProperty("posts_count")
    private int postsCount;
}

스프링부트는 spring-boot-starter-web에 Jackson 라이브러리를 제공하고 있고 Json의 직렬/역직렬화 기본적으로 Jackson을 사용하게 된다.
deserialize은

  1. 기본 생성자로 객체를 생성하고
  2. public 필드 또는 public의 getter/setter로 필드를 찾아 바인딩

하기 때문에 @NoArgsConstructor@Getter가 필요하다
문제는 depth 3 에 있는 posts_count와 tags를 꺼내와 바인딩 해주어야하기 때문에 직접 매핑을 해주어야 한다.

binding

직접 Custom deserializer을 작성하는 방법과 클래스 내에서 어노테이션을 사용하는 방법 등이있다.

Custom deserializer

public class UserTagsDeserializer extends JsonDeserializer<UserTags> {

    @Override
    public UserTags deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {

        ObjectMapper objectMapper = new ObjectMapper();

        JsonNode node = p.getCodec().readTree(p);
        JsonNode tagsNode = node.findValue("tags");

        int posts_count = node.get("userTags").get("posts_count").asInt();
        List<Tags> tags = Arrays.stream(objectMapper.treeToValue(tagsNode, Tags[].class)).toList();

        return new UserTags(posts_count, tags);
    }
}

JsonNode를 통해 매핑해 주었다.
요소에 접근하는 몇가지 방법이있는데 그중에서

  • get() - 노드의 필드를 찾고 없으면 null return
  • path() - 노드의 필드를 찾고 없으면 MissingNode return
  • findValue() - 노드와 자식노드들에서 필드를 찾고 없으면 null return

순차적인 접근을 위해서는 get() 또는 path()를 사용한다. findValue()는 노드 하위 전체에서 필드를 찾아주어 편하지만 동일한 필드명이 존재하는 경우 원치않는 필드를 가져올 수 있다.
posts_count 필드가 여러개 있었기 때문에 get()로 순차접근하여 원하는 필드를 가져왔다.
(findValue()를 사용했더니 다른값을 가져왔다,,,)

List값을 받기 위해서 objectMapper.treeToValue를 사용해 배열로 받아 list로 변환해 주었다.

UserTags

@AllArgsConstructor
@NoArgsConstructor
@Getter
@JsonDeserialize(using = UserTagsDeserializer.class)
public class UserTags{

    private final int totalPostsCount;
    private final List<Tags> tags;
}

UserTags에는 역직렬화시에 해당 deserializer을 사용하도록 @JsonDeserialize를 추가해주었다.

custom deserializer을 작성하였을때 역직렬화 코드를 별도 클래스로 작성하여 dto클래스가 깔끔해지고 재사용면에서 장점이 있을 것 같았다.
하지만 이번 프로젝트에서는 재사용할 일이없고,, request DTO마다 별도로 deserializer을 작성해주어야 해서 deserializer클래스만 불어나는 일이 발생했다.

annotation

해서 annotation을 사용해 주도록 변경하였다.

@Getter
public class UserTags{

    private final int totalPostsCount;
    private final List<Tags> tags;

    @JsonCreator
    public UserTags(@JsonProperty("data") JsonNode node) throws JsonProcessingException {

        ObjectMapper objectMapper = new ObjectMapper();

        JsonNode tagsNode = node.findValue("tags");

        this.totalPostsCount = node.get("userTags").get("posts_count").asInt();
        this.tags =  Arrays.stream(objectMapper.treeToValue(tagsNode, Tags[].class)).toList();
    }
}

@JsonCreator@JsonProperty를 사용한 방법이다.
@JsonCreator은 기본생성자 + setter 조합을 대체 하기때문에 @NoArgsConstructor가 필요없다. 객체를 생성하고 필드를 생성과 동시에 채워 setter없이 immutable한 객체를 얻을 수 있다는 장점이 있다.

@JsonProperty로 depth 1의 data를 가져와 주었다.

@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
public class CurrentUser {

    private final String username;
    private final String thumbnail;
    private final String displayName;

    @JsonCreator
    public CurrentUser(@JsonProperty("username") String username, @JsonProperty("profile") JsonNode profileNode) {

        this.username = username;
        this.thumbnail = profileNode.get("thumbnail").asText();
        this.displayName = profileNode.get("display_name").asText();
    }
}

또 다른 dto에서는 사용하지 않는 필드가 있어 @JsonIgnoreProperties을 추가해주었다.
추가해 주지않으면

com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field ...

과같은 오류가 발생
@JsonProperty를 통해서 다양한 타입으로 받아올 수 있어 바로 사용할 필드는 String으로 받아오고 중첩된 필드에 대해서는 JsonNode로 받아왔다..!


jackson 관련 어노테이션도 아주 많고,, 방법도 다양해서 좋은 방법을 찾느라 삽질을 좀 했는데
annotation을 사용해 deserializer 클래스들을 없애 주니 깔끔하고 좋은 것 같다

profile
안되면 되게하라

0개의 댓글