@RequestBody 사용을 위한 DTO의 조건을 정리해보자
학습 과정에서 작성한 글로, 잘못된 내용이 있을 수 있음을 미리 밝힙니다.
피드백은 항상 환영합니다.
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 를 사용할 수 있는 경우는 아래와 같다.
- 기본 생성자가 있으면 사용 가능하다.
- 기본 생성자가 없지만, 다른 생성자에게 위임하는 특정 경우 사용 가능하다.
2번 경우로 어떤 것이 있는지 코드와 함께 정리해보자.
2-1. 자동 위임을 통해 필드와 일치하는 모든 인자를 받는 생성자에 역직렬화를 위한 정보를 위임하는 경우
아래 조건을 만족해야 한다.
- jackson-module-praramter-names 모듈이 의존성 등록되어 있다
- 요청 메시지의 필드와 일치하는 인자를 받는 생성자가 있다
- 인자의 개수가 2개 이상이다
@Getter
@AllArgsConstructor
public class PlayRequestDto {
private String names;
private int count;
}
2-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에 역직렬화를 위한 정보를 위임하는 경우
아래 조건을 만족해야 한다.
- 필드와 일치하는 인자 중 일부(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 기반 클래스가 무엇인지?
- 팩토리 메서드로도 역직렬화를 수행할 수 있는 것 같은데 사실인지, 어떻게 하는지?
참고자료
[baeldung] Jackson – Decide What Fields Get Serialized/Deserialized
[tistory] jackson을 이용한 data bidning 이해하기
[우테코 prolog] ObjectMapper로 역직렬화할 객체에 기본 생성자가 필요한 이유
[테코블] @RequestBody vs @ModelAttribute
[tistory] @RequestBody에 기본 생성자만 필요하고 Setter는 필요없는 이유 - 1