Velog API로 HTTP request를 보낸후 json응답을 객체로 받아와야 한다.
간단한 binding은 필드명만 맞게 작성한다면 문제가 없었지만..
받아오는 json이 중첩되어있었기 때문에 직접 binding를 해주어야했다.
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)
로 중첩이 되어있다.
응답을 mapping 시킬 클래스를 작성해주었다.
@NoArgsConstructor
@Getter
public class UserTags{
private int totalPostsCount;
private List<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은
- 기본 생성자로 객체를 생성하고
- public 필드 또는 public의 getter/setter로 필드를 찾아 바인딩
하기 때문에 @NoArgsConstructor
와 @Getter
가 필요하다
문제는 depth 3 에 있는 posts_count와 tags를 꺼내와 바인딩 해주어야하기 때문에 직접 매핑을 해주어야 한다.
직접 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로 변환해 주었다.
@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을 사용해 주도록 변경하였다.
@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 클래스들을 없애 주니 깔끔하고 좋은 것 같다