공부/Spring

jackson으로 응답 값 직렬화할 때 속성값의 첫 단어가 한 글자인 경우

2021. 6. 28. 00:51

json형식의 응답으로 dDay라는 키를 반환하다가 문제가 생겨서 이 글을 작성하게 되었다.

(아래 코드는 Github에서 확인이 가능하다.)

 

먼저 반환하려는 객체는 다음과 같다.

public class FooDto {
    private int dDays;

    public int getDDays() {
        return dDays;
    }
}

기대하는 응답 값은 다음과 같다.

{
    "dDays": 0
}

 

하지만 실제로 응답된 값은 이러하다.

{
    "ddays": 0
}

 

이를 기반으로 테스트 코드를 작성해 보았다.

@WebMvcTest
public class FooControllerTest {

    @Autowired
    private MockMvc mvc;

    @Test
    public void jsonSerializeTest() throws Exception {
        mvc
                .perform(get("/")
                    .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(content().json("{\"dDays\":0}"));
    }
}

 

원인을 찾기 위해 디버깅을 해보았고, object -> json으로 변환해주는 jackson 라이브러리의 코드를 확인하였다.

com.fasterxml.jackson.databind.introspect.DefaultAccessorNamingStrategy 이 클래스의 아래 메서드에서 프로퍼티 명을 만들어 준다.

해당 메서드를 보면 먼저 basename이 getter에서 가져온 getDDays가 되고 offet이 3이 되어 먼저 get을 제거하고 DDays를 가지고 프로퍼티명을 만든다.

155라인에서 첫 글자가 소문자인지 검사하여 소문자일 경우 그 값 그대로 반환하는데 소문자가 아니다.

160라인에 첫 글자를 소문자로 변경하고 넣는다. (첫 번째 D가 d로 들어간다)

162라인부터 두 번째 글자부터 돌면서 검사를 하는데

165라인에서 보면 소문자일 경우 그 값들을 모두 넣는다. 그러나 두 번째 D는 소문자가 아니다.

그래서 169라인에서 두 번째 문자의 소문자인 d를 넣는다.

그리고 다음 반복문에서 a는 소문자이기 때문에 그 뒤로 모두 넣는다.

최종적으로 ddays가 반환이 된다.

해당 코드가 버그인 줄 알고 깃헙에 pr을 올렸으나, 짧은 영어실력 탓인지 DDays로 반환되는 것이 맞다는 답변을 받았다.

 

그래서 우리가 원하는 값 dDay가 나오게 하려면 어떻게 해야 하는가.

두 가지 방법이 존재한다.

 첫 번째. @JsonProperty 어노테이션을 사용한다.

public class FooDto {
    @JsonProperty("dDays")
    private int dDays;

    public int getDDays() {
        return dDays;
    }
}

이렇게 하면 해당 어노테이션에 있는 값으로 반환된다.

간단하게 해결이 가능한 장점이 있다.

하지만 단점으로는 모든 첫 단어가 한 글자이고 여러 단어로 된 속성에 저 어노테이션을 붙여줘야 한다.

 

그래서 두 번째 방법으로는 네이밍 정하는 클래스를 재 정의하는 것이다.

package com.example.jacksonbindtest;

import com.fasterxml.jackson.databind.PropertyNamingStrategies;

public class CustomNamingStrategy extends PropertyNamingStrategies.NamingBase {
    @Override
    public String translate(String input) {
        if (input == null || input.isEmpty()){
            return input;
        }
        char c = input.charAt(0);
        char lc = Character.toLowerCase(c);
        if (c == lc) {
            return input;
        }
        StringBuilder sb = new StringBuilder(input);
        sb.setCharAt(0, lc);
        return sb.toString();
    }

먼저 PropertyNamingStrategies.NamingBase를 상속받아 첫 글자를 소문자로 바꾸는 로직을 구현한다.

그리고 application.yml에 다음과 같이 작성한다.

spring:
  jackson:
    property-naming-strategy: com.example.jacksonbindtest.CustomNamingStrategy
    mapper:
      use-std-bean-naming: true

속성 이름 전략을 방금 만든 커스텀 클래스로 설정한다.

그리고 use-std-bean-naming 값을 true로 한다.

(false로 하게 되면 translate 메서드의 파라미터가 ddays로 나오게 된다.)

이렇게 하고 실행하면 translate 메서드의 파라미터가 DDays가 전달되고 첫 글자만 소문자로 변경되어 반환한다.

 

테스트도 성공하게 된다.

 

 

회사에서 개발하다가 발견하게 되어 소스의 문제인 줄 알고 잭슨 라이브러리에 기여할 수 있겠다는 생각에 얼른 찾아서 pr을 올려봤으나 스펙이 그러한 것이어서 아쉬웠다. 그래도 해결 방법을 찾았으니 다행이다.

 

 

 

 

반응형