@RequestBody
HTTP Request Message Body
를 통해 전달된 데이터를 지정한 객체로 역직렬화하는 애노테이션HttpMessageConverter
의 일종인 MappingJackson2HttpMessageConverter
으로 처리기본 생성자 & getter
getter
가 없어도 DTO
의 생성 자체는 가능하지만, 필드가 초기화되지 않음setter
를 사용해도 되지만, DTO
의 특성상 getter
를 사용할 것을 권장모든 필드를 초기화할 수 있는 단 하나의 생성자
-parameters
필요Gradle
환경에서는 별도의 설정 없이 사용 가능Intellij
환경에서 실행할 경우 추가 설정 필요Maven
환경에서는 spring-boot-starter-parent
필요Property 생성자
를 명시하거나 Handler
를 추가한 Custom ObjectMapper
를 사용해 해결할 수도 있음기본 생성자
와 모든 필드를 초기화할 수 있는 단 하나의 생성자
가 있을 경우, 기본 생성자
가 우선권을 가짐처음 @RequestBody
를 사용했을 때, 아무튼 기본 생성자가 필요하다는 점 정도만 알고, 나머지는 잘 몰랐던 저는 다음과 같이 코드를 작성했습니다.
public class GameRequest {
private String names;
private int count;
public GameRequest() {
}
public GameRequest(final String names, final int count) {
this.names = names;
this.count = count;
}
public String getNames() {
return names;
}
public int getCount() {
return count;
}
}
그러자 다음과 같은 피드백을 받았습니다.
리뷰어께서 사용하지 않는 생성자
인 기본 생성자
를 왜 작성했는지, 그리고 접근 제어자는 왜 public
으로 둔 것인지 질문을 하셨습니다.
그래서 이와 관련해 조금 더 검색한 결과, @RequestBody
는 아무튼 내부적으로 알아서 리플렉션을 활용하기 때문에 굳이 접근 제어자를 public
으로 둘 필요가 없다는 사실을 알게 되었습니다.
public class GameRequest {
private String names;
private int count;
private GameRequest() {
}
private GameRequest(final String names, final int count) {
this.names = names;
this.count = count;
}
public String getNames() {
return names;
}
public int getCount() {
return count;
}
}
그래서 이렇게 리펙토링 했습니다.
기본 생성자는 @RequestBody
를 만드는 데에만 사용하기 때문에, 외부에서 접근할 수 없도록 막았습니다.
구글링을 했을 때 대부분의 코드가 이런 식으로 처리했기도 해서 이 정도면 충분하겠다고 생각했습니다.
그러자 또 다음과 같은 피드백을 받았습니다.
?
public class GameRequest {
private final String names;
private final int count;
public GameRequest(final String names, final int count) {
this.names = names;
this.count = count;
}
public String getNames() {
return names;
}
public int getCount() {
return count;
}
}
바로 기본 생성자를 삭제하고 실행해봤습니다.
?
안되는데요..?
당황한 저는 하루 정도 @RequestBody
관련해 구글링을 했지만, 답을 찾지 못했습니다.
뭐지? 리뷰어께서는 어떤 것을 의도하신거지?
개꿀잼 몰카인가? 싶었던 저는, 결국 그냥 코드를 까보기로 결심했습니다.
코드를 까 보기 전 먼저 이것들이 어떠한 용도인지 살펴보도록 하겠습니다.
API 문서에는 다음과 같이 설명하고 있습니다.
메소드 파라미터가 web request body에 명시한 데이터를 통해 bind 될 것을 나타내는 애노테이션입니다.
request body는 HttpMessageConverter에게 전달되어 바인딩됩니다.
필요하다면 @Valid 애노테이션을 사용해 유효성 검사를 자동으로 처리할 수 있습니다.
HTTP Request Message
의 Body
를 HttpMessageConverter
를 통해 내부적으로 알아서 처리해서 지정한 클래스로 binding
한다는 의미로 이해했습니다.
API 문서에는 다음과 같이 설명하고 있습니다.
HTTP request와 response를 특정 객체로 변환하기 위한 전략 인터페이스입니다.
너무 짧으니 Spring 문서도 살펴보도록 하겠습니다.
spring-web 모듈에는 InputStream과 OutputStream을 통해 HTTP 요청과 응답의 본문을 읽고 쓰기 위한 HttpMessageConverter 인스턴스가 포함되어 있습니다.
HttpMessageConverter 인스턴스는 클라이언트 측(예: RestTemplate)과 서버 측(예: Spring MVC REST 컨트롤러)에서 사용됩니다.
주요 미디어(MIME) 유형에 대한 구체적인 구현은 프레임워크에서 제공되며, 기본적으로 클라이언트 측에서는 RestTemplate에, 서버 측에서는 RequestMappingHandlerAdapter에 등록되어 있습니다.
그 밑에는 HttpMessageConverter
구현체가 설명되어 있는데, 그 중 MappingJackson2HttpMessageConverter
가 현 상황에 알맞은 HttpMessageConverter
입니다.
Jackson의 ObjectMapper를 사용해 JSON을 읽고 쓸 수 있는 HttpMessageConverter 구현체입니다.
Jackson에서 제공하는 주석을 사용하여 필요에 따라 JSON 매핑을 Customize 할 수 있습니다.
추가적으로 처리해야하는 로직이 필요한 경우(특정 유형에 대해 사용자 정의 JSON serializers/deserializers를 제공해야 하는 경우) ObjectMapper Property를 통해 사용자 정의 ObjectMapper를 주입할 수 있습니다.
기본적으로 이 HttpMessageConverter는 application/json을 지원합니다.
application/json
을 지원해준다고 명시되어있으므로 현재 상황에 가장 알맞을 것입니다.
그럼 API 문서도 확인해보도록 하겠습니다.
Jackson 2.x의 ObjectMapper를 사용하여 JSON을 읽고 쓸 수 있는 HttpMessageConverter의 구현체입니다.
이 HttpMessageConverter는 유형화된 빈 또는 유형화되지 않은 해시맵 인스턴스에 바인딩하는 데 사용할 수 있습니다.
기본적으로 이 변환기는 UTF-8 문자 집합으로 application/json 및 application/*+json을 지원합니다.
이는 supportedMediaTypes 속성을 설정하여 재정의할 수 있습니다.
그런데 코드를 확인해보니, 다음과 같은 메소드만이 명시되어 있었습니다.
public void setJsonPrefix(String jsonPrefix)
public void setPrefixJson(boolean prefixJson)
protected void writePrefix(JsonGenerator generator, Object object)
단순히 Json
의 Prefix
과 관련된 메소드만이 있어, 핵심적인 기능은 다른 곳에 있다고 판단하고 MappingJackson2HttpMessageConverter
의 상위 클래스 AbstractJackson2HttpMessageConverter
를 살펴봤습니다.
API 문서를 살펴보겠습니다.
Jackson 기반의 Content-Type과는 독립적인 HttpMessageConverter 구현을 위한 Abstract based class입니다.
저 기반
이라는 의미는 다음과 같습니다.
Jackson 라이브러리를 사용하여 HTTP 요청/응답 본문을 Java 객체로 변환하고 Java 객체를 JSON으로 변환하는 HttpMessageConverter의 기본적인 구현을 제공
그래서 메소드를 확인하면 canRead(), canWrite()
와 같이 사용할 수 있는지 여부를 확인하는 메소드와 read(), write()
와 같은 실제 Converter
로직 등 주요한 메소드가 정의되어 있습니다.
jackson objectMapper
를 활용하는 HttpMessageConverter
는 MappingJackson2HttpMessageConverter
를 포함해 4가지가 있는데, 이 HttpMessageConverter
모두 jackson objectMapper
를 사용하다보니 로직이 동일해 이를 하나로 추상화했다고 이해했습니다.
여기까지 살펴본 내용을 정리하면 다음과 같습니다.
@RequestBody
는 Http Request Message Body
로 전달된 데이터를 지정한 객체로 binding
하겠다는 의미를 지닌 일종의 마커 애노테이션@RequestBody
로 인해 객체가 binding
될 때, HttpMessageConverter
중 application/json
을 지원하는 MappingJackson2HttpMessageConverter
으로 인해 binding
진행여기까지 진행하자 이러한 궁금증이 생겼습니다.
HttpMessageConverter
를 호출하는 주체는 무엇일까?
그래서 찾아봤습니다.
이 그림은 요청을 수신한 후 응답을 반환할 때까지 Spring MVC의 처리 흐름
을 표현한 그림입니다.
이 중 2단계에서 해당 요청을 처리할 수 있는 Controller(Handler)
를 찾고, 3단계에서 이 요청을 처리하기 위해 Controller
가 필요로 하는 형식의 데이터로 변환해 4단계, Controller
를 호출하는 로직이 진행됩니다.
이 3단계의 HandlerAdapter
, 여기서는 RequestMappingHandlerAdapter
가 HttpMessageConverter
, 지금은 MappingJackson2HttpMessageConverter
를 통해 주어진 요청의 데이터를 Controller
가 필요한 형식의 데이터로 변환합니다.
이렇게 데이터의 형식을 변경해야 Controller
의 메소드를 호출하면서 필요한 데이터를 전달할 수 있게 됩니다.
더 자세히 알아보고 싶으시다면 그림의 출처인 여기를 참고해주시면 좋을 것 같습니다.
아무튼 지금은 HandlerAdapter
가 ArgumentResolver
를 호출하고, ArgumentResolver
에서 HttpMessageConverter
를 호출한다 정도로 넘어가겠습니다.
일단 어렴풋하게 용도와 흐름에 대해 이해했으니 디버깅을 해보겠습니다.
AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters()
@RequestBody
를 처리하는 RequestResponseBodyMethodProcessor
가 ArgumentResolver
중 AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters()
를 호출합니다.
AbstractMessageConverterMethodArgumentResolver
는 내부적으로 List<HttpMessageConverter<?>> messageConverters
를 가지고 있으며, AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters()
에서 모든 HttpMessageConverter
를 확인하며 해당 요청을 처리할 수 있는 HttpMessageConverter
를 찾아 read()
메소드를 호출합니다.
AbstractJackson2HttpMessageConverter.canRead()
해당 HttpMessageConverter
가 해당 HTTP Request Body
를 읽을 수 있는지 판단하기 위해 canRead()
가 호출됩니다.
AbstractJackson2HttpMessageConverter.canRead()
의 내부를 확인하면 MediaType
이 application/json
인지, 역직렬화 하고자 하는 타입을 핸들링할 수 있는 ObjectMapper
가 존재하는지, 그 ObjectMapper
가 해당 타입으로 역직렬화가 가능한지 확인하고 여부를 반환합니다.
AbstractJackson2HttpMessageConverter.read()
application/json
이므로 MappingJackson2HttpMessageConverter
가 선택되고, HTTP Request Messagre Body
가 존재하므로 @RequestBody
로직을 처리하기 위해서 AbstractJackson2HttpMessageConverter.read()
메소드를 호출합니다.
read()
메소드는 어떠한 클래스로 binding
을 수행해야하는지 확인하고, AbstractJackson2HttpMessageConverter.readJavaType()
를 호출합니다.
여기서 Type
을 JavaType
으로 변환하는데, Type
은 자바 리플렉션이며, JavaType
은 jackson
라이브러리에 포함되어있는, deserializer
에서 역직렬화를 수행할 때 사용되는 클래스입니다.
AbstractJackson2HttpMessageConverter.readJavaType()
먼저 해당 HTTP Request Message Body
의 charset
을 확인합니다.
기본적으로 Content-Type: appllication/json; charset=UTF-8
이므로 내부 플래그 isUnicode
는 true
가 됩니다.
이제 아래 try-catch
블록을 수행하게 됩니다.
첫 번째 분기문은 false
이므로 다음으로 넘어갑니다.
두 번째 분기문은 isUnicode
가 true
이므로 ObjectMapper.readValue()
가 호출됩니다.
ObjectMapper.readValue()
ObjectMapper
의 readValue()
는 오버로딩 되어있는 상태입니다.
이 오버로딩된 ObjectMapper.readValue()
는 ObjectMapper._readMapAndClose()
메소드를 호출합니다.
ObjectMapper._readMapAndClose()
ObjectMapper._readMapAndClose()
에서 HTTP Request Message Body
를 변환한 객체를 result
로 반환합니다.
여기서 JsonToken
을 통해 json
데이터가 있는지 확인합니다. JsonToken
은 json
의 시작 글자를 토큰으로 가져오며, 시작, 끝, 필드 이름, 문자열
등으로 분류할 수 있습니다.
JsonToken t = _initForReading(p, valueType);
를 통해 JsonToken
을 조회하는데, 현재 HTTP Request Message Body
가 있는 것이 이전에 확인되었으므로 검증하는 타입은 {
, 시작 토큰이므로 값은 START_OBJECT
가 됩니다.
그러므로 모든 분기에 걸리지 않고, else
블록에서 객체를 변환하게 됩니다.
DefaultDeserializationContext.readRootValue()
_config
는 일반적인 역직렬화 관련 구성을 의미합니다.
해당 기능은 기본적으로 비활성화 되어 있으므로 다음 분기문으로 넘어갑니다.
ObjectMapper._readMapAndClose()
에서 valueToUpdate
를 null
올 전달했기 때문에 두 번째 분기문이 동작합니다.
BeanDeserializer.deserialize()
JsonParser
는 이전에 START_OBJECT
, {
이므로 첫 번째 분기문에 들어갑니다.
이후 _vanillaProcessing
도 false
(특수 기능이 없는지 여부를 표현하는 플래그)고, _objectIdReader
도 null
이기 때문에(요청에 objectId
가 존재하지 않으므로 관련 Reader
가 존재하지 않음) 마지막 return
문이 실행됩니다.
BeanDeserializer.deserializeFromObject()
여기까지가 공통처리이며, 기본 생성자가 있는지 없는지에 따라 해당 메소드에서 분기 처리가 됩니다.
public class GameRequest {
private String names;
private int count;
public GameRequest() {
}
public String getNames() {
return names;
}
public int getCount() {
return count;
}
}
위와 같이 기본 생성자가 있는 경우입니다.
BeanDeserializer.deserializeFromObject()
위의 분기문을 모두 무시하고 createUsingDefault()
메소드를 호출합니다.
ValueInstantiator.createUsingDefault()
_defaultCrator.call()
을 호출합니다.
AnnotatedConstructor.call()
자바 리플렉션으로 가져온 기본 생성자를 통해 객체를 생성하는 것을 확인할 수 있습니다.
public class GameRequest {
private String names;
private int count;
public GameRequest(final String names, final int count) {
this.names = names;
this.count = count;
}
public String getNames() {
return names;
}
public int getCount() {
return count;
}
}
기본 생성자 대신 필드를 모두 초기화할 수 있는 생성자를 추가했습니다.
BeanDeserializer.deserializeFromObject()
기본 생성자를 사용했을 때에는 무시되던 분기문 중, _nonStandardCreation
이 true
가 되어 deserializeFromObjectUsingNonDefault()
메소드를 호출합니다.
BeanDeserializer.deserializeFromObjectUsingNonDefault()
현재 DTO
로 사용하고 있는 GameRequest
는 delegate
가 적용되어있지 않으므로 두 번째 분기문으로 가게 됩니다.
필드를 모두 초기화할 수 있는 생성자가 단 하나이기 때문에 이는 Property 생성자
로 인식되고, 이를 토대로 DTO 객체
를 생성합니다.
BeanDeserializer._deserializeUsingPropertyBased()
중간에 생성자
를 통해 빈을 생성하고 이를 반환합니다.
그래서 왜 기존 프로젝트에서는 기본 생성자 없이는 안된건가?
에 대한 부분은, 솔직히 디버깅으로는 알 수 없었습니다.
그래서 찾아본 결과, ParameterNamesModule
이 원인인 것을 확인할 수 있었습니다.
이 클래스의 용도는 다음과 같습니다.
Jackson의 ParameterNamesModule은 JavaBean 규칙이나 애노테이션을 사용하는 대신 매개변수 이름을 사용하여 Java 개체를 직렬화 및 역직렬화하는 방법을 제공하는 Java 라이브러리입니다.
이를 통해 개발자는 직렬화 및 역직렬화 중에 생성자 또는 메서드 매개 변수 이름을 JSON 속성 이름으로 사용할 수 있습니다.
즉 Jackson
을 사용할 때, 기본적으로는 기본 생성자 & getter / setter
와 같은 JavaBean
규칙을 따르거나 @JsonCreator / @JsonProperty
와 같은 애노테이션을 통해 Property Creator
를 지정해줘야 하지만,
ParameterNamesModule
를 등록한다면 매개 변수 이름을 통해서도 접근이 가능하다는 의미라고 볼 수 있습니다.
매개 변수 이름이니 생성자를 통해서도 접근이 가능할 것입니다.
이러한 ParameterNamesModule
는 org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration
에서 자동으로 추가됩니다.
@AutoConfiguration
@ConditionalOnClass(ObjectMapper.class)
public class JacksonAutoConfiguration {
// ...
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ParameterNamesModule.class)
static class ParameterNamesModuleConfiguration {
@Bean
@ConditionalOnMissingBean
ParameterNamesModule parameterNamesModule() {
return new ParameterNamesModule(JsonCreator.Mode.DEFAULT);
}
}
// ...
ObjectMapper
가 있고, ParameterNamesModule
이 없으면 무조건 등록됩니다.
즉 무조건 등록된다고 볼 수 있습니다.
이제 생성자로 건네주고 있는 JsonCreaor.Mode
도 살펴보도록 하겠습니다.
JsonCreator.Mode
는 inner enum
입니다.
javadoc 문서의 설명을 매우 짧게 요약하자면 다음과 같습니다.
생성자 및 팩토리 메소드를 연관된 클래스의 새 인스턴스를 인스턴스화하는 데 사용할 하나의 메서드로 정의하는 데 사용할 수 있는 마커 애노테이션입니다.
DEFAULT, DELEGATING, PROPERTIES, DISABLED
로 총 4가지가 존재하는데, 일단 DEFAULT
만 살펴보겠습니다.
JsonCreator.Mode.DEFAULT의 경우, ObjectMapper는 생성자 메소드의 매개변수와 입력 JSON 객체의 필드를 매칭합니다.
이는 JSON 필드가 생성자의 매개변수와 동일한 이름을 가져야 함을 의미합니다.
각 매개변수에 대해 일치하는 항목이 발견되면 JSON 개체의 해당 값을 사용하여 메서드가 호출됩니다.
설명만 보면 필드를 모두 초기화하는 생성자로도 충분히 사용할 수 있을 것 같습니다.
그래서 왜 Intellij
환경에서는 동작하지 않는가?에 대한 대답은 Jackson
의 github repository
의 README.md
에서 확인할 수 있습니다.(링크는 여기)
첫 번째 항목입니다.
Person 클래스는 공식 매개변수 이름을 저장하는 옵션(-parameters 옵션)이 켜져 있는 Java 8 호환 컴파일러로 컴파일해야 합니다.
런타임에 매개변수 이름에 액세스하기 위한 Java 8 API에 대한 자세한 내용은 method parameter reflection을 참조하세요.
program arguement
로 -parameters
설정을 줘야 한다는 것입니다.
이제 Intellij 환경
에서도 실행할 수 있게 되었습니다.
settings -> Java Compiler -> Additional command line parameters
에 -parameters
를 입력합니다.
Build -> Rebuild Project
를 통해 적용하면 됩니다.
정상적으로 실행됩니다.
그렇다면 왜 gradle
에서는 설정 없이도 정상적으로 동작했을까요?
gradle java plugin 문서에서는 못 찾았지만, 스프링 부트 문서에서는 찾을 수 있었습니다.
10번 항목에 -parameters 컴파일러 인수를 사용하도록 모든 JavaCompile 작업을 구성합니다
라고 명시되어 있기 때문이었습니다.
안 쓸 것 같기는 하지만 Maven
은 또 어떤가 싶어서 스프링 부트 문서를 확인해봤습니다.
spring-boot-starter-parent
를 사용한다면 여러 기본 값을 세팅해주는데, 그 중 세 번째 항목처럼 -parameters
를 통해 컴파일을 해 준다고 명시되어 있습니다.
다음과 같이 결론을 지을 수 있을 것 같습니다.
ObjectMapper.readValue()
는 기본 생성자 & POJO
혹은 Property Creator
가 기본기본 생성자
나 Property Creator
를 사용하지 않고 모든 필드를 초기화하는 생성자
를 통해 binding
하고자 하는 경우 -paremeters
컴파일 옵션 필요Intellj
: 별도 설정 필요Gradle
: 자동Maven
: spring-boot-starter-parent
필요디버깅을 통해 @RequestBody
가 어떤 식으로 동작하는지 살펴봤으니 궁금했던 부분을 하나씩 실험해보도록 하겠습니다.
막연히 자바 리플렉션을 사용하기 때문
이라고 알고 있었는데, 조금 더 자세히 확인하고 싶었습니다.
확인해본 결과, BeanDeserializer.deserializeFromObject()
에서 ValueInstantiator.createUsingDefault()
호출로 DTO 객체
를 생성한 이후 리플렉션을 통해 값을 세팅하고 있었습니다.
그렇다면 getter
를 사용하지 않으면 객체가 생성되지만, 필드는 초기화되지 않을 것 같습니다.
동일한 코드에서 property
를 찾지 못해 null
로 표현된 것을 확인할 수 있습니다.
디버깅으로 확인해본 결과 예상대로 GameRequest
의 필드는 초기화되지 않았음을 확인할 수 있었습니다.
@Valid
를 통해 DTO
의 값을 검증한 결과, 객체 생성 이후 필드가 초기화되지 않아 검증에 실패한 것을 확인할 수 있었습니다.
public class GameRequest {
private String names;
private int count;
public GameRequest(final String names) {
this.names = names;
}
}
이렇게 모든 필드를 초기화하지 않는 생성자만이 존재한다면 어떤 식으로 동작할까요?
일단 deserializeFromObjectUsingNonDefault()
메소드가 호출됩니다.
이후 deserializeFromObjectUsingNonDefault()
메소드까지는 진행되지만, _propertyBasedCreator
이 null
이여서 예외가 반환됩니다.
여담으로 HttpMessageConversionException
발생 시 메세지를 어디서 만들어주는지도 확인할 수 있었습니다.
public class GameRequest {
private String names;
private int count;
public GameRequest() {
}
public GameRequest(final String names, final int count) {
this.names = names;
this.count = count;
}
public String getNames() {
return names;
}
public int getCount() {
return count;
}
}
제가 처음에 작성했던 코드는 @RequestBody
로 인해 binding
될 때 어떤 생성자를 사용할지 궁금했습니다.
기본 생성자가 우선되는 것을 확인할 수 있었습니다.
public class GameRequest {
private String names;
private int count;
public GameRequest(final String names, final int count) {
this.names = names;
this.count = count;
}
public GameRequest(final int count, final String names) {
this.names = names;
this.count = count;
}
public String getNames() {
return names;
}
public int getCount() {
return count;
}
}
이 경우에는 어떤 생성자가 호출될까요?
모든 필드를 초기화하지 않는 생성자만 있는 경우와 같이, 어떤 생성자가 Property 생성자
인지 판단할 수 없어 예외가 발생하는 것을 확인할 수 있었습니다.
그냥 생성자 둘 중 하나를 삭제하면 되겠지만 굳이 다른 방법으로 해결해보도록 하겠습니다.
@JsonCreator
와 @JsonProperty
를 사용하면 어떤 생성자를 사용할지 ObjectMapper
에게 알려줄 수 있습니다.
public class GameRequest {
private String names;
private int count;
@JsonCreator
public GameRequest(@JsonProperty("names") final String names, @JsonProperty("count") final int count) {
this.names = names;
this.count = count;
}
public GameRequest(final int count, final String names) {
this.names = names;
this.count = count;
}
public String getNames() {
return names;
}
public int getCount() {
return count;
}
}
이전과 다르게 _propertyBasedCreator
가 존재하는 것을 확인할 수 있습니다.
_propertyBasedCreator
를 확인해보면 @JsonCreator
로 지정한 GameRequest(String, int)
임을 확인할 수 있습니다.
이전에 기본 생성자가 아닌 생성자를 통해 binding
을 할 때, 다음과 같은 상황에서는 예외가 발생한다고 말씀드렸습니다.
하지만 사실 바로 예외가 발생하는 것은 아니고, 과정이 하나 더 있습니다.
DeserializationContext.handleMissingInstantiator()
에서 생성자를 찾지 못한 상황이라도 이를 수행할 수 있는 h, handler
가 있다면 객체를 binding
해서 반환합니다.
등록된 모든 handler
를 찾게 되는데, 기본적으로는 아무 handler
로 등록되어 있지 않습니다.
이러한 과정을 거친 뒤에야 위와 같이 예외를 발생시키는 객체를 반환하는 방식입니다.
이 handler
는 DeserializationProblemHandler
으로, 이를 확장한 handler
를 정의해 ObjectMapper
에 등록하면 이를 해결할 수 있습니다.
// Handler
public class GameRequestDeserializationProblemHandler extends DeserializationProblemHandler {
@Override
public Object handleMissingInstantiator(final DeserializationContext ctxt, final Class<?> instClass,
final ValueInstantiator valueInsta, final JsonParser p, final String msg) throws IOException {
if (GameRequest.class.isAssignableFrom(instClass)) {
return handleMissingInstantiatorForGameRequest(ctxt);
}
return super.handleMissingInstantiator(ctxt, instClass, valueInsta, p, msg);
}
private Object handleMissingInstantiatorForGameRequest(final DeserializationContext ctxt) {
try {
final JsonNode jsonNode = ctxt.getParser().getCodec().readTree(ctxt.getParser());
final String names = jsonNode.get("names").asText();
final int count = jsonNode.get("count").asInt();
return new GameRequest(names, count);
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
}
// Configuration
@Configuration
public class RaceConfiguration {
@Bean
ObjectMapper objectMapper() {
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.addHandler(new GameRequestDeserializationProblemHandler());
return objectMapper;
}
}
이 경우 GameRequestDeserializationProblemHandler
를 등록했기 때문에 정상적으로 처리가 가능합니다.
모르는 기능을 사용하고자 할 때에는 먼저 문서를 봐야 할 것 같습니다.
역시 지토 폼 미쳤다.