글 작성자: 개발섭

일단 글또 5주차를 맞이하여 테크테크스러운 글을 많이 작성하고 있다. 특히 요즘에는 별다른건 아니고, Jackson에 좀 꽂혀서 요즘 Serialize 혹으 Deserialize하는 방식에 대해서 어떤식으로 작동하는 거고 해당하는 코드들이 어떤 일련의 과정 속에서 움직이는 게 궁금했었다.
일단 기본적으로 Include를 많이 사용은 하는데 어떤 상황에서 사용하는게 좋을지 취사선택해야하는 경우가 많은데, 해당하는 값을 어떤식으로 처리하는게 좋을지 궁금해졌다.
사실은 Include nonNull 동작원리를 찾다가 동작방식을 찾아내질 못해서, 다른 주제로 선회하긴했다. 찾아보니까 이렇게 딥한 내용이었나 싶어서 좀 더 구체적으로 테스트 해봐도 좋겠다싶었다

JsonInclude란?

일단 JsonInclude라는 녀석은 무슨 녀석인가 하면, 이친구는 Json으로 Serialize변환될 때 이제 Dto의 값에따라서 이값을 넣을지? 아니면 뺄건지에 대해 결정하는 녀석으로 보면 된다.
해당하는 옵션은 다음처럼 총 5개이상이 있다.
조금 더 자세하게 JavaDoc을 찾아봤는데, 꽤 흥미롭다.
그래서 각각의 테스트를 좀 더 깊게 뒤적거려봤다. 테스트코드도 작성해봤다.

ALWAYS

속성값의 상관 없이 속성이 항상 포함되는 것
기본적으로는 이친구가 "기본값"이다

public class PojoDefault {  

    private String string;  
    private Long _long;  
    private Boolean aBoolean;  
    private Integer integer;  
    private LocalDateTime localDateTime;  
}

기본값이 없이 만들면 일단 기본적으로는 null 형태로 쭉 들어간다.

 {
  "string" : null,
  "_long" : null,
  "integer" : null,
  "localDateTime" : null,
  "aboolean" : null
}

NON_NULL

null이면 Json에서 빠지게됨.
일단 이 옵션을 키게되면 null이면 아예 빠지게된다. 뭐 null이면 애초에 빠지는 옵션이므로, 직관적으로 파악할 수 있다.

NON_ABSENT

값이 없는 프로퍼티가 포함됨.
Optional, AtomicReference가 null이 아닌 값일때는 들어가게됨.

@Builder  
@Getter  
public class PojoDefaultAbsent {  

    private String string;  
    private Long _long;  
    private Boolean aBoolean;  
    private Integer integer;  
    private LocalDateTime localDateTime;  
    private Optional<String> stringOptional;  
    private AtomicInteger atomicInteger;  

}

이걸 만들면 값이 null로 모두 들어가게되는데, 일단 이 상태에서는 아예 빈 Json이 나오게된다.
문제는 Optional과 AtomicInteger.. 즉, AtomicReference는 형태가 다르게 나오게 되는데.. 다음처럼 나오게 된다.

// Optional.ofNullable(null)으로 값을 넣기만 해도...
{
  "stringOptional" : {
    "empty" : true,
    "present" : false
  }
}
//atomic 객체가 만들어졌다면? new AtomicInteger()
{
  "atomicInteger" : 0
}

이를 통해서 일단 값이 들어가게되는 Optional의 경우는 주의해서 사용해야할 필요가 있다. 그리고 DTO의 형태에서 Optional을 지양하는 글들을 많이 봤는데 직접적으로 왜 그런지도 알거 같았다. 일단 Optional을 기본적으로 이상하게 처리한다. 뭐 empty니.. present니.. Json 형태가 기이해지므로 최대한 기본값이 있는 형태로 만드는 게 훨씬 효과적이라는 생각이 들었다.

NON_EMPTY

  • 값이 0이거나 비어 있는 것으로 간주되는 속성만 포함하지 않도록 지정
  • 일단 비어있는 것에 대한 기준은 다음과 같다.
    • Null 값.
    • "부재중" 값(NON_ABSENT 참조)
      를 기본값으로 설정하여 "비어 있음" 집합에 NON_NULL 및 NON_ABSENT에서 제외되는 값이 포함되도록 합니다.
      이 기준 외에도 다음 유형에는 추가 빈 값이 있습니다:
  • collection이나 map의 경우, isEmpty를 호출해서true인 경우
  • Java 배열의 경우, 빈 배열은 길이가 0인 배열입니다. 배열 길이 가지고 판단.
  • Java 문자열의 경우, length()가 호출되고 반환값이 0이면 빈 문자열을 나타냅니다.
    이런값들을 사용할 수는 있는데, JsonSerializer 구현을 직접해서 구체적으로 뭘 빼고, 뭘 포함하고 할 수 있음
    예를 들면 isEmpty를 사용하니 isEmpty를 아에 재정의 해서 하면 비어있는걸로 간주하지 않게 할 수도 있다는 뜻.
  • 2.6버젼까지는 다음도 비어있다고 포함했는데.. 그이후부터는 수정되었음.
  • 원시 유형의 기본값(예: int/java.lang.Integer의 경우 0, bool/Boolean의 경우 false)
  • 날짜/시간 타입의 타임스탬프 0

현재는 Spring버젼을 높혀사용하므로 다음과 같은 이슈는 없을거라고 생각은 하되, 레거시 코드들이 다음과 같은 이슈를 가지고 있으면 확인이 필요하다.

@Builder  
@Getter  
public class PojoDefaultEmpty {  

    private String string;  
    private Map map;  
    private Integer[] array;  
    private int integer;  
    private boolean trueorFalse;
}

이 형태를 다음처럼 primitive값인 경우는 노출이된다

{
  "integer" : 0,
  "trueorFalse" : false
}

단, 저기처럼 배열인데, 값이 뭐가 들어가있기만 하면 무조건 노출된다.

//array new Integer[1]이면 null로 들어가긴 한다. 근데 이러면 노출은 됨.
// 대신 new Integer[0]은 안나온다. array.length가 0이므로
{
  "integer" : 0,
  "trueorFalse" : false,
  "array" : [ null ]
}

NON_DEFAULT

  • 이 설정의 의미는 문맥에 따라 달라집니다. 즉, 어노테이션이 POJO 유형(클래스)에 대해 지정되었는지 여부에 따라 달라집니다.
  • 제외 케이스
    • "비어 있는" 것으로 간주되는 모든 값(NON_EMPTY에 따라)은 제외됩니다.
    • Primitive/wrapper 기본값은 제외됩니다.
    • 타임스탬프(에포크 이후 밀리초 단위의 long 값, 날짜 참조)가 0L인 날짜/시간 값은 제외됩니다.
  • 속성 값이 'null'인 경우를 제외하고는 equals() 메서드를 사용하여 값이 기본값으로 사용되며, 이 경우 곧바로 null 검사가 사용됩니다.
@Builder  
@Getter  
@ToString  
public class PojoDefaultNon {  

    private String string;  
    private int integer;  
    private boolean trueorFalse;  
    private Timestamp timestamp;  
    private char aChar;  
    private float aFloat;   
}

이렇게 만든다음에..
TimeStamp를 0L으로 만들거나.. String을 ""으로 들어가면?

{ }
//아래는 기본값의 상태를 노출하는건데..timestamp가 0L이면 이렇게된다.
PojoDefaultNon(string=, integer=0, trueorFalse=false, timestamp=1970-01-01 09:00:00.0, aChar=, aFloat=0.0)

즉, 만약 이값이 살짝만 바뀌어도 노출이 된다는 뜻이고..
그럼 이값을 살짝만 바꿔보자. String은 " " 한칸은 띄고, timeStamp는 1만 추가했는데 값이 바귄다.

{
  "string" : " ",
  "timestamp" : 1
}
//아래처럼 "" , 1l의 경우 다르다.
PojoDefaultNon(string= , integer=0, trueorFalse=false, timestamp=1970-01-01 09:00:00.001, aChar=, aFloat=0.0)

즉, 이경우는 primitive타입까지 고려해서 관리할때 유용한 옵션으로 보인다.

USE_DEFAULTS

  • (원문)
    • 의사 값은 포함 값을 재정의하지 않기 위해 상위 수준의 기본값이 합리적임을 나타내는 데 사용됩니다.
  • 예를 들어 프로퍼티에 대해 반환된 경우 프로퍼티가 정의된 경우 프로퍼티를 포함하는 클래스의 기본값을 사용하고, 정의된 것이 없는 경우 전역 직렬화 포함 세부 정보를 사용합니다.
  • 정의가 안되있으면 글로벌 따라간다는 거같음. 클래스 자체에 포함되어있으면 그거 따라가고.
    이경우는 테스트를 하기가 곤란하긴 했지만 유용한 예제를 봤다.
    즉, 정의여부에 따라서 글로벌 옵션을 먼저 둘건지.. 아니면 글로벌 옵션이 없는 경우는 원래것을 먼저 따라가는 것이였다.
@Builder  
@Getter  
public class PojoDefaultEmpty {  

    private String string;  
    private Map map;  
    private Integer[] array;  
    private int integer;  
    private boolean trueorFalse;  
    @JsonInclude(JsonInclude.Include.NON_NULL)  
    private List<String> list;  


    @JsonInclude(JsonInclude.Include.NON_DEFAULT)  
    public List<String> getList() {  
        return list;  
    }

다음처럼 Empty를 글로벌 옵션을 줄건데.. list는.. non null 옵션을 따라갈거다.
즉, 이런 경우라면? 기본적으론 list가 new ArrayList(); 인 경우는 노출이 안될거다.
→ 문제는 아래처럼 getter에 non Default가 걸려있다면?

{
  "integer" : 0,
  "trueorFalse" : false
}

음..? list는 어디로간거지? 즉, getter에 이옵션이 달려있다보니까 이게 empty 정책을 따라가게 되버린거고.. 없어졌다.
그럼 주석처리하게 된다면?

{
  "integer" : 0,
  "trueorFalse" : false,
  "list" : [ ]
}

null이 아니므로 노출된다. 이런거 처럼 다양한 옵션을 활용할 수 있다.

CUSTOM

  • 별도의 필터 객체(값 자체의 경우 JsonInclude.valueFilter() 및/또는 구조화된 유형의 콘텐츠의 경우 JsonInclude.contentFilter()로 지정됨)가 사용됨.
    커스텀의 경우는 우리가 원하는 값을 막 조작할 수 있으므로 이번에는 검증하지 않았다.

결론

회사에서 JsonInclude NON_NULL 옵션이 걸려있는 경우가 종종 있어서 해당하는 거가 null을 삭제한다는걸 대강 알고는 있었으나, 이번 기회에 어떤 옵션을 통해서 값이 어떤식으로 처리되었는지에 대해서 좀 더 꼼꼼하게 알아볼 수 있어서 좋았다.
특히, Empty라던가.. absent는 꽤 유용해보이긴했고, default 옵션도 좋아보였다. 이런식으로 Deep Dive하는 경험은 꽤 많은 도움을 주는 것 같았는데, 피상적으로 남들이 Optional 자제할것.. 이런식으로 이야기 해주는 것보다.. 오히려 지금처럼 테스트 해보면서 처리하면 그 이야기가 왜 나오는지에 대해서 다시 한번 알아보는 기회가 된것 같기도해서 좋았다..

출처

 

Jackson JSON - @JsonInclude USE_DEFAULTS Example

Jackson JSON - @JsonInclude USE_DEFAULTS Example [Last Updated: Aug 11, 2020]

www.logicbig.com

 

jackson-annotations 2.16.1 javadoc (com.fasterxml.jackson.core)

Latest version of com.fasterxml.jackson.core:jackson-annotations https://javadoc.io/doc/com.fasterxml.jackson.core/jackson-annotations Current version 2.16.1 https://javadoc.io/doc/com.fasterxml.jackson.core/jackson-annotations/2.16.1 package-list path (us

javadoc.io

P.S.

https://stackoverflow.com/questions/66572470/compare-2-json-strings-using-junit5-and-mockito
https://www.baeldung.com/jsonassert
JSON Junit테스팅 할때는 예상보다 좀 쉬웠어서 해당하는 코드들을 사용했다. 나중에 도움이 될 수 있으므로. 확인해보는걸 추천한다.

 

Compare 2 Json strings using Junit5 and Mockito

I have the below Junit.i am trying to test compare if both Json string have same fields (order and value is not important). My test keep getting failed with below error org.opentest4j.

stackoverflow.com

테스트 코드

https://github.com/ventulus95/test-tech-project/blob/JsonInclude/src/test/java/com/ventulus95/testtechproject/JsonIncludeTest.java