[Spring] Why - @RequestBody와 역직렬화 그리고 기본 생성자에 대해서.

하쮸·2024년 11월 27일

Error, Why, What, How

목록 보기
5/68
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시간 디버깅 했던 걸 한번 정리 해봤음.

  • 호기심이 좀 많은 편이고 아무래도 눈으로 직접 보는 게 제일 직관적으로 이해할 수 있다고 생각함.
  • 역시 눈으로 직접 보니깐 흥미롭고 나름 재미있었음.

1. 간단히 알고 가야할 개념.


1-1. @RequestBody

  • @RequestBody는 주로 POST, PUT 요청에서 사용되며, HTTP 요청의 본문(Body)에 실려있는 데이터를 Java 객체로 매핑할 때 사용됨.
    • 즉, 클라이언트가 보낸 JSON과 같은 데이터를 파라미터로 매핑하기 위해서.
  • HTTP 요청 본문(Body)에 담긴 JSON이나 XML과 같은 데이터를 Java 객체로 변환하기 위해서는 @RequestBody 어노테이션이 반드시 필요.

1-2. 직렬화, 역직렬화.

  • 직렬화.
    • 객체를 바이트 스트림으로 변환하여 저장하거나 네트워크를 통해 전송할 수 있도록 만드는 과정.
    • 즉, 서버 -> 클라이언트로 데이터를 보낼 때 객체를 JSON으로 직렬화.
  • 역직렬화.
    • 직렬화된 데이터를 원래의 객체로 변환하는 과정.
    • 즉, 클라이언트 -> 서버로 보낸 데이터를 객체로 변환해서 사용하기 위해서.

1-2-1. Jackson

  • Jackson 라이브러리는 JSON 데이터를 Java 객체로 변환하고, 그 반대의 작업도 수행할 수 있는 라이브러리.
    • ObjectMapper를 이용해서 직렬화와 역직렬화를 수행

  • spring-boot-starter-web에 들어있음.

1-3. ObjectMapper

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

    • Jackson 라이브러리에서 제공하는 클래스 중 하나.
    • Java 객체와 JSON 간의 변환을 담당.
      • 이를 통해 JSON 데이터를 Java 객체로 변환하거나, Java 객체를 JSON 문자열로 변환할 수 있음.
  • JSON 데이터 -> Java 객체

    • 역직렬화. (Deserialization)
    • JSON 데이터를 Java 객체로 변환.
  • Java 객체 -> JSON 데이터

    • Java 객체를 JSON 문자열로 변환.
    • 직렬화(Serialization)
  • readValue(String content, Class<T> valueType)

    • JSON 문자열을 Java 객체로 변환.
  • writeValueAsString(Object value)

    • Java 객체를 JSON 문자열로 변환.
  • readTree(String content)

    • JSON 문자열을 JsonNode 객체로 변환하여 트리 형태로 데이터 접근 가능.
  • JSON 필드와 Java 필드의 이름이 다를 경우

    • JSON 필드와 객체 필드가 다르면, Jackson은 기본적으로 매핑하지 못함.
      • 이를 해결하려면 @JsonProperty어노테이션을 사용

1-4. Reflection.


2. @RequestBody에 왜 기본 생성자가 필요한가?

  • DTO클래스를 토대로 REST Controller의 메서드로 값을 보내봤음.

2-1. 기본 생성자

  1. 가장 먼저 호출된 것은 기본 생성자.

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);
    }
    // 코드 생략.
}

2-2. this._constructor.newInstance(), Reflection

  1. return this._constructor.newInstance((Object[])null);
    • call()메서드가 호출되었는데 중괄호 안에 _constructor newInstance 가 있음.
    • 이 메서드는 new 연산자 없이 Reflection API를 이용해 기본 생성자를 호출하여 객체를 생성하는 역할을 함.
      • this._constructorConstructor<?> 타입의 변수로, 리플렉션을 통해 특정 클래스의 생성자 정보를 담고 있음.
        • Constructor<?> 클래스는 자바의 리플렉션 API에서 특정 클래스의 생성자를 나타내는 객체로, 해당 생성자를 호출할 수 있는 메서드를 제공.
      • newInstance((Object[]) null)은 기본 생성자를 호출해 객체를 생성함.
        • Constructor 객체의 newInstance() 메서드는 해당 생성자를 사용해서 새로운 인스턴스를 동적으로 생성하는 메서드.
    • 즉, 이 메서드는 기본 생성자를 사용해 새로운 객체를 동적으로 생성.
    • 생성된 객체에 JSON 데이터가 이후에 리플렉션을 통해 필드에 주입됨.
  • Jackson은 필드에 값을 할당할 때, setter 메서드를 호출하지 않고 리플렉션을 통해 필드에 직접 값을 주입하고, 이 과정에서 private 필드도 접근할 수 있음.
  • Reflection? 아 그래서 기본 생성자가 필요했구나 싶었음.

2-3. this._defaultCreator.call()

@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));
            }
        }
    }
// 이하 코드 생략.
  1. return this._defaultCreator.call();
    • 해당 메서드는 Jackson 라이브러리에서 역직렬화(Deserialization) 중에 기본 생성자를 이용해 객체를 생성하는 역할.

  • 상단에 if (this._defaultCreator == null)을 보고 혹시...?
    쟤를 타고 계속 들어가면 기본 생성자가 없을 때 발생하는 에러가 있나 싶음.

2-4. deserializeFromObject()

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;
            }
        }
    }
  • 메서드 이름만봐도, 또는 클래스 이름만 봐도 역직렬화와 관련되어있다는 걸 유추할 수 있음.
    • Jackson에서 JSON 데이터를 Java 객체로 역직렬화하는 데 사용.
  1. bean = this._valueInstantiator.createUsingDefault(ctxt);
    • 기본 생성자를 호출해 새로운 빈(객체)을 생성함.

  • p.setCurrentValue(bean)는 현재 JSON 파서(JsonParser)의 컨텍스트에 생성된 Java 객체를 설정하는 작업.
    • 즉, 현재 처리 중인 JSON 데이터가 이 객체(bean)와 관련 있다 라는 정보를 저장
    • 이후로 JSON 필드(title, content)를 읽으면서 bean의 title, content 필드에 값을 채움.

2-5. deserialize()

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());
        }
    }
  • deserialize는 JSON 데이터를 읽어 Java 객체로 변환하는 핵심 메서드.
    • JSON의 시작 토큰과 상태를 기반으로 어떤 방식으로 데이터를 처리할지 결정함.

2-6. readRootValue()

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);
        }
    }
  • readRootValue는 JSON 데이터를 파싱하여 객체로 역직렬화하는 역할.
  • return valueToUpdate == null ? deser.deserialize(p, this) : deser.deserialize(p, this, valueToUpdate);
    • valueToUpdate가 null인 경우
      • 새로운 객체를 생성하며 역직렬화 함.
      • deser.deserialize(p, this)는 JSON 데이터를 새 객체로 변환함.
      • 즉, JSON에 맞는 Java 객체를 처음부터 새로 생성.
    • valueToUpdate가 null이 아닌 경우
      • 기존 객체(valueToUpdate)를 업데이트하며 역직렬화합니다.
      • deser.deserialize(p, this, valueToUpdate)는 기존 객체를 재활용하고 JSON 데이터를 덮어씀.

2-7. _bindAndClose()

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 객체로 변환.

  • 이렇게 값이 들어왔음

2-7. readValue()

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));
    }
  • readValue(InputStream src)는 입력 스트림(InputStream)으로부터 JSON 데이터를 읽어 Java 객체로 변환(역직렬화)
    • 즉, JSON 데이터를 읽고 Java 객체로 변환하는 핵심 작업을 수행

2-8. readJavaType()

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);

    • 입력 스트림에서 JSON 데이터를 읽어, 해당 데이터를 지정된 Java 타입의 객체로 변환하는 역할

  • 헤더에 어떤 것들이 들어가 있는지도 보여짐.

2-9. read()

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);
    }
  • 이 메서드는 HTTP 요청에 실려있는 Body 데이터를 읽고, 이를 주어진 타입에 맞는 Java 객체로 변환하는 작업을 함.

2-10. readWithMessageConverters()

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())

    • HTTP 메시지가 Body를 가지고 있는 지 확인
  • HttpInputMessage msgToUse = this.getAdvice().beforeBodyRead(message, parameter, targetType, converterType);

    • beforeBodyRead 메서드는 Body를 읽기 전에 호출.
      • 이 메서드는 Body을 읽기 전에 추가적인 처리가 필요할 경우, 예를 들어, 로깅, 압축 해제, 헤더 검증 등을 위해 사용됨.
  • body = this.getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);

    • afterBodyRead 메서드는 본문(Body)을 읽고 변환한 후 호출.
    • 변환된 데이터를 후처리할 수 있는 단계.


2-11. readWithMessageConverters()

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;
        }
    }
  • HttpServletRequest로부터 HTTP 요청 Body부분을 MessageConverters를 사용하여 읽고 변환하는 역할.
  • Object arg = this.readWithMessageConverters(inputMessage, parameter, paramType);
    • this.readWithMessageConverters 메서드는 ServletServerHttpRequest 객체인 inputMessage를 사용해 요청 Body를 읽음.
    • parameter: 이 파라미터는 메서드의 파라미터 정보를 가지고 있음. Body의 데이터가 해당 파라미터 타입에 맞게 변환되도록 도움.
    • paramType: 메서드 파라미터의 타입 정보. 변환될 대상 타입을 알려주는 역할.
      Ex @RequestBody가 적용된 파라미터의 타입을 확인하고, 이를 바탕으로 본문을 적절히 변환.
  • 결국 이 코드에서 Body를 읽고 변환하는 과정은 inputMessage와 parameter, paramType을 기반으로 처리되고 적절한 MessageConverters가 선택되어 데이터를 원하는 객체 타입으로 변환시킴.


2-12. getMethodArgumentValues()

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;
        }
    }
  • HTTP 요청을 처리할 메서드의 파라미터 값을 설정하는 역할.
  • NativeWebRequest에서 필요한 데이터를 추출하고, 각 파라미터에 대해 적합한 값을 찾아서 배열로 반환.


2-13. 확인.


3. Setter, Getter 테스트.



  • 3가지 테스트를 통해 기본 생성자가 있다하더라도 @Setter or @Getter 둘 중 하나는 있어야 필드에 값이 제대로 매핑 된다는 것도 알았음.
  • 따라서 캡슐화에 영향을 줄 수 있는 @Setter보다 @Getter를 쓰는 게 나을듯.

4. Ref

profile
Every cloud has a silver lining.

0개의 댓글