BackEnd/Spring

[Spring] @RequestBody에 기본생성자만 필요하고 Setter는 필요없는 이유 - 1

샤아이인 2022. 6. 11.

그간 밀어오고 밀어왔던 내용에 대해 정리하고 넘어가야겠다 싶어 정리하는 글이다.

 

항상 무의식적으로 Client에게 데이터를 보내거나, 받을 때 일명 DTO를 사용하여 받아왔다.

난 너무나 자연스럽게? 혹은 무의식적으로 DTO에 @Getter를 선언하면서 사용해 온 것은 아닐까? 

나는 이전에 과연 한 번이라도 이게 왜 그런지 이전에 궁금해한 걸까? 나 스스로에게 정말 반성하는 의미로 남기는 글이다.

왜 이게 당연하다 생각하고 호기심 있게 살펴보지 않았을까... 뒤늦은 후회감이 조금은 온다.

 

또한 @Getter는 왜 추가한 것일까? 값을 꺼내오려고??

이러한 DTO를 Controller에서 받기 위해 @RequestBody를 사용해 왔다. 왜??

@RequestBody는 어떻게 DTO를 생성하는 것일까??

 

이 글은 이러한 궁금증으로부터 시작됐다. (글 처음 쓸 때만 해도 이렇게 복잡한 과정이 있을 줄은 몰랐는데....)



우선 @RequestBody에서 어떤 방식으로 객체를 생성하는지 파악한 후,

해당 객체에 값을 어떤 방식으로 binding 시키는지에 대하여 알아봅시다.

 

우선 이 글은 2개의 글로 만들어질 예정입니다.

1) 기본 생성자가 필요한 이유 (이번글)

2) Setter가 없어도 되는 이유

 

1. 기본 생성자가 필요한 이유

1-1) 예제 class 준비

우선 설명에서 사용할 간단한 DTO와 Controller에 대하여 알아봅시다.

 

우선 DTO는 다음과 같습니다. OrderDto입니다.

@Getter
@ToString
@NoArgsConstructor
public class OrderDto {

    private String itemName;
    private int amount;
    private RequestType requestType;

    public OrderDto(String itemName, int amount) {
        this.itemName = itemName;
        this.amount = amount;
    }

    public OrderDto(String itemName, int amount, RequestType requestType) {
        this.itemName = itemName;
        this.amount = amount;
        this.requestType = requestType;
    }

    @AllArgsConstructor
    public enum RequestType {
        GET ("get"),
        POST ("post");

        private String method;
    }
}

이를 통해 주문을 받는 Controller는 다음과 같습니다.

 

@Slf4j
@RestController
@RequestMapping("/api")
public class OrderController {

    @PostMapping("/order")
    public OrderDto orderRequest (@RequestBody OrderDto orderDto) {
        log.info("[" + orderDto.getRequestType().name() + "] " + orderDto.getItemName() + " : " + orderDto.getAmount());
        return orderDto;
    }
}

 

1-2) Spring의 기반 지식

우선 @ReqeustBody가 바인딩되는 곳을 찾기 위해서는 조금은 Spring에 대한 설명이 먼저 필요할 것 같습니다.

 

우선 우리가 Controller에 요청을 보내면,

1) 해당 요청을 처리할 Handler(Controller)를 찾아온 후

2) 해당 Handler를 처리할 수 있는 HandlerAdapter를 조회한다.

3) HandlerAdapter를 통해 Handler에게 요청을 위임시킨다.

 

생각해보면 HandlerAdapter에서 handler의 인자를 어떻게 알고 넘겨줄까?

handler의 애노테이션 기반의 컨트롤러는 매우 다양한 파라미터를 사용한다!

 

HttpServletRequest , Model, @RequestParam , @ModelAttribute 같은 애노테이션 그리고 @RequestBody , HttpEntity 같은 HTTP 메시지를 처리하는 부분까지 매우 큰 유연함을 보여준다.

 

이렇게 파라미터를 유연하게 처리할 수 있는 이유가 바로 ArgumentResolver 덕분이다.

 

0) HandlerAdapter는 ArgumentResolver를 호출한다.

1) ArgumentResolver는 컨트롤러의 파라미터, 애노테이션 정보를 기반으로 전달 데이터 생성한다.

2) 실제 컨트롤러(헨들러)를 호출할때 ArgumentResolver가 반환해준 인자를 전달하여 호출한다.

3) 컨트롤러에서 반환할 값이 있다면 ReturnValueHandler를 통하여 반환한다.

애노테이션 기반 컨트롤러를 처리하는 RequestMappingHandlerAdaptor는 바로 이 ArgumentResolver 를 호출해서 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터의 값(객체)을 생성한다.

 

스프링 MVC는 @RequestBody @ResponseBody 가 있으면 RequestResponseBodyMethodProcessor (ArgumentResolver)

 

이때 ArgumentResolver에서 Json 데이터를 Object로 변환하기 위해 MessageConverter가 사용됩니다.

ArgumentResolver 들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성하는 것 입니다.

 

여기까지 기초 지식을 설명했습니다. 이제 코드로 살펴보죠!

 

1-3) @RequestBody로 Object 생성하기 (기본 생성자 있는 경우, 인자를 받는 생성자는 2개 있음)

1. RequestResponseBodyMethodProcessor 브레이크 포인트 걸기

우선 RequestResponseBodyMethodProcessor 의 메서드 중에서 다음과 같은 메서드가 존재합니다.

이곳에 디버깅을 해보기 위해 breakpoint를 걸었습니다.

 

2. post 요청 보내기

PostMan을 통해 다음과 같이 POST 요청을 보내게 되었습니다.

 

3. MessageConverter 찾기

이후 위 사진에서 빨간 박스 안에있는 readWithMessageConverters()를 호출하게 됩니다.

readWithMessageConverters() 코드 내부에서 적합한 MessageConverter를 찾는 다음과 같은 과정이 있습니다.

우리는 JSON으로 요청으므로 MappingJackson2HttpMessageConverter가 선택되게 됩니다.

이렇게 찾아온 MessageConverter를 통해 canRead() 메서드를 호출하게 됩니다.

 

4. MappingJackson2HttpMessageConverter 의 동작

우선 canRead 를 통해 orderDto를 읽을 수 있는지 판별합니다.

읽을 수 있다 판별되면 true를 반환하게 됩니다.

 

위 canRead에서 true를 반환하였으니, 다음으로는 read() 메서드를 호출하게 됩니다.

 

5. read() 호출하기

다음과 같이 read() 안으로 들어오게 된다.

우선 JavaType에 해당되는 정보들을 얻어오게 됩니다.

이후 여기서 readJavaType 메서드를 호출하죠!

 

6. readJavaType() 호출하기

내부적으로 다음과 같습니다.

위 코드 부분에서 ObjectMapper의 readValue() 를 호출하여 객체로 변환하게 됩니다.

readValue() 내부에서 어떻게 변환하는지를 알아야 기본 생성자가 왜 필요한지 알 수 있습니다.

 

7. objectMapper.readValue() 내부

objectMapper의 readValue()내에서는 Request의 Body가 null인지만 확인하고, 다음과 같이 _readMapAndClose() 메서드를 호출합니다.

 

8. _readMapAndClose() 내부

다음과 같이 readRootValue를 호출하여 result를 반환하게 됩니다. 이 result가 변환된 Object일것 입니다.

따라서 변환 과정을 보려면 readRootValue 내부로 한번더 들어가야 합니다.

 

9. readRootValue 내부

여기서 핵심은 deser라는 Json을 Object로 역직렬화 시켜주는 Bean을 통해서 변환하게 됩니다.

그럼 Deserializer인 deser의 deserialize() 라는 매서드의 내부도 살펴 봐야겠죠?

 

10. deser.deserialize()의 내부

먼저 p.isExpectedStartObjectToken() 메서드는 Json이 "{"로 시작하는지 확인하는 메서드입니다.

 

그리고 _vanillaProcessing가 맞는지 확인하는데, _vanillaProcessing의 주석을 보니 "특수 기능이 전혀 없음을 나타내는 플래그로 가장 간단한 처리가 가능합니다." 라고 적혀있습니다.


다음에는 _objectIdReader가 null이 아닌지 확인하는데, _objectIdReader는 deserializer가 처리하는 값에 Object Id를 사용하려는 경우에 사용됩니다.

현재는 Object Id를 사용하지 않으므로 if(_objectIdReader !=null) 내부로 들어가지 않습니다.

 

11. deserializerFromObject() 내부로

거의 다 와 값니다. 조금만 더 들어가 봅시다~~

deserializerFromObject 내부에서 createUsingDefault()를 호출하게 됩니다.

 

뭔가 이름만 봐도 기본생성자로 객체를 생성해줄 것 같은 메서드 입니다.

 

12. createUsingDefault 내부로

내부에 들어가 보면 드디어 JSON을 객체로 변환시키는 부분에 도달할 수 있다.

우선 _defaultCreator 가 null인지 확인합니다. 여기서 defaultCreator는 기본 생성자를 말하겠죠?

기본 생성자가 null이 아님을 확인한 이후 defaultCreator.call()을 호출하여 객체를 생성하게 됩니다.

여기서 newInstance는 java.lang.reflect 페키지 안에있는 함수 입니다.

즉, 리플렉션을 활용하여 객체를 생성하게 됩니다.

 

지금까지의 설명은 기본 생성자가 있었을때 이렇게 동작한는 것 입니다.

만약 기본 생성자가 없었다면? 어떻게 작동했을까요?

 

 

1-4) @RequestBody로 Object 생성하기 (기본 생성자 없는 경우, 인자를 받는 생성자는 2개 있음)

그럼 기존의 OrderDto에서 @NoArgsConstructor를 생략해 봅시다! 기본생성자가 없는 것 이죠!

@Getter
@ToString
public class OrderDto {

    private String itemName;
    private int amount;
    private RequestType requestType;

    public OrderDto(String itemName, int amount) {
        this.itemName = itemName;
        this.amount = amount;
    }

    public OrderDto(String itemName, int amount, RequestType requestType) {
        this.itemName = itemName;
        this.amount = amount;
        this.requestType = requestType;
    }

    @AllArgsConstructor
    public enum RequestType {
        GET ("get"),
        POST ("post");

        private String method;
    }
}

위 코드는 기본 생성자가 없을 뿐, 받는 인자가 서로다른 생성자가 2개 존재하고 있습니다.

 

1. BeanDeserializer 에서 걸린다.

이후 이전처럼 deserializerFromObject() 내부로 들어가 보자.

 

2. deserializerFromObject() 내부

내부에서 이전에는 그냥 통과 했던 if문에 걸리게 된다.

이전 코드는 아래 부분에서 확인할 수 있다. 다음 코드를 보자.

우리는 이번에는 생성자가 없기 때문에 앞에서 deserializeFromObjectUsingNonDefault 라는 메서드에 걸리게 된다.

이름부터 "기본 생성자를 사용하지 않고 객체로 역직렬화하기" 임이 명확하게 들어난다.

 

3. deserializeFromObjectUsingNonDefault 내부로!

다음 코드를 살펴봅시다!

메서드의 내부를 보면 _delegateSerializer()를 사용하도록 되어 있습니다.

하지만 저의 DTO는 delegate를 하지 않아서 delegateSerializer를 못 가져오기 때문에 오류가 발생한 것입니다.

 

마지막 빨간 박스 부분에 보면, delegate 가 없거나 property 기반의 생성자가 없는 경우 역직렬화 할 수 없다고 나옵니다!

여기서 handleMissingInstantiator 를 호출하게 됩니다!

 

4. handleMissingInstantiator 내부로

여기서 다음과 같이 에러 message를 담아서 반환하게 됩니다!

따라서 다음과 같이 결과에서 확인할 수 있게 됩니다.

 

결국 default 생성자가 없어서 오류가 생기는 것은 맞다.

 

하지만, default 생성자가 없더라도 property 기반 클래스(property 관련 어노테이션이 적용된)면 생성 가능하며, delegate 되어있다면 이또한 생성 가능할 것 입니다.

 

1-5) @RequestBody로 Object 생성하기 (기본 생성자 없는 경우, 인자를 받는 생성자는 1개 있음)

기본 생성자가 없는 상황에서, 인자를 받는 생성자가 1개만 있다면 상황은 또 달라진다.

이 경우에는 Object를 생성할수가 있다.

Jackson에서 기본생성자와 getter,setter가 없으면 자동적으로 @JsonCreator를 해당 인자를 받는 생성자에 붙여서 동작하도록 도와준다.

 

1-6) @RequestBody로 Object 생성하기 (기본 생성자 없는 경우, 인자를 받는 생성자에서 인자를 1개만 받는 경우)

단일 생성자에 단일 파라미터가 있는 경우, 위와 같이 JackSon이 @JsonCreator를 추가해주는 기능이 동작하지 않아서 직접@JsonCreator 어노테이션을 생성자에 붙여줘야한다.

 

1-7) 결론

이번 글에서는 @RequestBody에 왜 기본 생성자가 필요한지 알아봤습니다.

 

이번글을 요약하면 다음과 같습니다.
1. 일반적인 상황에서는 기본 생성자가 꼭 필요하다.
-> RestController에서 @RequestBody를 바인딩을 하기 위해 ObjectMapper를 사용하는데 기본 생성자로 DTO를 생성하기 때문.

 

2. Property 기반 클래스(@JsonProperty, @JsonAutoDetect)인 경우 기본생성자 없이 생성 가능하다.

3. 생성자가 위임된 경우라면 기본생성자 없이 생성 가능하다.

4. 기본생성자가 없고, 인자를 받는 생성자가(인자 2개 이상) 있다면 생성 가능하다.

5. 기본생성자가 없고, 인자를 받는 생성자가 1개의 인자를 받는다면 @JsonCreator를 직접 추가해야 생성 가능하다.


다음 글에서는 "@Setter는 왜 필요 없는지" 에 대해 알아봅시다!

 

댓글