Develop/Spring+JPA

[Spring] @RequestBody가 빈 생성자가 필요한 이유 (hint. ObjectMapper)

연로그 2022. 5. 1. 17:59
반응형

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `클래스명` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator) 에러

 

Spring에서 @RequestBody를 통해 데이터를 가져오는 중에 위와 같은 에러를 만났다.

구글링을 해보니 하나같이 빈 생성자를 생성하라고 하는데 대체 왜? 라는 생각이 들었다.

 

 

Spring은 Http Message Body를 읽기 위해
HttpMessageConverter를 사용한다.

 

 

클라이언트로부터 값을 받으면 여러 Converter 중에서 해당 값을 읽을 수 있는 Converter를 찾는다.

읽을 수 있는 컨버터를 찾으면 read() 메서드를 통해 값을 읽고 원하는 Object로 변환한다.

 

참고로 Spring에서 JSON의 형변환은 Jackson2HttpMessageConverter가 담당한다.

Jackson2HttpMessageConverter의 read() 메서드를 살펴보면 ObjectMapper를 통해 값을 변환시킨다.

 

+ json이 뭔지 모른다면? 👉 https://yeonyeon.tistory.com/48

 

대략 이런 흐름으로 간다. 아래에서 코드와 다시 살펴보자.

 


👨‍💻 코드로 과정 살펴보기

 

1. HttpMessageConverter 목록 가져오기

아래는 @RequestBody에 설명된 주석의 일부를 가져왔다.

The body of the request is passed through an HttpMessageConverter to resolve the method argument depending on the content type of the request.

 

좀 더 구체적으로 들어가보자면 @RequestBody나 @ResponseBody를 처리를 도와주는 RequestResponseBodyMethodProcessor라는 클래스가 존재한다.

public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
    // ...
    @Nullable
    protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
            Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
        // ...
    }
}

 

해당 메서드에서 converter 목록을 가져와 사용 가능한 converter인지 선택하는 로직이 존재한다.

 

2. MappingJackson2HttpMessageConverter 선택

여러가지 HttpMessageConverter 중에서 적합한 Converter를 찾는다.

Spring은 JSON의 경우 Jackson2HttpMessageConverter를 선택한다.

 

3. read() 호출

read()의 로직은 MappingJackson2HttpMessageConverter가 AbstractJackson2HttpMessageConverter에서 상속받았다.

jackson의 ObjectMapper를 호출해 값을 반환하는 것을 볼 수 있다.

@Override
public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)
        throws IOException, HttpMessageNotReadableException {

    JavaType javaType = getJavaType(type, contextClass);
    return readJavaType(javaType, inputMessage);
}

private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
    ObjectMapper objectMapper = selectObjectMapper(javaType.getRawClass(), contentType);
    
    // 실제 로직은 훨씬훨씬 복잡하지만 return시 ObjectMapper를 사용한다는 것만 알아두자.
    
    return objectMapper.readValue(inputStream, javaType);
}

 

4. ObjectMapper가 JSON -> Object로 변환

public <T> T readValue(InputStream src, JavaType valueType)
    throws IOException, StreamReadException, DatabindException
{
    _assertNotNull("src", src);
    return (T) _readMapAndClose(_jsonFactory.createParser(src), valueType);
} 

protected Object _readMapAndClose(JsonParser p0, JavaType valueType)
    throws IOException
{
    // Object 반환
}

 

 


💡 ObjectMapper의 변환 과정

 

JSON을 인식 후 Object로의 변환 과정은 아래와 같다.

  1. Object 생성: 대부분 기본 생성자 이용. (예외: Property 사용 예제)
  2. Object 필드 인식: Setter 또는 Getter 이용
  3. Object 필드에 값 넣기: Reflection 이용 (setter 이용 X)

 

코드를 뜯어보며 좀 더 자세히 살펴보자.

 

0. 테스트 코드 작성

위에서 ObjectMapper의 readValue(InputStream, JavaType)이 호출되는 것을 확인했었다.

이제 테스트 코드를 작성하고 하나하나 디버깅하며 따라가보려 한다.

public class JsonTest {
    @Test
    void test() {
        ObjectMapper mapper = new ObjectMapper();
        String json = "{\"name\":\"yeonlog\"}";
        InputStream inputStream = new ByteArrayInputStream(json.getBytes());

        try {
            User user = mapper.readValue(inputStream, User.class);
            System.out.println(user);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
public class User {

    private String name;

    private User() {
    }

    public String getName() {
        return name;
    }
}

 

1. Object 생성

readValue()의 로직을 따라가다보면 아래 BeanDeserializer의 deserialize()를 호출한다.

deserialize()에서 vanillaDeserialize() 호출
StdValueInstantiator 클래스의 createUsingDefault() 호출
AnnotatedConstructor의 call() 호출
User의 default constructor 호출

 

2. Object 필드에 값 넣기

다시 BeanDeserializer의 vanillaDeserialize() 메서드로 돌아오자.

 

우선 JSON에서 key를 가져온다.

 

JSON에서 value를 가져온 뒤, 필드에 값을 set한다.

 

Field의 set() 메서드 로직에 대해서는 자세히 살펴보지는 않았다.

다만 reflect 패키지에 존재하는 클래스인 것 정도만 확인했다.

(Object 내에 존재하는 setter 메서드를 이용하지 않는다는 것을 증명하기 위해 확인하였음)

Package java.lang.reflect
Provides classes and interfaces for obtaining reflective information about classes and objects.

 

 

많은 삽질을 했는데 내가 맞게 디버깅했는지는 잘 모르겠다...T.T

이상한 점이나 예외사항이 있다면 댓글 부탁드립니다😂


참고

반응형