이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • @RequestBody는 왜 query parameter가 아니라 HTTP body와 연결되는가?
  • JSON 요청은 어떤 과정을 거쳐 Java DTO가 되는가?
  • 400, 415, 406 문제를 MessageConverter 관점에서 어떻게 나누어 보는가?

개요

@RequestBody는 HTTP request body를 Java 객체로 바인딩한다. Spring MVC는 HttpMessageConverter를 사용해 body를 읽고 쓴다. JSON의 경우 Jackson 기반 converter가 대표적으로 사용된다.

기존 MVC 문서의 ResponseBody JSON 데이터 직접 전송 Jackson, FormData, multipart, content type 관련 내용은 MessageConverter와 media type 이해 위에서 정리할 수 있다. 특히 form data는 Servlet request parameter 처리와 연결되므로 JSON body와 같은 방식으로 @RequestBody에 넣어 이해하면 안 된다.

원리

public record CreateMemberRequest(String name, int age) {
}
 
@PostMapping(value = "/members", consumes = "application/json")
public MemberResponse create(@RequestBody CreateMemberRequest request) {
    return memberService.create(request);
}

요청 body가 JSON이면 Spring은 Content-Type과 대상 Java 타입을 보고 적절한 HttpMessageConverter를 선택한다. Jackson converter는 JSON을 읽어 CreateMemberRequest 객체로 역직렬화한다. body가 비어 있거나 JSON 문법이 깨졌거나 DTO 생성 규칙과 맞지 않으면 검증 이전에 읽기 오류나 400 계열 예외가 발생할 수 있다.

Content-Type과 Accept

Content-Type은 클라이언트가 보내는 요청 body의 형식이다. Accept는 클라이언트가 받고 싶은 응답 형식이다.

  • Content-Type: application/json: 내가 보내는 body는 JSON이다.
  • Accept: application/json: 나는 JSON 응답을 받고 싶다.

Content-Type이 handler의 consumes나 converter와 맞지 않으면 415가 날 수 있다. 서버가 Accept에 맞는 응답을 만들 수 없으면 406이 날 수 있다.

검증과도 구분해야 한다. @Valid @RequestBody에서 body를 객체로 읽는 데 성공한 뒤 제약 조건이 실패하면 보통 MethodArgumentNotValidException 흐름이고, 아예 converter가 body를 객체로 만들지 못하면 validation 실패가 아니라 message conversion 실패다.

DTO 설계

요청 DTO는 외부 입력 계약이다. Entity와 바로 연결하면 내부 도메인 구조가 API에 노출되고, 검증과 보안 경계가 흐려진다.

좋은 요청 DTO는 다음 성질을 가진다.

  • API 입력 필드만 가진다.
  • 검증 애너테이션을 붙이기 쉽다.
  • 도메인 객체로 변환하는 경계가 명확하다.
  • Entity의 lazy loading이나 양방향 연관관계와 무관하다.

실전 팁

  • JSON 요청이 null로 들어오면 Content-Type, body 구조, DTO 생성자, 필드명 매칭을 순서대로 확인한다.
  • record DTO는 불변 입력 모델을 표현하기 좋지만 Jackson과 Bean Validation 지원 방식을 확인한다.
  • body stream은 기본적으로 한 번 읽는 성격이 있으므로 Filter에서 직접 읽을 때는 caching wrapper를 고려해야 한다.
  • form URL encoded 요청은 @ModelAttribute@RequestParam이 자연스럽다. Servlet API에서 request parameter 접근이 body 파싱과 연결되므로 @RequestBody로 다시 안정적으로 읽기 어렵다.
  • multipart는 JSON body와 다른 요청 형식이다. MultipartFile, @RequestPart, @ModelAttribute 중 무엇이 맞는지 구분한다.
  • 요청 DTO에서 Entity 변환을 할지, Service에서 변환할지는 팀 규칙으로 통일한다.

위험 신호!

  • JSON body를 보내면서 Content-Type을 지정하지 않는다.
  • HTML form 요청을 JSON API와 같은 @RequestBody DTO 흐름으로 처리하려 한다.
  • Entity를 @RequestBody로 직접 받는다.
  • Filter나 Interceptor에서 request body를 읽고 Controller에서 다시 읽으려 한다.
  • 415와 400을 모두 validation 실패로 처리한다.
  • DTO 필드명과 JSON 필드명이 어긋났는데 converter 문제로만 본다.

확인 질문

확인 질문

  • @RequestBody는 어떤 컴포넌트와 연결되는가?
    • HttpMessageConverter와 연결되어 HTTP body를 객체로 읽는다.
  • Content-TypeAccept의 차이는 무엇인가?
    • Content-Type은 요청 body 형식이고, Accept는 원하는 응답 형식이다.
  • 요청 DTO를 Entity와 분리하는 이유는 무엇인가?
    • 외부 입력 계약과 내부 도메인 모델의 변경 이유가 다르고 보안/검증 경계도 다르기 때문이다.

참고 문서