개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

공부 › Spring › 웹 MVC

REST API와 메시지 컨버터

컨트롤러가 돌려준 객체는 어디서 JSON이 되나

컨트롤러 메서드는 자바 객체를 받고 자바 객체를 돌려주는데, 네트워크로 오가는 것은 JSON 같은 글자입니다. 그 사이에서 글자를 객체로, 객체를 글자로 바꾸는 것이 메시지 컨버터입니다. 요청의 Content-Type과 Accept 헤더가 어느 컨버터를 쓸지 정하고, 맞는 컨버터가 없으면 415나 406이 됩니다.

실행으로 대조 · 스프링 부트 4.1.1 서버에 열여섯 가지 조합의 요청을 보내 대조

이 실습에 나오는 개념

HttpMessageConverter
HTTP 요청 본문을 자바 객체로, 자바 객체를 HTTP 응답 본문으로 변환하는 구성 요소다. @RequestBody와 @ResponseBody가 이를 사용한다. JSON의 경우 Jackson 라이브러리를 이용한 컨버터가 변환을 맡는다.
@Controller와 @RestController
@Controller의 메서드가 반환한 문자열은 기본적으로 뷰 이름으로 해석된다. @RestController는 @Controller에 @ResponseBody를 더한 것으로, 메서드가 반환한 객체가 JSON 등으로 변환되어 응답 본문에 바로 쓰인다.
콘텐츠 협상(Accept와 Content-Type)
응답 본문을 어떤 형식으로 받을지 클라이언트와 서버가 헤더로 맞추는 일이다. 클라이언트는 Accept 헤더로 받고 싶은 응답의 형식을 알리고, 서버는 그 가운데 만들 수 있는 형식을 골라 응답의 Content-Type 헤더에 적어 보낸다. 요청에 본문이 있을 때는 클라이언트가 그 본문의 형식을 요청의 Content-Type 헤더로 알리는데, 이것은 협상이 아니라 보내는 쪽이 알려 주는 것이다. 값은 application/json이나 text/html 같은 미디어 타입으로 적는다. 서버가 요청 본문의 형식을 읽지 못하면 415(Unsupported Media Type)로, 원하는 형식의 응답을 만들지 못하면 406(Not Acceptable)으로 응답한다. 스프링은 본문의 내용을 보고 형식을 짐작하지 않고 Content-Type 헤더를 따르므로, JSON을 보내면서 Content-Type을 다르게 적으면 읽지 못한다.
ResponseEntity
응답의 상태 코드와 헤더, 본문을 한꺼번에 담아 컨트롤러가 돌려주는 객체다. 객체만 돌려주면 상태 코드는 200으로 정해지지만, ResponseEntity.status(201).body(객체)나 ResponseEntity.created(주소).body(객체)처럼 쓰면 상태 코드와 헤더를 컨트롤러가 직접 정할 수 있다. 본문으로 담은 객체는 그냥 돌려줄 때와 똑같이 메시지 컨버터가 JSON 등으로 바꾼다. 경우에 따라 다른 상태 코드를 돌려줘야 하는 메서드나 @ExceptionHandler에서 주로 쓴다.
ViewResolver
컨트롤러가 반환한 뷰 이름(문자열)을 실제 화면을 그릴 뷰 객체로 바꿔 주는 구성 요소다. 서버에서 HTML을 만들어 응답하는 방식에서 사용하며, JSON을 직접 응답하는 REST API에서는 사용되지 않는다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. 요청이 도착합니다: POST /orders. Content-Type 헤더(application/json)는 "내가 보내는 본문의 형식"을, Accept 헤더(application/xml)는 "내가 받고 싶은 응답의 형식"을 알려 줍니다.
  2. @RequestBody OrderRequest req: 컨트롤러를 부르기 전에 요청 본문을 OrderRequest 객체로 바꿔야 합니다. 스프링은 Content-Type 헤더를 보고 읽을 컨버터를 고릅니다. application/json이므로 JSON 메시지 컨버터(Jackson)가 맡습니다.
  3. JSON 읽기: Jackson이 본문의 JSON에서 이름이 같은 값을 찾아 OrderRequest 객체(item은 "book", qty는 2)를 만듭니다. 글자가 자바 객체가 됐습니다.
  4. create(req) 실행: 컨트롤러 메서드가 주문을 만들고 OrderResponse 객체를 돌려줍니다.
  5. @RestController: 이 애너테이션은 @Controller에 @ResponseBody를 더한 것입니다. @ResponseBody가 있으면 돌려준 값을 화면 이름으로 보지 않고, 메시지 컨버터로 응답 본문에 직접 씁니다.
  6. 응답 형식 고르기: 클라이언트는 Accept로 application/xml을 원합니다. 이 서버에는 OrderResponse 객체를 XML로 만들 줄 아는 컨버터가 없습니다. "원하는 형식으로는 줄 수 없다"는 뜻의 406으로 응답합니다(HttpMediaTypeNotAcceptableException). 컨트롤러 메서드는 이미 실행된 뒤이므로 주문은 만들어진 상태입니다.

실행 결과

코드

// 보내는 요청
// POST /orders
// Content-Type: application/json
// Accept: application/xml
//
// {"item": "book", "qty": 2}

record OrderRequest(String item, int qty) { }
record OrderResponse(long id, String item, int qty) { }

@RestController                        // @Controller + @ResponseBody
class OrderController {

    @PostMapping("/orders")
    OrderResponse create(@RequestBody OrderRequest req) {
        return new OrderResponse(1, req.item(), req.qty());
    }
}

이 모형의 전제

컨트롤러 네 가지와 요청 네 가지를 곱한 열여섯 조합을 모두 실제 스프링 부트 4.1.1 서버(스프링 프레임워크 7.0.9, 내장 톰캣 11, Jackson 3)에 HTTP 요청으로 보내, 상태 코드와 컨트롤러 실행 여부, 스프링이 던진 예외의 이름, 요청 본문을 읽은 컨버터와 응답 본문을 쓴 컨버터, 성공한 응답의 Content-Type과 Location 헤더가 화면과 일치하는 것을 확인했습니다. 오류 응답(400, 406, 415)의 본문과 Content-Type은 버전과 설정에 따라 달라서 결과로 삼지 않았습니다. 대조한 서버에는 XML을 만드는 라이브러리가 없습니다. jackson-dataformat-xml 같은 라이브러리를 넣으면 Accept: application/xml 요청에 XML로 응답하므로 406이 되지 않습니다. 컨버터의 클래스 이름(JacksonJsonHttpMessageConverter)은 스프링 7 기준이고, 그 전 버전에서는 이름이 다릅니다. 응답의 Content-Type은 형식 이름만 보였습니다(실제로는 뒤에 charset이 붙기도 합니다). @Controller가 돌려준 화면 이름은 템플릿 엔진 대신 같은 이름의 뷰 객체를 등록해 확인했습니다. 화면을 돌려주는 요청이 Accept와 관계없이 200이 된 것은 이 구성에서 본 결과이며, 뷰 설정에 따라 다를 수 있습니다. 화면에 없는 경우도 같은 서버에서 확인했습니다. 그 이름의 화면이 없으면 404였고(템플릿 엔진을 쓰면 다른 오류가 될 수 있으며 그것은 확인하지 않았습니다), @Controller의 메서드에 @ResponseBody를 붙이면 @RestController와 같은 결과였고, produces = "application/json"을 적은 메서드는 Accept: application/xml 요청에 컨트롤러를 실행하지 않고 406으로 응답했습니다. Accept 헤더 없이 보내면 객체는 JSON으로, 문자열은 text/plain으로 나갔습니다. ResponseEntity를 돌려주고 406이 된 조합에서는 응답에 Location 헤더가 남아 있었는데, 이것은 버전에 따라 다를 수 있어 화면에 넣지 않았습니다.

앞 실습: 검증 · 다음 실습: 스프링 부트 자동 설정

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요