ERROR 7956 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
[Request processing failed: org.springframework.http.converter.HttpMessageConversionException: Type definition error:
[simple type, class com.project.articlecomment.dto.ArticleForm]] with root cause
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of `com.project.articlecomment.dto.ArticleForm`
(no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 2, column: 5]
위와 같은 에러로 인해 호기심 으로
1시간 디버깅 했던 걸한번 정리 해봤음.
- 호기심이 좀 많은 편이고 아무래도
눈으로 직접 보는 게 제일 직관적으로 이해할 수 있다고 생각함.- 역시 눈으로 직접 보니깐 흥미롭고 나름 재미있었음.

spring-boot-starter-web에 들어있음.public class ObjectMapper extends ObjectCodec implements Versioned, Serializable {
private static final long serialVersionUID = 2L;
protected static final AnnotationIntrospector DEFAULT_ANNOTATION_INTROSPECTOR = new JacksonAnnotationIntrospector();
protected static final BaseSettings DEFAULT_BASE;
protected final JsonFactory _jsonFactory;
protected TypeFactory _typeFactory;
protected InjectableValues _injectableValues;
protected SubtypeResolver _subtypeResolver;
protected final ConfigOverrides _configOverrides;
protected final CoercionConfigs _coercionConfigs;
protected SimpleMixInResolver _mixIns;
protected SerializationConfig _serializationConfig;
protected DefaultSerializerProvider _serializerProvider;
protected SerializerFactory _serializerFactory;
protected DeserializationConfig _deserializationConfig;
protected DefaultDeserializationContext _deserializationContext;
protected Set<Object> _registeredModuleTypes;
protected final ConcurrentHashMap<JavaType, JsonDeserializer<Object>> _rootDeserializers;
// 이하 생략.
ObjectMapper
JSON 데이터 -> Java 객체
역직렬화. (Deserialization)Java 객체 -> JSON 데이터
직렬화(Serialization)readValue(String content, Class<T> valueType)
writeValueAsString(Object value)
readTree(String content)
JSON 필드와 Java 필드의 이름이 다를 경우
@JsonProperty어노테이션을 사용

public final class AnnotatedConstructor extends AnnotatedWithParams {
private static final long serialVersionUID = 1L;
protected final Constructor<?> _constructor;
protected Serialization _serialization;
// 코드 생략.
public final Object call() throws Exception {
return this._constructor.newInstance((Object[])null);
}
// 코드 생략.
}
return this._constructor.newInstance((Object[])null);_constructor newInstance 가 있음.this._constructor는 Constructor<?> 타입의 변수로, 리플렉션을 통해 특정 클래스의 생성자 정보를 담고 있음.Constructor<?> 클래스는 자바의 리플렉션 API에서 특정 클래스의 생성자를 나타내는 객체로, 해당 생성자를 호출할 수 있는 메서드를 제공.newInstance((Object[]) null)은 기본 생성자를 호출해 객체를 생성함.Constructor 객체의 newInstance() 메서드는 해당 생성자를 사용해서 새로운 인스턴스를 동적으로 생성하는 메서드.Reflection? 아 그래서 기본 생성자가 필요했구나 싶었음.@JacksonStdImpl
public class StdValueInstantiator extends ValueInstantiator implements Serializable {
private static final long serialVersionUID = 1L;
protected final String _valueTypeDesc;
protected final Class<?> _valueClass;
protected AnnotatedWithParams _defaultCreator;
protected AnnotatedWithParams _withArgsCreator;
protected SettableBeanProperty[] _constructorArguments;
protected JavaType _delegateType;
protected AnnotatedWithParams _delegateCreator;
protected SettableBeanProperty[] _delegateArguments;
protected JavaType _arrayDelegateType;
protected AnnotatedWithParams _arrayDelegateCreator;
protected SettableBeanProperty[] _arrayDelegateArguments;
protected AnnotatedWithParams _fromStringCreator;
protected AnnotatedWithParams _fromIntCreator;
protected AnnotatedWithParams _fromLongCreator;
protected AnnotatedWithParams _fromBigIntegerCreator;
protected AnnotatedWithParams _fromDoubleCreator;
protected AnnotatedWithParams _fromBigDecimalCreator;
protected AnnotatedWithParams _fromBooleanCreator;
public Object createUsingDefault(DeserializationContext ctxt) throws IOException {
if (this._defaultCreator == null) {
return super.createUsingDefault(ctxt);
} else {
try {
return this._defaultCreator.call();
} catch (Exception var3) {
return ctxt.handleInstantiationProblem(this._valueClass, (Object)null, this.rewrapCtorProblem(ctxt, var3));
}
}
}
// 이하 코드 생략.
return this._defaultCreator.call();
if (this._defaultCreator == null)을 보고 혹시...?public class BeanDeserializer extends BeanDeserializerBase implements Serializable {
private static final long serialVersionUID = 1L;
protected transient Exception _nullFromCreator;
private transient volatile NameTransformer _currentlyTransforming;
public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt) throws IOException {
if (this._objectIdReader != null && this._objectIdReader.maySerializeAsObject() && p.hasTokenId(5) && this._objectIdReader.isValidReferencePropertyName(p.currentName(), p)) {
return this.deserializeFromObjectId(p, ctxt);
} else {
Object bean;
if (this._nonStandardCreation) {
if (this._unwrappedPropertyHandler != null) {
return this.deserializeWithUnwrapped(p, ctxt);
} else if (this._externalTypeIdHandler != null) {
return this.deserializeWithExternalTypeId(p, ctxt);
} else {
bean = this.deserializeFromObjectUsingNonDefault(p, ctxt);
return bean;
}
} else {
bean = this._valueInstantiator.createUsingDefault(ctxt);
p.setCurrentValue(bean);
if (p.canReadObjectId()) {
Object id = p.getObjectId();
if (id != null) {
this._handleTypedObjectId(p, ctxt, bean, id);
}
}
if (this._injectables != null) {
this.injectValues(ctxt, bean);
}
if (this._needViewProcesing) {
Class<?> view = ctxt.getActiveView();
if (view != null) {
return this.deserializeWithView(p, ctxt, bean, view);
}
}
if (p.hasTokenId(5)) {
String propName = p.currentName();
do {
p.nextToken();
SettableBeanProperty prop = this._beanProperties.find(propName);
if (prop != null) {
try {
prop.deserializeAndSet(p, ctxt, bean);
} catch (Exception var7) {
this.wrapAndThrow(var7, bean, propName, ctxt);
}
} else {
this.handleUnknownVanilla(p, ctxt, bean, propName);
}
} while((propName = p.nextFieldName()) != null);
}
return bean;
}
}
}
bean = this._valueInstantiator.createUsingDefault(ctxt);
p.setCurrentValue(bean)는 현재 JSON 파서(JsonParser)의 컨텍스트에 생성된 Java 객체를 설정하는 작업.현재 처리 중인 JSON 데이터가 이 객체(bean)와 관련 있다 라는 정보를 저장public class BeanDeserializer extends BeanDeserializerBase implements Serializable {
private static final long serialVersionUID = 1L;
protected transient Exception _nullFromCreator;
private transient volatile NameTransformer _currentlyTransforming;
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
if (p.isExpectedStartObjectToken()) {
if (this._vanillaProcessing) {
return this.vanillaDeserialize(p, ctxt, p.nextToken());
} else {
p.nextToken();
return this._objectIdReader != null ? this.deserializeWithObjectId(p, ctxt) : this.deserializeFromObject(p, ctxt);
}
} else {
return this._deserializeOther(p, ctxt, p.currentToken());
}
}
public abstract class DefaultDeserializationContext extends DeserializationContext implements Serializable {
private static final long serialVersionUID = 1L;
protected transient LinkedHashMap<ObjectIdGenerator.IdKey, ReadableObjectId> _objectIds;
private List<ObjectIdResolver> _objectIdResolvers;
public Object readRootValue(JsonParser p, JavaType valueType, JsonDeserializer<Object> deser, Object valueToUpdate) throws IOException {
if (this._config.useRootWrapping()) {
return this._unwrapAndDeserialize(p, valueType, deser, valueToUpdate);
} else {
return valueToUpdate == null ? deser.deserialize(p, this) : deser.deserialize(p, this, valueToUpdate);
}
}
return valueToUpdate == null ? deser.deserialize(p, this) : deser.deserialize(p, this, valueToUpdate);valueToUpdate가 null인 경우valueToUpdate가 null이 아닌 경우public class ObjectReader extends ObjectCodec implements Versioned, Serializable {
private static final long serialVersionUID = 2L;
protected final DeserializationConfig _config;
protected final DefaultDeserializationContext _context;
protected final JsonFactory _parserFactory;
protected final boolean _unwrapRoot;
private final TokenFilter _filter;
protected final JavaType _valueType;
protected final JsonDeserializer<Object> _rootDeserializer;
protected final Object _valueToUpdate;
protected final FormatSchema _schema;
protected final InjectableValues _injectableValues;
protected final DataFormatReaders _dataFormatReaders;
protected final ConcurrentHashMap<JavaType, JsonDeserializer<Object>> _rootDeserializers;
protected transient JavaType _jsonNodeType;
protected Object _bindAndClose(JsonParser p0) throws IOException {
JsonParser p = p0;
Throwable var3 = null;
Object var7;
try {
DefaultDeserializationContext ctxt = this.createDeserializationContext(p);
JsonToken t = this._initForReading(ctxt, p);
Object result;
if (t == JsonToken.VALUE_NULL) {
if (this._valueToUpdate == null) {
result = this._findRootDeserializer(ctxt).getNullValue(ctxt);
} else {
result = this._valueToUpdate;
}
} else if (t != JsonToken.END_ARRAY && t != JsonToken.END_OBJECT) {
result = ctxt.readRootValue(p, this._valueType, this._findRootDeserializer(ctxt), this._valueToUpdate);
} else {
result = this._valueToUpdate;
}
if (this._config.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)) {
this._verifyNoTrailingTokens(p, ctxt, this._valueType);
}
var7 = result;
} catch (Throwable var16) {
var3 = var16;
throw var16;
} finally {
if (p0 != null) {
if (var3 != null) {
try {
p.close();
} catch (Throwable var15) {
var3.addSuppressed(var15);
}
} else {
p0.close();
}
}
}
return var7;
}
_bindAndClose)는 JSON 데이터를 객체로 변환한 뒤 JSON 파서(Parser)를 끝마침.readRootValue가 JSON 데이터를 읽고 역직렬화기를 사용해 Java 객체로 변환.
public class ObjectReader extends ObjectCodec implements Versioned, Serializable {
private static final long serialVersionUID = 2L;
protected final DeserializationConfig _config;
protected final DefaultDeserializationContext _context;
protected final JsonFactory _parserFactory;
protected final boolean _unwrapRoot;
private final TokenFilter _filter;
protected final JavaType _valueType;
protected final JsonDeserializer<Object> _rootDeserializer;
protected final Object _valueToUpdate;
protected final FormatSchema _schema;
protected final InjectableValues _injectableValues;
protected final DataFormatReaders _dataFormatReaders;
protected final ConcurrentHashMap<JavaType, JsonDeserializer<Object>> _rootDeserializers;
protected transient JavaType _jsonNodeType;
public <T> T readValue(InputStream src) throws IOException {
return this._dataFormatReaders != null ? this._detectBindAndClose(this._dataFormatReaders.findFormat(src), false) : this._bindAndClose(this._considerFilter(this.createParser(src), false));
}
public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
private static final Map<String, JsonEncoding> ENCODINGS = CollectionUtils.newHashMap(JsonEncoding.values().length);
protected ObjectMapper defaultObjectMapper;
@Nullable
private Map<Class<?>, Map<MediaType, ObjectMapper>> objectMapperRegistrations;
@Nullable
private Boolean prettyPrint;
@Nullable
private final PrettyPrinter ssePrettyPrinter;
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
MediaType contentType = inputMessage.getHeaders().getContentType();
Charset charset = this.getCharset(contentType);
ObjectMapper objectMapper = this.selectObjectMapper(javaType.getRawClass(), contentType);
Assert.state(objectMapper != null, () -> {
return "No ObjectMapper for " + javaType;
});
boolean isUnicode = ENCODINGS.containsKey(charset.name()) || "UTF-16".equals(charset.name()) || "UTF-32".equals(charset.name());
try {
InputStream inputStream = StreamUtils.nonClosing(inputMessage.getBody());
if (inputMessage instanceof MappingJacksonInputMessage mappingJacksonInputMessage) {
Class<?> deserializationView = mappingJacksonInputMessage.getDeserializationView();
if (deserializationView != null) {
ObjectReader objectReader = objectMapper.readerWithView(deserializationView).forType(javaType);
objectReader = this.customizeReader(objectReader, javaType);
if (isUnicode) {
return objectReader.readValue(inputStream);
}
Reader reader = new InputStreamReader(inputStream, charset);
return objectReader.readValue(reader);
}
}
ObjectReader objectReader = objectMapper.reader().forType(javaType);
objectReader = this.customizeReader(objectReader, javaType);
if (isUnicode) {
return objectReader.readValue(inputStream);
} else {
Reader reader = new InputStreamReader(inputStream, charset);
return objectReader.readValue(reader);
}
} catch (InvalidDefinitionException var12) {
throw new HttpMessageConversionException("Type definition error: " + var12.getType(), var12);
} catch (JsonProcessingException var13) {
throw new HttpMessageNotReadableException("JSON parse error: " + var13.getOriginalMessage(), var13, inputMessage);
}
}
readJavaType 메서드는 HTTP 요청 본문(Body)에 포함된 JSON 데이터를 읽고 이를 Java 객체로 변환하는 작업을 수행
return objectReader.readValue(inputStream);

public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
private static final Map<String, JsonEncoding> ENCODINGS = CollectionUtils.newHashMap(JsonEncoding.values().length);
protected ObjectMapper defaultObjectMapper;
@Nullable
private Map<Class<?>, Map<MediaType, ObjectMapper>> objectMapperRegistrations;
@Nullable
private Boolean prettyPrint;
@Nullable
private final PrettyPrinter ssePrettyPrinter;
public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
JavaType javaType = this.getJavaType(type, contextClass);
return this.readJavaType(javaType, inputMessage);
}
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
private static final Set<HttpMethod> SUPPORTED_METHODS;
private static final Object NO_VALUE;
protected final Log logger;
protected final List<HttpMessageConverter<?>> messageConverters;
private final RequestResponseBodyAdviceChain advice;
@Nullable
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
Class<?> contextClass = parameter.getContainingClass();
Class var10000;
if (targetType instanceof Class clazz) {
var10000 = clazz;
} else {
var10000 = null;
}
Class<T> targetClass = var10000;
if (targetClass == null) {
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
targetClass = resolvableType.resolve();
}
boolean noContentType = false;
MediaType contentType;
try {
contentType = inputMessage.getHeaders().getContentType();
} catch (InvalidMediaTypeException var20) {
throw new HttpMediaTypeNotSupportedException(var20.getMessage(), this.getSupportedMediaTypes(targetClass != null ? targetClass : Object.class));
}
if (contentType == null) {
noContentType = true;
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
HttpMethod var27;
if (inputMessage instanceof HttpRequest httpRequest) {
var27 = httpRequest.getMethod();
} else {
var27 = null;
}
HttpMethod httpMethod = var27;
Object body = NO_VALUE;
EmptyBodyCheckingHttpInputMessage message = null;
try {
message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
Iterator var11 = this.messageConverters.iterator();
while(var11.hasNext()) {
HttpMessageConverter<?> converter = (HttpMessageConverter)var11.next();
Class<HttpMessageConverter<?>> converterType = converter.getClass();
GenericHttpMessageConverter var28;
if (converter instanceof GenericHttpMessageConverter ghmc) {
var28 = ghmc;
} else {
var28 = null;
}
GenericHttpMessageConverter<?> genericConverter = var28;
if (genericConverter != null) {
if (!genericConverter.canRead(targetType, contextClass, contentType)) {
continue;
}
} else if (targetClass == null || !converter.canRead(targetClass, contentType)) {
continue;
}
if (message.hasBody()) {
HttpInputMessage msgToUse = this.getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
body = genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) : converter.read(targetClass, msgToUse);
body = this.getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
} else {
body = this.getAdvice().handleEmptyBody((Object)null, message, parameter, targetType, converterType);
}
break;
}
} catch (IOException var21) {
throw new HttpMessageNotReadableException("I/O error while reading input message", var21, inputMessage);
} finally {
if (message != null && message.hasBody()) {
this.closeStreamIfNecessary(message.getBody());
}
}
if (body != NO_VALUE) {
LogFormatUtils.traceDebug(this.logger, (traceOn) -> {
String formatted = LogFormatUtils.formatValue(body, !traceOn);
return "Read \"" + contentType + "\" to [" + formatted + "]";
});
return body;
} else if (httpMethod != null && SUPPORTED_METHODS.contains(httpMethod) && (!noContentType || message.hasBody())) {
throw new HttpMediaTypeNotSupportedException(contentType, this.getSupportedMediaTypes(targetClass != null ? targetClass : Object.class), httpMethod);
} else {
return null;
}
}
if (message.hasBody())
HttpInputMessage msgToUse = this.getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
body = this.getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter, Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
HttpServletRequest servletRequest = (HttpServletRequest)webRequest.getNativeRequest(HttpServletRequest.class);
Assert.state(servletRequest != null, "No HttpServletRequest");
ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
Object arg = this.readWithMessageConverters(inputMessage, parameter, paramType);
if (arg == null && this.checkRequired(parameter)) {
throw new HttpMessageNotReadableException("Required request body is missing: " + parameter.getExecutable().toGenericString(), inputMessage);
} else {
return arg;
}
}
Object arg = this.readWithMessageConverters(inputMessage, parameter, paramType);this.readWithMessageConverters 메서드는 ServletServerHttpRequest 객체인 inputMessage를 사용해 요청 Body를 읽음.Ex @RequestBody가 적용된 파라미터의 타입을 확인하고, 이를 바탕으로 본문을 적절히 변환.

public class InvocableHandlerMethod extends HandlerMethod {
private static final Object[] EMPTY_ARGS = new Object[0];
private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();
private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
@Nullable
private WebDataBinderFactory dataBinderFactory;
public InvocableHandlerMethod(HandlerMethod handlerMethod) {
super(handlerMethod);
}
public InvocableHandlerMethod(Object bean, Method method) {
super(bean, method);
}
protected InvocableHandlerMethod(Object bean, Method method, @Nullable MessageSource messageSource) {
super(bean, method, messageSource);
}
public InvocableHandlerMethod(Object bean, String methodName, Class<?>... parameterTypes) throws NoSuchMethodException {
super(bean, methodName, parameterTypes);
}
public void setHandlerMethodArgumentResolvers(HandlerMethodArgumentResolverComposite argumentResolvers) {
this.resolvers = argumentResolvers;
}
public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
this.parameterNameDiscoverer = parameterNameDiscoverer;
}
public void setDataBinderFactory(WebDataBinderFactory dataBinderFactory) {
this.dataBinderFactory = dataBinderFactory;
}
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
MethodParameter[] parameters = this.getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
} else {
Object[] args = new Object[parameters.length];
for(int i = 0; i < parameters.length; ++i) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] == null) {
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
} catch (Exception var10) {
if (logger.isDebugEnabled()) {
String exMsg = var10.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw var10;
}
}
}
return args;
}
}





@Setter or @Getter 둘 중 하나는 있어야 필드에 값이 제대로 매핑 된다는 것도 알았음.@Setter보다 @Getter를 쓰는 게 나을듯.