공부/Spring

@RequestBody 사용을 위한 DTO의 조건을 정리해보자

d02 2023. 4. 24. 22:33

학습 과정에서 작성한 글로, 잘못된 내용이 있을 수 있음을 미리 밝힙니다.

피드백은 항상 환영합니다. 



Spring에서 HTTP 요청의 body를 그대로 자바 객체로 변환하고 싶을 때,
핸들러 메서드의 매개변수에 @RequestBody 어노테이션을 붙여준다.

 

이 때 변환하고자 하는 자바 객체의 타입(클래스)을 매개변수로 둔다.

이 클래스를 DTO라고 했을 때, @RequestBody를 사용할 수 있는 DTO의 조건이 어떻게 되는지 정리해보려고 한다.

 

요청 파라미터 또는 Form-Data(이 역시 최종적으로는 key=value 요청 파라미터로 붙어서 나간다)로 요청할 때에는 body에 값을 담지 않으므로 이 어노테이션을 사용하지 않는다.
주로 XML 또는 json 형식의 content-type으로 요청해서 body에 정보가 담겨있을 때 사용한다.

 

상황 설정 

POST /plays HTTP/1.1
content-type: application/json; charset=UTF-8
host: localhost:8080

{
    "names": "브리,토미,브라운",
    "count": 10
}

요청 메시지의 body를 PlayRequestDto 라는 객체로 변환해 받고 있다.
해당 DTO 클래스는 String names, int count 두 개의 private 인스턴스 변수를 가진다.

    @PostMapping("/plays")
    public ResponseEntity<PlayResponseDto> play(@RequestBody PlayRequestDto playRequestDto) {
        final int count = playRequestDto.getCount();
        final List<String> carNames = TextParser.parseByDelimiter(playRequestDto.getNames(), CAR_NAMES_DELIMITER);

        final PlayResponseDto playResult = racingCarService.playGame(count, carNames);

        return ResponseEntity.ok(playResult);
    }

 

 

간단한 내부 동작 설명

사용법을 정리함이 주 목적이므로, 내부 동작에 대해서는 최대한 간단하게 넘어가겠다.


기본 동작

MappingJackson2HttpMessageConverter를 통해 json 값을 객체로 역직렬화한다.

이 때 Jackson 라이브러리의 ObjectMapper를 통해 값을 바인딩해주는데,

객체를 먼저 생성하고, Reflection으로 값을 설정해준다.

=> 기본적으로 기본 생성자(빈 생성자)가 있어야 한다. 

 

jackson-module-pararamter-names 모듈이 의존성 등록되어 있는 경우

해당 모듈은 어노테이션을 직접 붙이지 않고도, 생성자 인수에 액세스할 수 있도록 한다.

따라서 기본 생성자가 없더라도, 요청 메시지의 변수들과 일치하는 인자를 받는 생성자가 있다면

Jackson이 자동적으로 해당 생성자에 위임 어노테이션 @JsonCreator을 붙여서 역직렬화를 진행한다.

단, 인자를 하나만 받는 생성자 하나만 가지고 있다면 이 기능은 동작하지 않는다.

 

해당 모듈은 Spring Boot 에서 제공하는 spring-boot-starter-web 모듈에 기본적으로 포함되어 있기 때문에

Spring Boot에서는 별도 설정 없이 위와 같이 동작함을 확인할 수 있다.

주의할 점은, 이는 외부 라이브러리이기 때문에 IntelliJ로 빌드 시 "Download external annotations for dependencies" 란이 체크 해제되어있으면 정상적으로 동작하지 않는다.

 

@RequestBody를 사용할 수 있는 경우

따라서 @RequestBody 를 사용하려면 기본 생성자가 있어야 한다는 것이 사실이지만,

Jackson bind 라이브러리에서 지원하는 위임 어노테이션을 사용할 경우 기본 생성자 없이도 동작한다.

 

결론적으로 @RequestBody 를 사용할 수 있는 경우는 아래와 같다.

 

  1. 기본 생성자가 있으면 사용 가능하다.
  2. 기본 생성자가 없지만, 다른 생성자에게 위임하는 특정 경우 사용 가능하다.

2번 경우로 어떤 것이 있는지 코드와 함께 정리해보자.

 

 

 

2-1. 자동 위임을 통해 필드와 일치하는 모든 인자를 받는 생성자에 역직렬화를 위한 정보를 위임하는 경우


아래 조건을 만족해야 한다.

  1. jackson-module-praramter-names 모듈이 의존성 등록되어 있다
  2. 요청 메시지의 필드와 일치하는 인자를 받는 생성자가 있다
  3. 인자의 개수가 2개 이상이다
@Getter
@AllArgsConstructor
public class PlayRequestDto {

    private String names;
    private int count;
}

 

 

2-2. 직접 어노테이션을 붙여서 필드와 매핑할 모든 인자를 받는 생성자에 역직렬화를 위한 정보를 위임하는 경우


아래 조건을 만족해야 한다.

  1. 요청 메시지의 필드와 매핑할 인자를 받는 생성자가 있다
  2. 직접 @JsonCreator, @JsonProperty 어노테이션을 붙여준다
    (필드명이 일치하지 않더라도 어노테이션에서 이를 일치시켜줄 수 있다
@Getter
public class PlayRequestDto {

    @JsonProperty
    private String names;
    @JsonProperty
    private int count;

    @JsonCreator
    public PlayRequestDto(final String names, final int count) {
        this.names = names;
        this.count = count;
    }
}

 

 

2-3. 자동 위임을 통해 필드와 일치하는 일부 인자를 받는 생성자와 setter에 역직렬화를 위한 정보를 위임하는 경우


아래 조건을 만족해야 한다. 

  1. 필드와 일치하는 인자 중 일부(2개 이상)를 받는 생성자가 있다
  2. 생성자 인자로 받지 않는 필드에 대한 setter가 있다

주어진 상황에서는 받는 필드의 수가 2개였지만,
예시 코드를 위해 테스트에서 요청 메시지 정보와 Dto에 playId라는 필드를 추가하고 테스트해본 결과 값을 잘 받아옴을 확인할 수 있었다.

@Getter
public class PlayRequestDto {

    private Long playId;
    private String names;
    private int count;

    public PlayRequestDto(final Long playId, final String names) {
        this.playId = playId;
        this.names = names;
    }

    public void setCount(final int count) {
        this.count = count;
    }
}

 

결론

생각보다 여러 경우일 때 @RequestBody를 사용할 수 있지만,

특정 라이브러리에 의존적인 방식을 지양하려면 기본 생성자를 정의하여 사용하는 것이 좋겠다.

public 기본 생성자를 정의하는 것이 문제가 된다면 private으로 정의하거나 어노테이션을 활용할 수 있다.

그리고 아무 생성자도 정의하지 않으면 자동으로 기본 생성자가 생기지만,
다른 개발자가 @RequestBody의 사용법을 모를 수도 있으므로 이는 명시적으로 정의해두는 것이 좋겠다.

 

또한 3번 같은 방식도 변경에 열려있는 setter 사용은 지양하는 것이 좋기 때문에 특수한 상황이 아니라면 쓰지 않을 것 같다.

 

추가로 정리가 필요한 내용

- 직렬화 / 역직렬화 와 ObjectMapper
- Reflection
- property 기반 클래스의 경우 기본 생성자 없이도 역직렬화가 가능하다는데, property 기반 클래스가 무엇인지?

- 팩토리 메서드로도 역직렬화를 수행할 수 있는 것 같은데 사실인지, 어떻게 하는지?

 

참고자료

[공식문서] https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/RequestBody.html

[baeldung] Jackson – Decide What Fields Get Serialized/Deserialized

[tistory] jackson을 이용한 data bidning 이해하기

[우테코 prolog] ObjectMapper로 역직렬화할 객체에 기본 생성자가 필요한 이유

[테코블] @RequestBody vs @ModelAttribute
[tistory] @RequestBody에 기본 생성자만 필요하고 Setter는 필요없는 이유 - 1

 

반응형