Spring

@RequestBody 이게 뭐야?

개발자성장기 2025. 4. 17. 21:55
반응형


웹 개발을 공부하다 보면 클라이언트와 서버라는 말을 자주 듣게 된다.

간단히 말해 클라이언트는 웹브라우저처럼 서버에 정보를 요청하는 쪽이고, 서버는 이 요청을 받아서 처리한 후 응답을 보내주는 쪽이이다.
클라이언트가 서버에게 보내는 메시지를 요청(Request)이라고 하고, 반대로 서버가 클라이언트에게 보내는 메시지를 응답(Response)이라고 부른다.


우리가 사용하는 많은 웹사이트는 페이지를 새로 고치지 않고도 데이터를 주고받으며 자연스럽게 동작한다.

이런 방식을 바로`비동기 통신`라고 한다.


비동기 통신을 하려면 클라이언트는 서버에게 요청을 보낼 때 데이터를 'Body'라는 부분에 담아서 보내야 한다.

서버도 클라이언트에게 응답할 때 마찬가지로 'Body'에 데이터를 담아서 보내준다.


이렇게 Body에 담아서 보내는 데이터를 각각 요청 Body, 응답 Body이라고 한다.
'Body'에 담길 수 있는 데이터 형식은 다양하지만, 가장 널리 사용되는 것은 JSON이라는 형식이다.

즉, 비동기 방식에서는 클라이언트와 서버가 주로 JSON 형식의 데이터를 주고받는다.


스프링을 사용할 때도 이 방식이 자주 사용된다.

클라이언트가 JSON이나 XML 등의 데이터를 보내면, 스프링이 자동으로 이 데이터를 자바 객체로 변환해서 처리할 수 있도록 도와준다.

 

여기서 중요한 역할을 하는 것이 바로 `@RequestBody`와 `@ResponseBody`라는 어노테이션이다.

  • `@RequestBody`는 클라이언트에서 보낸 HTTP 요청 Body(JSON 등)을 자바 객체로 바꿔준다.
  • `@ResponseBody`는 자바 객체를 다시 HTTP 응답 Body(JSON 등)으로 바꿔서 클라이언트에 보내준다.

이렇게 스프링 MVC는 비동기 통신을 더욱 쉽게 할 수 있도록 많은 편의 기능을 제공한다.  

자 이제 대략적인 흐름을 알았으니 본격적으로 `@RequestBody`에 대해서 알아보자.

 

📌 @RequestBody

HTTP 요청으로 넘어오는 body의 내용을 HttpMessageConverter를 통해 자바 객체로 바꿔준다. (역직렬화)

HTTP 요청의 body 내용을 통째로 자바 객체로 변환해서 매핑된 메소드 파라미터로 전달해준다.  

 

그럼 도대체 언제 바꿔주는 걸까? 

 

그전에 `@RequsetBody`가  Spring Web MVC 모듈에서 HTTP 요청이 오면 대략적으로 언제 동작하는지 알기 위해 Dispatcher Servlet의 흐름을 대강 알아보자.

 

source : 망나니개발자

1. 클라이언트의 요청을 디스패처 서블릿이 받음

2. 요청 정보를 통해 요청을 위임할 컨트롤러를 찾음

3. 요청을 컨트롤러로 위임

4. 핸들러 어댑터가 컨트롤러로 요청을 위임함

5. 비지니스 로직을 처리함

6. 컨트롤러가 반환값을 반환함

7. 핸들러 어댑터가 반환값을 처리함

8. 서버의 응답을 클라 이언트로 반환함

 

"갑자기 뭐지? 이걸 다 이해하라고?" 

 

절대 아니다 그러니 걱정할 필요는 없다.

지금 당장 Dispatcher Servlet을 전부 이해하라는 소리가 아니다.  

 

지금은 그냥 "클라이언트의 요청을 Dispatcher Servlet이 요청을 처리할 컨트롤러를 찾아서 위임하고, 그 결과를 받아서 클라이언트로 반환한다"는 정도만 이해하면된다.  

(더 자세히 이해하고 싶다면 이곳으로 가면 된다.)

 

우리는 여기서 4번째 과정인 "핸들러 어댑터가 컨트롤러로 요청을 위임함"에만 집중하면 된다. 

source : 망나니 개발자

바로 여기서 `@RequestBody`가 드디어 일을 시작한다.  

쉽게 말해 클라이언트에서 보낸 요청에 있는 body를 자바 객체로 바꿔주는 일을 바로 여기서한다.  

 

`@RequsetBody`는 HTTP 요청으로 같이 넘어오는 Header의 Content-type을 보고 어떤 `Converter`를 사용할지 정하기에 Content-type을 반드시 명시해야 한다.   

(Content-type은 따로 default 값이 없다. 그래서 프론트엔드와 협업할 때 종종 요청에 Content-Type을 명시하지 않아 에러가 발생하기도 한다)

📌 Content-Type이란?
HTTP 요청이나 응답이 주고받는 데이터가 어떤 형식인지 명시하는 헤더이다.  

naver 메인화면

📚 자주 사용하는 Content-type 종류
- application/json : JSON 데이터 전송
- application/x-www-form-urlencoded : 폼 데이터 전송 (HTML 폼 형식)
- application/xml : XML 데이터 전송
- text/plain : 단순 문자열 데이터를 보낼 때 사용
- multipart/form-data : 파일 전송 (spring에서는 주로 @RequestPart와 함꼐 사용)
- text/html : HTML 전송

 

자 이제 Content-type을 대략적으로 알았으니 이제 좀 전에 말한 "Converter"에 대해서 알아보자.

 

일단 자세히 알아보기 전에 요약하자면 스프링에서는 위에서 말한 작업을 HttpMessageConverter라는 친구가 자동으로 처리해준다.  
기본적으로는 Jackson이라는 라이브러리를 사용해서 JSON -> 객체(역직렬화) / 객체 -> JSON(직렬화)로 변환을 해준다.

 

그럼 여기서 말한 HttpMeesageConverter는 무엇일까?

 

📌 HttpMessageConverter

Spring 웹 모듈에는 HttpMessageConverter라는 인터페이스가 있다. (조금 뒤에서 보여드릴게요😎)

이 인터페이스는 HTTP 요청의 바디와 응답의 body를 읽고 쓰는데 사용된다.   

 

Spring은 다양한 미디어 타입에 맞는 여러 구현체를 기본으로 제공하며, 필요에 따라 개발자가 직접 구현하거나 커스터마이징할 수도 있다.

그리고 이러한 구현체들은 기본적으로 `RestTemplate`이나 `RequestMappingHandlerAdapter`에 등록되어 있어,  특별한 설정 없이도 편리하게 사용할 수 있다.

  

📚 주요 HttpMessageConverter 구현체들 

더보기

StringHttpMessageConverter

HTTP 요청이나 응답의 바디를 String으로 읽거나 쓸 수 있도록 도와준다.

기본적으로 모든 텍스트를 지원하면 응답 시  Content-Type은 text/plain으로 설정된다.

FormHttpMessageConverter

HTML 폼 데이터를 변환한다.

기본적으로 application/x-ww-form-urlencoded 타입을 읽고 씁니다.  

 

ByteArrayHttpMessageConverter

HTTP 메시지를 바이너리 배열(byte[])로 변환한다.

기본 application/octet-stream이다.

 

MarshallingHttpMessageConverter

XML 데이터 처리를 위한 변환키로, Spring의 Marshaller와 Unmarshaller를 사용한다.  

기본적으로 text/xml과 application/xml 미디어 타입을 지원한다.

 

MappingJackson2HttpMessageConverter

Json 데이터를 변환하는 대표적인 구현체로, Jackson의 ObjectMapper를 사용한다.  

기본적으로 application/json 미디어 타입을 지원한다.  

Jackson 애노테이션을 이용해 JSON 매핑을 커스터마이징할 수 있다.  

커스텀 ObjectMapper를 주입하여 더욱 세밀한 제어가 가능하다.

Spring에서는 위의 HttpMessageConverter 구현체들이 기본적으로 등록되어 있어, RestController를 사용할 때 별도의 설정 없이도 HTTP 요청과 응답의 데이터를 자동으로 변환해준다.  

 

예를 들어, 클라이언트가 JSON 데이터를 보내면, `MappingJackson2HttpMessageConverter`가 이를 자바 객체로 변환해주고, 서버가 자바 객체를 JSON으로 응답할 때 역시 같은 변환기를 사용한다.  

 

개발자는 필요한 경우, 특정 미디어 타입을 지원하도록 변환기를 커스터마이징하거나 새롭게 등록할 수 있다.

이러한 유연성 덕분에 다양한 데이터 포맷과 복잡한 요구 사항에 쉽게 대응할 수 있다.

 

그럼 HttpMessageConverter는 이 많은 구현체들 중 어떻게 알맞은 컨버터 구현체를 골라서 역직렬화(JSON -> 객체)를 할 수 있을까?

 

이전에 소개만 HttpMessageConverter의 모든 구현체들은 아래 `interface`에 있는 메서드를 당연히 갖는다.  

 

`canRead`는 요청 body를 특정 타입으로 읽어들일 수 있는지 판단할 때 사용한다. 

(클라이언트가 @RequestBody로 객체를 받을 때 - 역직렬화)

 

`canWrite`는 객체를 응답 body로 쓸 수 있는지 판단할 때 사용한다. 

(컨트롤러가 반환한 객체를 @ResponseBody or ResponseEntity로 JSON/XML등으로 내보낼 때 - 직렬화)

 

자 이제 위 내용을 바탕으로 대략적으로 역직렬화가 어떻게 동작하는지 알아보자

 

프론트에서 아래와 같은 요청을 보냈다고 가정해보자

POST /reservations HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json

{
  "name": "홍길동",
  "date": "2025-04-20",
  "time": "15:30:00"
}

 

 

@RestController
@RequestMapping("/reservations")
public class ReservationController {

    @PostMapping
    public ResponseEntity<Reservation> create(@RequestBody ReservationRequest request) {
      // ...
    }
}

 

`@RequestBody` 어노테이션이 있기에 

 

 

1. DispatcherServlet이 요청을 받는다.  

2. HandlerMapping -> `/reservations` url에 매핑된 `creat(...)` 메서드를 찾음

3. HandlerAdapter 메서드를 실행하기 전 인자 해석 단계로 진입

4. RequestResponseBodyMethodProcessor

 

`@RequestBody`붙은 파라미터에 대해 등록된 HttpMessageConverter들 중 `canRead(ReservationRequest.class, MediaType.APPLICATION_JSON)`이 `true`인 컨버터 선택

 

실제 컨버터를 선택하는 코드는 일부이다.

`this.messageConverters`를 순회하면서 `canRead(...)`메서드를 통해 알맞은 컨버터 타입을 찾고있음을 알 수 있다.  

 

컨버터의 순서는 공식문서에서 MessageConverter Implementations 표에 있는 순서와 동일하다)

대개 객체라면 `MappingJackson2HttpMessageConverter`가 선택되어, 내부 `ObjectMapper`로 역직렬화(JSON -> ReservationRequest 객체 생성)를 한다.

 

여기서 ObjectMapperJackson 라이브러리의 클래스이다.  

`spring-boot-starter-web`의존성만 추가해도 포함되는 라이브러리다.

 

`objectMapper.readValue`를 호출해  JSON을 `ReservationRequest` 같은 자바타입으로 변환한다.

이렇게 변환할 때 해당 객체는 반드시 기본 생성자(매개변수가 없는 "비어있는" 생성자)를 가지고 있어야한다.  

물론 자바가 아무 생성자도 가지고 있지 않다면 컴파일러가 자동으로 기본 생성자를 만들어주기 때문에 신경쓸 필요가 없지만  매개변수가 있는 생성자를 만들면  명시적으로 기본 생성자를 만들어줘야한다.  

 

그렇다면 왜 기본생성자를 요구할까?

 

Jackson 라이브러리는 객체를 역직렬화(deserialization)할 때 내부적으로 객체를 먼저 생성하고 그 다음에 리플렉션으로 필드를 채워 넣기 때문이다.  

🔎 Jackson의 역직렬화 과정

1. 객체 생성
Jackson은 대상 클래스의 기본 생성자를 호출하여 객체를 생선한다.
기본 생성자가 없을 경우, @JsonCreator와 @JsonProperty를 사용하여 매개변수 있는 생성자를 통해 객체를 생성할 수 있다.

public class Person {
    private final String name;
    private final int age;

    @JsonCreator
    public Person(@JsonProperty("name") String name,
                  @JsonProperty("age") int age) {
        this.name = name;
        this.age = age;
    }
    // Getter methods...
}​


2. 필드 값 주입
생성된 객체에 JSON의 각 필드 값을 주입한다. 이때, Jackson은 다음과 같은 방법을 사용한다.
1. Setter 메서드 : 해당 필드에 대한 setter 메서드가 존재하면 이를 호출하여 값을 설정한다.
2. 직접 필드 접근 : setter 메서드가 없을 경우, 리플렉션을 통해 필드에 직접 접근하여 값을 설정한다. 

 

🔎 Jackson의 직렬화 과정

직렬화 할 때는 이미 만들어진 객체에서 Jackson 내부적으로 getter 메서드를 호출해서 값을 읽고 JSON 문자열로 변환한다.  
따라서 역질렬화 과정과 다르게 기본 생성자는 필요없고 getter만 있으면 된다.  

만약 getter이 없다면 Jackson은 아무 필드도 읽지 못해서 빈 JSON이 나올 수 있다.  
하지만 `@JsonProperty`나 `@JsonAutoDetect`등의 어노테이션을 쓰면 리플렉션으로 필드 값을 읽을 수 있도록 강제할 수 있다.  

public class Person {
    @JsonProperty
    private String name;

    @JsonProperty
    private int age;
}​

➡ 이런 경우엔 getter 없어도 직렬화 가능!

 

위와 같은 과정을 거치면 요청 Body에 있는 JSON이 객체로 변환되고 그것을 통해 내부 비즈니스 로직이 동작한다.  

 

기본 생성자가 없는 경우도 있는데 이는 왜 역직렬화가 되는거죠?

Jackson은 객체에 생성자가 하나만 있으면 그 생성자를 묵시적(@JsonCreator 없이)으로 ‘Creator’로 간주해서 디폴트 생성자 없이도 역직렬화해 준다.

다만 이게 잘 동작하려면 다음 조건이 충족되어야 한다.

  1. 생성자가 오직 하나여야 한다
    • 다른 파라미터 생성자가 하나라도 더 있으면 Jackson은 어느 쪽을 쓸지 몰라서 에러를 낸다.
  2. 파라미터 이름이 JSON 필드 이름과 일치해야 한다

 

 

 

 

<작성 예정>

Controller에서 받을때 어노테이션 생략시 @ModelAttribute가 기본값이므로 @RequestBody를 사용하고자 하는 경우에는 반드시 기술해야 한다.

 

 

📌 Reference

https://www.baeldung.com/jackson-deserialization

https://www.baeldung.com/jackson-object-mapper-tutorial

https://docs.spring.io/spring-framework/reference/web/webmvc/message-converters.html

https://github.com/FasterXML/jackson

[Spring] Dispatcher-Servlet(디스패처 서블릿)이란? 디스패처 서블릿의 개념과 동작 과정

반응형