백엔드/Spring

[Spring] 스프링 HTTP 응답 하기

happy_life 2022. 7. 22. 17:36

목차

1. 응답 - 정적 리소스, 뷰 템플릿

2. 응답 - HTTP API, 바디에 직접 입력

3. HTTP 메시지 컨버터

4. 요청 매핑 핸들러 어댑터 구조

 

 

응답 - 정적 리소스, 뷰 템플릿

 

스프링에서 응답 데이터를 만드는 방법은 총 3가지이다.

 

1) 정적 리소스

스프링 부트에서는 /static, /publc, /resources, /META-INF/resources 정보를 제공한다.

src/main/resources는 리소스를 보관하는 곳이면서, 클래스 패스의 시작경로인 것이다.

따라서 위의 디렉토리에 리소스를 넣어두면 스프링 부트가 정적 리소스로 서비스를 제공하게 된다.

 

정적 리소스 경로

src/main/resources/static

 

만약 아래와 같은 경로에 파일이 있다고 하자

src/main/resources/static/basic/hello-form.html

 

이런 경우 웹 브라우저에서 아래와 같이 실행하면 된다.

http://localhost:8080/basic/hello-form.html

 

정적 리소스는 해당 파일을 변경 없이 그대로 서비스 하는 것이다. 

 

정적 리소스

 

 

 

 

2) 뷰 템플릿 사용

뷰 템플릿을 거쳐 HTML이 생성되고, 뷰가 응답을 만들어 전달하게 된다. 스프링 부트는 기본 뷰 템플릿 경로를 제공해준다.

 

뷰 템플릿 경로

src/main/resources/templates

 

뷰 템플릿 생성

src/main/resources/templates/response/hello.html

 

뷰 템플릿

 

정적 리소스와 달리 뷰 템플릿은 동적으로 작동하므로, 데이터를 담고 있거나 한다. 따라서 url로  먼저 입력을 받아 매핑된 컨트롤러에서 model에 데이터를 담는다. 이후 데이터와 함께 뷰 템플릿으로 dispatch하게 된다. 

url에 response-view가 입력되면, Model에 데이터를 ("data", "helloWorld!")로 매핑하고 return "response/hello"를 통해 뷰 템플릿으로 이동하게 된다.  뷰에서는 매핑되어있는 hello!를 키인 data로 찾아 empty에 hello!를 덮어쓰고 응답을 하게 되는 것이다.

 

spring.thymeleaf.prefix=classpath:/templates/

spring.thymeleaf.suffix=.html

 

가 기본으로 설정되어 있기때문에, 논리 이름으로도 뷰 템플릿으로 dispatch할 수 있는 것이다.

 

 

동작 방식

1. 뷰템플릿 담당 컨트롤러

@Slf4j
@Controller
public class ResponseController {

    @GetMapping("/response-view")
    public String responseViewV1(Model model) {
        model.addAttribute("data", "helloWorld!");

        return "response/hello";
    }
}

 

2. 뷰 템플릿 (Thymeleaf 사용)

<!DOCTYPE html>
<html xmlns:th="http://www.thymefleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<p th:text="${data}">we put data here!</p>
</body>
</html>

 

 

 

 

응답 - HTTP API, 바디에 직접 입력

 

HTTP API로 바디에 직접 입력하는 것이다.

 

@ResponseBody, HttpEntity를 사용하여 메시지 바디에 직접 응답 데이터를 출력한다. 이는 뷰 템플릿을 사용하지 않는다.

 

String 응답

    @GetMapping("/response-body-string")
    public void responseBodyV1(HttpServletResponse response) throws IOException {
        response.getWriter().write("ok");

    }

    @GetMapping("/response-body-string2")
    public ResponseEntity<String> responseBodyV2() {
        return new ResponseEntity<>("ok", HttpStatus.OK);
    }

    @ResponseBody // 이 방식을 사용하면 된다.
    @GetMapping("/response-body-string3")
    public String responseBodyV3() {
        return "ok";
    }

 

string으로 응답하기 위해 위의 3가지 방식을 사용할 수 있다.

 

String 바디

 

 

 

Json 응답

    @GetMapping("/response-body-json")
    public ResponseEntity<Member> responseJsonV1() {
        Member member = new Member();
        member.setId(1L);
        member.setName("memberA");
        member.setAge(20);

        return new ResponseEntity<>(member, HttpStatus.OK);
    }

    @ResponseBody
    @GetMapping("/response-body-json2")
    public Member responseJsonV2() {
        Member member = new Member();

        member.setId(1L);
        member.setName("memberA");
        member.setAge(20);

        return member;
    }

Json으로 응답하기 위해 위의 2가지 방식을 사용할 수 있다.

 

Json 응답

 

 

 

HTTP 메시지 컨버터

 

위의 예제들에서 보듯, 뷰 템플릿으로 HTML을 생성해 응답하는 것이 아니라, HTTP API처럼 데이터를 HTTP 메시지 바디에 직접 읽고 쓰는 경우에는 HTTP 메시지 컨버터를 사용하는 것이 편리하다.

 

1. 언제

HTTP 요청과 응답이 @RequestBody, HttpEntity인 경우에 사용된다.

 

 

2. 동작 방식

HTTP 메시지 컨버터는 canRead(), canWrite()로 요청과 응답으로 오는 정보를 지원하는지 체크하게 된다.

 

3. 스프링 부트 기본 메시지 컨버터

0 = ByteArrayHttpMessageConverter

1 = StringHttpMessageConverter

2 = MappingJackson2HttpMessageConverter

 

1) ByteArrayHttpMessageConverter

byte[] 데이터를 처리한다. 

클래스 타입: byte[], 미디어 타입: */* 를 지원한다.

 

요청 예시: @RequestBody byte[] data

응답 예시: @ResponseBody return byte[] 쓰기 미디어 타입: application/octet-stream

 

2) StringHttpMessageConverter

String 문자로 데이터를 처리한다.

클래스 타입: String, 미디어 타입: */*

 

요청 예시: @RequestBody String data

응답 예시: @ResponseBody return "ok" 쓰기 미디어 타입:text/plain

 

3) MappingJackson2HttpMessageConverter

Json으로 데이터를 처리한다.

클래스 타입: 객체 또는 HashMap, 미디어 타입: application/json 관련

 

요청 예시: @RequestBody HelloData data

응답 예시: @ResponseBody return helloData 쓰기 미디어 타입: application/json 관련

 

예시

StringHttpMessageConverter

 

 

동작과정

1) HTTP 요청 데이터 읽기

1.ByteConverter부터 순차적으로 돈다. -> canRead()? X

2. StringConverter를 돈다. canRead()? -> 클래스 타입이 String 이고, content-type이 application/json은 미디어 타입 */* 에 포함되므로  O

3. read() 호출 후 객체를 생성해서 반환한다.

 

2) HTTP 응답 데이터 생성

1. 컨트롤러에서 @ResponseBody 어노테이션을 체크하거나  return 타입이 HttpEntity인지 체크한다. 

2. 메시지 컨버터가 응답 데이터 String 형식으로 메시지를 쓸 수 있는지 canWrite()를 호출해서 체크한다.

3. canWrite()를 만족하면 write()를 호출해 응답메시지 바디에 데이터를 생성한다.

 

 

 

 

요청 매핑 핸들러 어댑터 구조

 

어노테이션 기반 컨트롤러는 어떤 원리에 의해 동작하는 것인지 알아보자.

    @ResponseBody
    @GetMapping("/response-get-json-body2")
    public String responseGetJsonV1(@RequestBody Member member) {
        log.info("id={}, name={}, age={}", member.getId(),member.getName(), member.getAge());
        return "ok";
    }

mvc패턴

 

기존에 공부했던 mvc 패턴을 다시 보며 과정을 이해해보자.

 

1. RequestMapping 어노테이션 기반의 핸들러(컨트롤러)이므로, RequestMappingHandler를 조회하게 된다.

2. 이후 핸들러 어댑터 목록에서 RequestMappingHandlerAdapter를 받아오게 된다.

 

이 두 과정은 기존의 이해와 같다. 하지만 어노테이션 기반의 핸들러 동작을 이해하기 위해  RequestMappingHandlerAdapter을 구체적으로 살펴볼 필요가 있다.

RequestMappingHandlerAdapter

 

2.1 어댑터는 ArgumentResolver를 호출하게 된다. 스프링에서는 30개가 넘는 ArgumentResolver를 지원하는데 이중 @RequestBody Member를 보고 객체 Argument를 지원하는 ArgumentResolver를 호출하게 된다. 호출 과정은 아래 사진을 참고하자.

호출 과정

 

2.2 만약 @RequestBody 어노테이션이 있거나 인자가 HttpEntity인 경우에는  ArgumentResolver가 HTTP 메시지 컨버터를 호출 후 객체를 생성한다.  (여기선 MappingJackson2HttpMessageConverter)

이를 반환하면 어댑터가 받고 가지고 있다가  핸들러 호출시 인자로 넘기게 된다. (@RequestBody, HttpEntity가 아닌 경우는 HTTP 메시지 컨버터를 호출하지 않고, ArgumentResolver에서만 로직이 작동하고 어댑터에 객체를 반환한다.)

 

 

2.3 받은 인자를 통해 핸들러에서 로직을 수행하고 return 을 통해 String이든, 객체든 반환하게 된다. 이럴 때 returnValueHandler가 사용된다. 앞서 ArgumentResolver와 마찬가지로, 지원할 수 있는 ValueHandler가 조회되고, ValueHandler가 HTTP 메시지 컨버터를 호출해 객체를 반환받아 Dispatcher Servlet에 반환하는 것이다. ((단, @RequestBody, HttpEntity가 아닌 경우는 HTTP 메시지 컨버터를 호출하지 않는다.)

 

 

 

 

 

 

 

 

 

본 포스팅은 김영한님 인프런 강의내용을 바탕으로 복습을 위해 작성하였습니다. 강의를 통해 배운 개념을 바탕으로 추가적으로 공부한 부분과 간단한 코드 예제를 작성하였습니다. 코드 전체를 복사한 것이 아니라 임의로 수정하거나 생략하였습니다.