글 작성자: 개발섭

왜 쓰게 되었나..

일단 회사 프로젝트가 잦은 야근으로 지난주를 훌렁 날려버린 나는 현재 눈물의 개발 글쓰기를 진행중인데, 해당하는 프로젝트 진행도중 Request는 공통정보가 있지만, 각 3rd-party 서비스 추가 응답을 활용해서 각각의 응답에 따라 다른 API를 넘겨줘야하는 상황에 봉착했다.

해당하는 프로젝트에서 중복적으로 많이 사용되는 field가 있었고.. 해당하는 필드를 각 DTO별로 중복적으로 사용하는건 좀 귀찮은 일이기도 하니.. 해당하는 형태를 부모-자식 형태를 통해 상속으로 처리를 하려했다. 상속의 경우 Json Serialize가 값으로 처리되기가 곤란한 경우가 종종 있었는데, 특히나 Spring boot에서 별다른 어노테이션이 없이 무자비하게 떨렁 응답에 대한 처리를 해봤자.. 상속하려는 모쪽의 값만 처리될뿐 자쪽의 필드 값이 처리가 안되는 문제가 있었다.

→ 일반의 경우는 naver 응답이 무시되었음.

→ 네이버 Json을 만들었지만 케스팅이 안된다.

 

흠... 이런 문제는 분명 많은 불편함을 낳았을텐데..

 

그렇다 이런 문제는 이미 해결된 문제로 @JsonTypeInfo기능을 활용하면 쉽게 해결이 된다. 해당 기능중 ID 옵션 값이 많다는 사실을 알았는데, name, class로 사용하는 것이 있고, 특이하게 Deduction 기능을 활용하는게 훨씬 편하다는 사실을 알았다.
일단, 사용법부터 일단 간단하게 알아보자. 기존 방식과 비교해서 어떤점이 좀 더 좋은지를 알아보고 마지막에는 이게 어떤 방식으로 동작 하는지 알아보자.

기초적인 사용법은 해당하는 Request 혹은 Dto에 부모쪽에 다음처럼 붙이면 된다

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)  
@JsonSubTypes({
    @JsonSubTypes.Type(NaverJsonTypeRequest.class), 
    @JsonSubTypes.Type(KakaoJsonTypeRequest.class)
})

그리고 JsonSubTypes안에 → 자식 타입을 넣으므로써.. 해당하는 값이 자동으로 직렬화 되게 해주면 된다.

기존 방식 Name방식 혹은 class가 있는 방식

JsonTypeInfo는 dto자체에 id값을 넣거나 class값을 넣게 되면 해당하는 class을 해당 타입으로 변환 해준다. 예를 들면...

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)  
@JsonSubTypes({
    @JsonSubTypes.Type(NaverJsonTypeRequest.class), 
    @JsonSubTypes.Type(KakaoJsonTypeRequest.class)
})
public class JsonCommonTypeRequest {  
    private Integer id;  
    private String title;  
    private String content;  
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")  
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyy-MM-dd HH:mm:ss")  
    private LocalDateTime uploadTime;  
}

JsonTypeInfo.Id.NAME 옵션이 있을때는 Json을 다음처럼 처리해야한다.

{  
  "id": 1,  
  "title": "123",  
  "content": "123",  
  "uploadTime": "2024-01-20 10:10:20",  
  "@type": "NaverJsonTypeRequest",  
  "naverId": "naverid명",  
  "blogInTitle": "블로그 타이틀명"  
}

매번마다 @type을 넣는 방식은 이런건 나중에 불편함을 초래하기 딱 좋다. 왜냐하면, 일단 기본적으로 들어가야할 값에 타입을 매번 넣어준다는건 각각 상황별로 type에 대한 Switch case가 존재하는 거고.. 그 상황일때는 Json 값을 넣어야하는건데...그건.. 좀 힘들고.. 심지어 class를 준경우는 class full path까지 사용해야한다.
클래스는 더 나아가서 fullpath주소를 줘야한다. 아래처럼. 심지어 하나라도 빼먹으면 Json은 인식하지 못하고 error를 뱉어낸다.
"@class": "com.ventulus95.testtechproject.dto.request.NaverJsonTypeRequest",
→ 이렇게 처리된다면.. 분명히 너죽고 나죽자.. 클라이언트 개발자와 서버개발자는 키배를 벌이게 될 수 있는 운명이다..

그렇다면 Deduction을 사용하게 된다면, 어떻게 쓰는게 좋을까?

Deduction을 사용하는 방법

@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)  
@JsonSubTypes({
    @JsonSubTypes.Type(NaverJsonTypeRequest.class), 
    @JsonSubTypes.Type(KakaoJsonTypeRequest.class)
})
public class JsonCommonTypeRequest {  
    private Integer id;  
    private String title;  
    private String content;  
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")  
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyy-MM-dd HH:mm:ss")  
    private LocalDateTime uploadTime;  
}

인 경우
JsonTypeInfo.Id.DEDUCTION 옵션이 있을때는 Json을 다음처럼 처리해야한다.

{  
  "id": 1,  
  "title": "123",  
  "content": "123",  
  "uploadTime": "2024-01-20 10:10:20",  
  "naverId": "naverid명",  
  "blogInTitle": "블로그 타이틀명"  
}

이렇게 해도? 정상적으로 동작하게 되는데...

Deduction은 자체적으로 자식 클래스에만 있는 필드들을 가지고 이 값들을 판단한다. 이것은 적어도 우리가 보기에는 훨씬 효과적으로 동작한다. 타입에대한 필드 값을 넣지 않더라도.. 우리는 해당하는 해당하는 class를 만들어 줄 수 있다는 것은 타 개발자들과 소통하는 측면에서 훨씬 불필요한 논의를 줄일 수 있고, 직관적이다.

물론 해당하는 방식도 한계가 있을 수 있다. optional한 값으로 처리된 값들중 자식들끼리 필드가 겹쳐서 값에 대한 명확한 판단을 내릴 수 없다면 오류를 뱉는 점이다.
다음처럼.. Kakao와 Naver의 값이 우연찮게 겹쳤는데.. 안겹치는 부분외의 값이 하필이면 optional인 상황인것이다.
네이버는 naverId와 blogIntitle이고 kakao도 동일하게, blogtitle이 있으며, 해당하는 kakaoId가 optional 값이여서 누락될 수 있다면... 다음과 같은 오류가 발생하게 된다.

2024-01-21T17:32:56.131+09:00  WARN 52506 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Could not resolve subtype of [simple type, class com.ventulus95.testtechproject.dto.request.JsonCommonTypeRequest]: Cannot deduce unique subtype of `com.ventulus95.testtechproject.dto.request.JsonCommonTypeRequest` (2 candidates match)

즉, 2개가 동시에 매칭이 되버리는바람에 어떤 타입인지 모를 수 있다는 점이다. 그래도 이런 경우는 드물기도 하며, 필수값이 아닌 optional로 겹치는 경우는 차라리 공통타입으로 변수를 올려서 값을 만드는게 더 나은게 아닌가? 라는 생각도 든다. 그렇게 겹칠거면 부모쪽 클래스로 처리해버리는게 더 좋다고 생각한다.

궁금증

아무튼 이런 연유 때문에 deduction과 objectmapper에 대해서 좀 찾아보게되었는데, 이 동작 방식에 대해서 글을 작성하려니까 이 문제 해결기보다 더 길이 좀 길어질거 같아서 차후에 아예 각잡고 한개의 글을 적는게 나을듯해서 정리해보는걸루 마무리하겠다.

출처

해당 코드 관련한 github 주소
https://github.com/ventulus95/test-tech-project/tree/feature-JsonTypeInfo

그외 참고한 여러가지 자료들
https://www.baeldung.com/jackson-deduction-based-polymorphism
https://see-ro-e.tistory.com/340
https://stackoverflow.com/questions/55161374/get-derived-dto-from-base-class-request-body-dto