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을 올려봤으나 스펙이 그러한 것이어서 아쉬웠다. 그래도 해결 방법을 찾았으니 다행이다.
'공부 > Spring' 카테고리의 다른 글
JPA data.sql로 초기화 시 에러 해결방법 Error creating bean with name 'dataSourceScriptDatabaseInitializer' (1) | 2021.08.29 |
---|---|
Spring @Order 로 Bean 순서 정의하기 (4) | 2021.02.15 |
Spring @Profile 로 환경에 따라 다르게 설정하기 (+ @ActiveProfiles 로 테스트 하기) (0) | 2021.01.08 |
Spring boot test 경로가 다른 패키지 테스트 시 오류 (6) | 2021.01.07 |
Spring Data Rest 알아보기 (0) | 2020.11.20 |