true를 보냈는데 왜 받지를 못하니?(feat. ObjectMapper의 Json 매핑)

Young B·2023년 12월 7일
0
post-thumbnail

📌 본 글에 나온 예시들은 실제 프로젝트에서 겪었던 일들을 테스트 프로젝트를 통해 재현한 것입니다.

문제 발생

1. OpenFeign 통신을 하며 생긴 문제

요청에선 true인데 왜 응답에는 false일까???

MSA 아키텍처의 프로젝트를 진행하며 OpenFeign을 통해 통신을 하고 있었다.
그러던 중 희한한 에러를 발견했다.
true로 와야할 값이 false로 오고 있던 것.

보내는 서비스의 로그

받는 서비스의 로그

분명 보내는 쪽은 true를 담아서 보내고 있는데, 받는 쪽은 false로 받고 있다...
매핑이 되지 않는 경우는 있었어도, 들어간 값이 잘못 전달되는 경우는 처음 겪는 일이었다.

원인 분석

2. 왜 false일까?

문제가 되는 DTO는 다음과 같았다.

ResponseDto

@Getter
public class GetTestResDto {

    private boolean isTest;

    public GetTestResDto(boolean isTest) {
        this.isTest = isTest;
    }
}

왜 여기서 isTest의 값이 false가 되는걸까?
조금 생각해보니 boolean의 기본값이 false라는 것이 떠올랐다.
즉, 값 자체가 들어가있지 않기 때문에 getter를 사용했을 때 false를 반환했던 것이다.

다음의 두 가지 가능성이 있었는데

  1. isTest에 false 값이 들어갔다.
  2. isTest에 값이 들어가지 않았다.

아무리 살펴봐도 통신 과정에 false가 들어가는 로직은 없으므로 1번일 가능성이 높아졌다.
그렇다면 왜 매핑이 되지 않는걸까?

3. 로그 확인

이를 확인하기 위해서 OpenFeign에 로그를 활성화할 필요가 있다.
OpenFeign에서 로그를 활성화하려면 다음 세 가지가 필요하다.

  1. application.yml 설정
  2. Configuration Bean 등록
  3. @FeignClient에 configuration 등록

application.yml

logging:
  level:
    {패키지명}: DEBUG

FeignLoggerConfig

@Configuration
public class FeignLoggerConfig {

    @Bean
    Logger.Level feignLoggerLevel(){
        return Logger.Level.FULL;
    }
}

TestClient

@FeignClient(name = "test", url = "http://localhost:8080/test",
configuration = FeignLoggerConfig.class)
public interface TestClient {


}

이렇게 설정을 하고 나면 다음과 같이 통신에 대한 로그를 확인할 수 있다.

확인해보니 json의 키값이 "isTest"가 아닌 "test"로 되어있는 것을 확인할 수 있었다.
왜 변수명이 "test"로 바뀐 것일까?

4. ObjectMapper의 매핑 방식

Spring 프로젝트에서 OpenFeign 라이브러리는
Jackson의 ObjectMapper를 통해 직렬화와 역직렬화를 진행한다.

ObjectMapper는 Visibility라는 속성의 값에 따라 자동으로 매핑할 변수들을 찾아낸다.
아래 이미지를 통해 ObjectMapper에서 기본으로 설정되어 있는 Visibility의 값들을 확인할 수 있다.

각 Visibility는 다음과 같은 방식으로 매핑을 진행한다.

1. getterVisibility

  • getter를 통해 변수명을 추론한다. 만약 "getName"이라는 함수가 있으면 name이라는 key값이 있다고 생각한다.
  • 기본 접근 권한은 PUBLIC_ONLY이다.

2. isGetterVisibility

  • is로 시작하는 getter를 통해 변수명을 추론한다. 만약 "isTest"라는 함수가 있으면 test라는 key값이 있다고 생각한다.
  • 기본 접근 권한은 PUBLIC_ONLY이다.

3. setterVisibility

  • setter를 통해 변수명을 추론한다. 만약 "setName"이라는 함수가 있으면 name이라는 key값이 있다고 생각한다.
  • getter와 달리 setterVisibility는 역직렬화에만 사용이 가능하다.
  • 기본 접근 권한은 ANY이다.

4. creatorVisibility

  • 주석을 통해 legacy 지원을 위해 남아있는 옵션임을 확인할 수 있다.
  • 기본 접근 권한은 ANY이다.

5. fieldVisibility

  • 필드값을 통해 변수명을 추론한다. 만약 "name"이라는 필드값이 있으면 name이라는 key값이 있다고 생각한다.
  • 기본 접근 권한은 PUBLIC_ONLY이다.

이제 왜 "isTest"가 "test"로 매핑되었는지 알 수 있다.
isGetterVisibility 옵션을 통해 test라는 key값을 추론해냈기 때문이다.
이 문제에 대해선 몇 가지 해결 방법이 있다.

문제 해결

좋지 않은 방법

1. Boolean Wrapper 클래스를 사용한다.

boolean 타입 대신 Boolean 타입을 사용하면 문제없이 매핑을 할 수 있다.
Lombok의 @Getter는 boolean 타입일 경우 prefix로 is를 붙이고, Boolean 타입일 경우 그대로 get을 붙인다.

boolean을 사용할 때(isTest()가 생긴다)

Boolean을 사용할 때(getIsTest()가 생긴다)

하지만 Boolean을 사용할 경우 Null값을 허용하기 때문에 NPE에 대한 추가적인 처리가 필요하다.
따라서 boolean을 사용하는 것보다 좋은 방법이 될 수 없다.

2. Setter를 추가한다.

응답을 받는 서비스의 DTO에 setter를 추가한다.
이렇게 하면 setter를 통해 test라는 키값을 매핑시킬 수 있다.
하지만 setter를 사용하면 불변성을 깨뜨리고 객체의 캡슐화를 약화시키므로 역시 좋은 방법이 될 수 없다.

3. FieldVisibility를 사용한다.

FieldVisibility 옵션을 사용하면 멤버변수를 그대로 키값과 매핑시킬 수 있다.
하지만 이 옵션을 사용할 경우 내부 구현이 모두 노출될 수 있으므로 좋은 방법이 될 수 없다.

좋은 해결 방법

1. @JsonProperty로 변수명을 명시해준다.

가장 좋은 방법은 @JsonProperty를 명시해주는 것이다.

RequestDto

@Getter
@JsonAutoDetect(isGetterVisibility = Visibility.NONE)
public class GetTestResDto {

    @JsonProperty("isTest")
    private boolean isTest;

    public GetTestResDto(boolean isTest) {
        this.isTest = isTest;
    }

}

이렇게 하면 json의 키값이 "isTest"로 정확히 매핑된다.
이 때 주의할 점은 isGetterVisibility를 NONE으로 설정해줘야 한다는 것이다.
만약 해당 옵션이 켜져있다면 "isTest"와 "test"라는 두 개의 키값이 생길 것이다.
@JsonAutoDectect라는 어노테이션을 통해 해당 클래스의 Visibility 옵션을 변경하는 것을 확인할 수 있다.
만약 Visibility를 클래스 단위가 아니라 전역으로 설정하고 싶다면 설정파일에서 다음과 같이 적용할 수 있다.

application.yml

spring:
  jackson:
    visibility:
      getter: PUBLIC_ONLY
      is-getter: NONE
      setter: ANY
      creator: ANY
      field: PUBLIC_ONLY

2. boolean값에 prefix로 is를 쓰지 않는다.

그 다음 방법은 아예 boolean값에 prefix로 is를 쓰지 않는 것이다.
물론 boolean값의 prefix로 is가 많이 사용되고 가독성 측면에서도 좋지만, 위의 사례와 같이 오류의 가능성을 내포하고 있다.
따라서 팀의 상황에 따라 합의하에 testState와 같은 다른 변수명으로 컨벤션을 대체할 수 있을 것이다.

3. getter를 따로 정의한다.

Lombok의 @getter 어노테이션을 사용하지 않고 getter를 따로 정의한다.
isTest라는 변수명을 매핑시키기 위해 getIsTest()라는 별도의 함수를 만드는 것이다.

이 방법은 일장일단이 있는데 장점은 Lombok에 대한 의존성을 제거할 수 있다는 것이고, 단점은 편의성이 떨어진다는 것이다.

실제 현업에서는 외부 라이브러리에 대한 의존성을 최대한 줄이려고 한다는 이야기를 들은 적이 있다.
의존성을 줄인다는 관점에서 보았을 때 getter를 따로 정의하는 것은 상당히 괜찮은 방법이 될 수 있을 것 같다.
더불어 Lombok을 사용하더라도, 다른 클래스에만 getter를 사용하고 boolean의 prefix로 인해 문제가 생기는 클래스에서만 @getter 어노테이션을 제거하는 것도 방법이 될 수 있다.

결론

  1. boolean 값에 prefix로 is를 사용할 때는 json 매핑에 주의하자.
  2. @JsonAutoDetect의 Visibility 옵션을 통해 json 매핑 전략을 선택하자.
profile
'Young'속성의 삽질기

0개의 댓글