1. 기본생성자가 없는 객체에 대한 역직렬화 차이
여기 객체 1개가 있다.
@Getter
@AllArgsConstructor
public class TestDto {
private String id;
}
기본생성자가 없는 객체를 만들었다.
직렬화/역직렬화 테스트를 해보자.
@RestController
public class TestController {
@GetMapping
public TestDto test(@RequestBody TestDto dto) {
return dto;
}
}
// input
{
"id": "1"
}
// output
{
"id": "1"
}
문제 없이 성공한다.
성공하는 이유는 JacksonHttpMessageConvertersConfiguration 클래스에 json을 직렬화/역직렬화할 수 있는 MappingJackson2HttpMessageConverter 빈이 등록되어 있기 때문이다.
<JacksonHttpMessageConvertersConfiguration>
@Bean
// ...
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
return new MappingJackson2HttpMessageConverter(objectMapper);
}
MappingJackson2HttpMessageConverter는 ObjectMapper 빈을 주입해서 생성한다.
ObjectMapper 빈은 JacksonAutoConfiguration 클래스에 등록되어 있다.
<JacksonAutoConfiguration>
@Bean
@Primary
@ConditionalOnMissingBean
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
return builder.createXmlMapper(false).build();
}
앞서 TestDto를 역직렬화할 때는 위 ObjectMapper 빈을 사용하는 것이다.
중요!! 이 때, Jackson2ObjectMapperBuilder를 통해 ObjectMapper를 생성한다.
-> 기억하고 넘어가자.
그런데 잘 보면, 빈 생성 메서드에 @ConditionalOnMissingBean 애노테이션이 붙어 있다.
@ConditionalOnMissingBean은 ObjectMapper 빈이 존재하지 않을 때만 빈 생성 메서드가 실행되어 빈을 등록한다.
즉, 개발자가 별도로 ObjectMapper 빈을 생성하지 않으면, Spring이 기본으로 제공하는 ObjectMapper 빈을 사용할 수 있다.
예를 들어, 다음과 같이 빈 주입을 사용할 수도 있다.
@RestController
@RequiredArgsConstructor
public class TestController {
private final ObjectMapper objectMapper;
// ...
}
그럼 별도로 ObjectMapper 빈을 생성해서 Spring이 기본으로 제공하는 ObjectMapper 빈을 대체해보자.
중요! 이 때, 기본 생성자를 통해 ObjectMapper를 생성해보자.
@Configuration
public class ObjectMapperConfig {
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
}
이제 다시 테스트를 해보자.
@RestController
public class TestController {
@GetMapping
public TestDto test(@RequestBody TestDto dto) {
return dto;
}
}
// input
{
"id": "1"
}
// output
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot construct instance of `com.example.jackson_deserialize_java.TestDto` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)]
오류가 발생한다. (역직렬화할 때 발생하는 오류)
왜 이런 차이가 발생할까?
jackson의 역직렬화 관련 로직은 다음 글에서 자세하게 기록해 놓았다.
(상당히 긴 글이기 때문에, jackson 역직렬화 동작 방식이 특별히 궁금하지 않다면 아래 내용으로 넘어가도 좋다)
https://gksdudrb922.tistory.com/336
[Spring] jackson 역직렬화, 필드가 1개일 때 HttpMessageNotReadableException
jackson 2.17 문제 상황개발환경java 21spring 3.3.0jackson 2.17.1 다음과 같은 java 코드가 있다.@RestControllerpublic class TestController { @GetMapping public TestDto test(@RequestBody TestDto dto) { return dto; }}@Getter@AllArgsConstructo
gksdudrb922.tistory.com
문제가 발생하는 원인을 간단히 요약하면 이렇다.
1. Jackson2ObjectMapperBuilder: property-based 역직렬화 가능
2. 생성자: property-based 역직렬화 불가능
+) property-based 역직렬화: JSON 데이터를 객체의 프로퍼티(필드)와 직접 매핑해서 역직렬화 하는 방식. (ex. 생성자)
앞서 첨부한 글은 역직렬화 대상 객체의 필드가 1개일 때 상황에 대해 언급했는데,
본 글에서 다룰 ObjectMapper 생성 방식에 따른 차이는 jackson 버전, 필드 개수와 무관하게 발생하는 이슈다.
첨부한 글에서 설명했지만 jackson 2.18 버전 이상 기준,
property-based 역직렬화를 사용하려면 다음 jackson 코드의 주석친 부분의 메서드가 호출되어야 한다.
<BeanDeserializerFactory._constructDefaultValueInstantiator()>
protected ValueInstantiator _constructDefaultValueInstantiator(DeserializationContext ctxt,
BeanDescription beanDesc)
throws JsonMappingException
{
// ...
final PotentialCreators potentialCreators = beanDesc.getPotentialCreators();
// ...
if (potentialCreators.hasPropertiesBased()) {
PotentialCreator primaryPropsBased = potentialCreators.propertiesBased;
if (primaryPropsBased.paramCount() == 0) {
creators.setDefaultCreator(primaryPropsBased.creator());
} else {
// property-based 역직렬화 사용하는 메서드
_addSelectedPropertiesBasedCreator(ctxt, beanDesc, creators,
CreatorCandidate.construct(config.getAnnotationIntrospector(),
primaryPropsBased.creator(), primaryPropsBased.propertyDefs()));
}
}
// ...
}
그리고 _addSelectedPropertiesBasedCreator() 메서드는 if (potentialCreators.hasPropertiesBased()) 조건을 통과해야 호출할 수 있다.
<PotentialCreators.hasPropertiesBased()>
public class PotentialCreators {
public PotentialCreator propertiesBased;
public boolean hasPropertiesBased() {
return (propertiesBased != null);
}
}
PotentialCreator는 Jackson이 객체를 역직렬화할 가능성이 있는 생성자를 결정하는데 활용된다.
hasPropertiesBased() 메서드는 property-based 역직렬화가 가능한 생성자가 있는지 확인한다.
그럼 propertiedBased 필드가 언제 세팅되는지 추적해보면 되겠다.
결론부터 말하자면,
1. Jackson2ObjectMapperBuilder: propertiesBased를 세팅한다.
2. 생성자: propertiesBased를 세팅하지 않는다.
propertiesBased를 세팅하는 코드 분석
이제 왜 두 방식에 따라 property-based 역직렬화 가능/불가능이 갈리게 되는지 알아보자.
protected ValueInstantiator _constructDefaultValueInstantiator(DeserializationContext ctxt,
BeanDescription beanDesc)
throws JsonMappingException
{
// ...
// Go!
final PotentialCreators potentialCreators = beanDesc.getPotentialCreators();
// ...
if (potentialCreators.hasPropertiesBased()) {
PotentialCreator primaryPropsBased = potentialCreators.propertiesBased;
if (primaryPropsBased.paramCount() == 0) {
creators.setDefaultCreator(primaryPropsBased.creator());
} else {
// property-based 역직렬화 사용하는 메서드
_addSelectedPropertiesBasedCreator(ctxt, beanDesc, creators,
CreatorCandidate.construct(config.getAnnotationIntrospector(),
primaryPropsBased.creator(), primaryPropsBased.propertyDefs()));
}
}
// ...
}
if (potentialCreators.hasPropertiesBased())를 호출하기 전 potentialCreators 변수를 조회하는 Go! 주석 부분으로 가보자.
<BasicBeanDescription.getPotentialCreators()>
@Override
public PotentialCreators getPotentialCreators() {
if (_propCollector == null) {
return new PotentialCreators();
}
// Go!
return _propCollector.getPotentialCreators();
}
<POJOPropertiesCollector.getPotentialCreators()>
public PotentialCreators getPotentialCreators() {
if (!_collected) {
// Go!
collectAll();
}
return _potentialCreators;
}
PotentialCreator를 조회하기 전에 전처리 과정을 거친다.
<POJOPropertiesCollector.collectAll()>
protected void collectAll() {
// ...
LinkedHashMap<String, POJOPropertyBuilder> props = new LinkedHashMap<String, POJOPropertyBuilder>();
// ...
if (!_classDef.isNonStaticInnerClass()) { {
// Go!
_addCreators(props);
}
}
<POJOPropertiesCollector.addCreators()>
protected void _addCreators(Map<String, POJOPropertyBuilder> props)
{
final PotentialCreators creators = _potentialCreators;
//...
final ConstructorDetector ctorDetector = _config.getConstructorDetector();
if (!creators.hasPropertiesBasedOrDelegating()
&& !ctorDetector.requireCtorAnnotation()) {
if ((_classDef.getDefaultConstructor() == null)
|| ctorDetector.singleArgCreatorDefaultsToProperties()) {
// Go!
_addImplicitConstructor(creators, constructors, props);
}
}
// ...
}
<POJOPropertiesCollector._addImplicitConstructor()>
private boolean _addImplicitConstructor(PotentialCreators collector,
List<PotentialCreator> ctors, Map<String, POJOPropertyBuilder> props)
{
// ...
if (ctor.paramCount() != 1) {
if (!ctor.hasNameOrInjectForAllParams(_config)) {
return false;
}
} else {
if ((_annotationIntrospector != null)
&& _annotationIntrospector.findInjectableValue(ctor.param(0)) != null) {
} else {
// ...
if (!ctorDetector.singleArgCreatorDefaultsToProperties()) {
POJOPropertyBuilder prop = props.get(ctor.implicitNameSimple(0));
if ((prop == null) || !prop.anyVisible() || prop.anyIgnorals()) {
return false;
}
}
}
}
ctors.remove(0);
// Go!
collector.setPropertiesBased(_config, ctor, "implicit");
return true;
}
코드 맨 하단에서 property-based 생성자를 세팅하는 것 같다.
<PotentialCreators.setPropertiesBased()>
public void setPropertiesBased(MapperConfig<?> config, PotentialCreator ctor, String mode)
{
if (propertiesBased != null) {
throw new IllegalArgumentException(String.format(
"Conflicting property-based creators: already had %s creator %s, encountered another: %s",
mode, propertiesBased.creator(), ctor.creator()));
}
// Go!
propertiesBased = ctor.introspectParamNames(config);
}
맞다. 여기서 propertiesBased를 세팅하기 때문에,
if (potentialCreators.hasPropertiesBased())를 통과해서, addPropertyCreator() 메서드 호출이 가능하다.
그런데, 이건 Jackson2ObjectMapperBuilder를 통해 ObjectMapper를 생성할 때 시나리오고,
만약 new ObjectMapper()를 사용한다면 setPropertiesBased()를 호출하기 전 다음 조건에 잡히게 된다.
private boolean _addImplicitConstructor(PotentialCreators collector,
List<PotentialCreator> ctors, Map<String, POJOPropertyBuilder> props)
{
// ...
if (ctor.paramCount() != 1) {
if (!ctor.hasNameOrInjectForAllParams(_config)) {
return false;
}
} else {
if ((_annotationIntrospector != null)
&& _annotationIntrospector.findInjectableValue(ctor.param(0)) != null) {
} else {
// ...
if (!ctorDetector.singleArgCreatorDefaultsToProperties()) {
// Go!
POJOPropertyBuilder prop = props.get(ctor.implicitNameSimple(0));
if ((prop == null) || !prop.anyVisible() || prop.anyIgnorals()) {
return false;
}
}
}
}
ctors.remove(0);
// propertiesBased 세팅하는 부분
collector.setPropertiesBased(_config, ctor, "implicit");
return true;
}
정확히는 prop 값이 null이어서 하단 코드로 진입이 실패한다.
prop이 null인 이유 -> ctor.implictiNameSimple(0) = null
<PotentialCreator.implicitNameSimple()>
public String implicitNameSimple(int ix) {
PropertyName pn = _implicitParamNames[ix];
return (pn == null) ? null : pn.getSimpleName();
}
1. Jackson2ObjectMapperBuilder: _implicitParamNames에 parameter값 존재. (TestDto.id)
2. 생성자: _implicitParamNames 비어 있다.
이제 거의 다 왔다. _implicitParamNames를 세팅해주는 코드를 분석해본다.
<PotentialCreator.introspectParamNames()>
public PotentialCreator introspectParamNames(MapperConfig<?> config)
{
// ...
final AnnotationIntrospector intr = config.getAnnotationIntrospector();
for (int i = 0; i < paramCount; ++i) {
AnnotatedParameter param = _creator.getParameter(i);
// Go!
String rawImplName = intr.findImplicitPropertyName(param);
if (rawImplName != null && !rawImplName.isEmpty()) {
_implicitParamNames[i] = PropertyName.construct(rawImplName);
}
// ...
}
return this;
}
Go! 주석 부분의 intr.findImplicitPropertyName() 값이 존재하면 _implicitParamNames를 세팅해준다.
+) 참고
_explicitParamNames: 명시적 매개변수(@JsonPropery 애노테이션이 붙은 매개변수 이름 목록)
_implicitParamNames: 암시적 매개변수(리플렉션 등)
intr.findImplicitPropertyName()에서 intr은 AnnotationIntrospector는 인터페이스인데, 각 상황에 따라 선택하는 구현체가 다른다.
1. Jackson2ObjectMapperBuilder: ParameterNamesAnnotationIntrospector
2. 생성자: JacksonAnnotationIntrospector
+) 두 케이스 모두 기본 Introspector는 JacksonAnnotationIntrospector
그러나 Jackson2ObjectMapperBuilder 케이스의 경우에만 ParameterNamesAnnotationIntrospector 구현체를 추가해서 우선으로 사용한다.
ParameterNamesAnnotationIntrospector는 리플렉션 사용으로 파라미터 이름을 조회할 수 있고,
JacksonAnnotationIntrospector는 파라미터 이름을 조회하지 못해, _implicitParamNames 세팅에 차이가 발생하게 된다.
ParameterNamesAnnotationIntrospector를 세팅하는 코드 분석
그럼 Jackson2ObjectMapperBuilder를 사용하는 경우에만 ParameterNamesAnnotationIntrospector를 세팅하는 이유를 알아보자.
<Jackson2ObjectMapperBuilder.build()>
Jackson2ObjectMapperBuilder로 ObjectMapper를 생성하는 build() 메서드
public <T extends ObjectMapper> T build() {
ObjectMapper mapper;
// ...
// Go!
configure(mapper);
return (T) mapper;
}
<Jackson2ObjectMapperBuilder.configure()>
public void configure(ObjectMapper objectMapper) {
// ...
List<Module> modules = new ArrayList<>();
for (List<Module> nestedModules : modulesToRegister.values()) {
modules.addAll(nestedModules);
}
// Go!
objectMapper.registerModules(modules);
// ...
}
ObjectMapper를 초기 세팅하기 위해 모듈을 등록한다.
new ObjectMapper()에서는 모듈 등록하는 코드가 없다. (여기서 차이가 난다)
<ObjectMapper.registerModule()>
public ObjectMapper registerModule(Module module)
{
// ...
// Go!
module.setupModule(new Module.SetupContext()
{
// ...
});
return this;
}
setupModule()은 추상메서드인데, 다음 모듈 구현체를 살펴보자.
JacksonAutoConfiguration에는 다음과 같은 모듈 빈이 등록되어 있다. (리플렉션을 통해 파라미터 이름 조회를 가능하게 하는 모듈 같다)
<JacksonAutoConfiguration>
@Bean
@ConditionalOnMissingBean
ParameterNamesModule parameterNamesModule() {
return new ParameterNamesModule(JsonCreator.Mode.DEFAULT);
}
해당 구현체의 setUpModule() 메서드로 이동해본다.
<ParameterNamesModule.setUpModule()>
@Override
public void setupModule(SetupContext context) {
super.setupModule(context);
// Go! 여기서 ParameterNamesAnnotationIntrospector 등록
context.insertAnnotationIntrospector(new ParameterNamesAnnotationIntrospector(creatorBinding, new ParameterExtractor()));
}
찾았다. 여기서 ParameterNamesAnnotationIntrospector를 등록한다.
정리
1. Jackson2ObjectMapperBuilder을 통해 ObjectMapper를 생성하면 리플렉션을 통해 생성자 파라미터를 찾을 수 있는 ParameterNamesAnnotationIntrospector를 등록한다.
@Override
public void setupModule(SetupContext context) {
super.setupModule(context);
// Go! 여기서 ParameterNamesAnnotationIntrospector 등록
context.insertAnnotationIntrospector(new ParameterNamesAnnotationIntrospector(creatorBinding, new ParameterExtractor()));
}
2. ParameterNamesAnnotationIntrospector를 통해 암시적 매개변수 목록인 PotentialCreator._implicitParamNames을 세팅한다.
public PotentialCreator introspectParamNames(MapperConfig<?> config)
{
// ...
final AnnotationIntrospector intr = config.getAnnotationIntrospector();
for (int i = 0; i < paramCount; ++i) {
AnnotatedParameter param = _creator.getParameter(i);
// Go!
String rawImplName = intr.findImplicitPropertyName(param);
if (rawImplName != null && !rawImplName.isEmpty()) {
_implicitParamNames[i] = PropertyName.construct(rawImplName);
}
// ...
}
return this;
}
3. _implicitParamNames가 존재하면 PotentialCreators.propertiesBased를 세팅한다.
private boolean _addImplicitConstructor(PotentialCreators collector,
List<PotentialCreator> ctors, Map<String, POJOPropertyBuilder> props)
{
// ...
if (ctor.paramCount() != 1) {
if (!ctor.hasNameOrInjectForAllParams(_config)) {
return false;
}
} else {
if ((_annotationIntrospector != null)
&& _annotationIntrospector.findInjectableValue(ctor.param(0)) != null) {
} else {
// ...
if (!ctorDetector.singleArgCreatorDefaultsToProperties()) {
POJOPropertyBuilder prop = props.get(ctor.implicitNameSimple(0));
if ((prop == null) || !prop.anyVisible() || prop.anyIgnorals()) {
return false;
}
}
}
}
ctors.remove(0);
// Go! propertiesBased 세팅하는 부분
collector.setPropertiesBased(_config, ctor, "implicit");
return true;
}
4. propertiesBased가 존재하면 property-based 역직렬화 사용하는 메서드를 호출할 수 있다.
protected ValueInstantiator _constructDefaultValueInstantiator(DeserializationContext ctxt,
BeanDescription beanDesc)
throws JsonMappingException
{
// ...
final PotentialCreators potentialCreators = beanDesc.getPotentialCreators();
// ...
if (potentialCreators.hasPropertiesBased()) {
PotentialCreator primaryPropsBased = potentialCreators.propertiesBased;
if (primaryPropsBased.paramCount() == 0) {
creators.setDefaultCreator(primaryPropsBased.creator());
} else {
// property-based 역직렬화 사용하는 메서드
_addSelectedPropertiesBasedCreator(ctxt, beanDesc, creators,
CreatorCandidate.construct(config.getAnnotationIntrospector(),
primaryPropsBased.creator(), primaryPropsBased.propertyDefs()));
}
}
// ...
}
5. 생성자 방식을 사용하면 위 과정을 수행하지 않는다.
6. ObjectMapper를 사용할 때는, 스프링이 제공하는 JacksonAutoConfiguration의 기본 빈을 사용하거나, 직접 정의한다고 해도 Jackson2ObjectMapperBuilder.build() 를 통해 생성하는 것이 역직렬화 안정성을 높여준다.
2. configure
(계속 작성중...)
초안
1. 모듈 등록
2. ProblemDetail 직렬화/역직렬화 정책
3. 기본 피처(default_view_inclusion, fail_on_unknown_properties)
3. 기본 Feature 값들
(계속 작성중...)
초안
1. 기본 직렬화 피처 -> objectmapper 세팅
2. 빌더에 applicationContext 세팅 -> objectmapper handerinitialor 세팅
3. 빌더에 기본 모듈 지정 -> objectmapper 세팅
'java > spring' 카테고리의 다른 글
[Spring] jackson 역직렬화, 필드가 1개일 때 HttpMessageNotReadableException (4) | 2025.02.09 |
---|---|
[Spring] Pointcut 유형에 따라 Proxy 생성 방식이 달라진다? (CGLIB or JDK Proxy) (0) | 2024.11.02 |
[Spring] junit test에서 lombok 사용하는 방법 (0) | 2024.10.16 |
[Spring] ControllerAdvice - 예외 처리 (2) | 2023.07.16 |
[Spring] @SpringBootTest vs @DataJpaTest (0) | 2023.07.15 |