티스토리 뷰

Spring/Spring Boot

@RequestBody, @ResponseBody

woo'^'chang 2022. 6. 16. 23:04

네트워크 통신

Network

컴퓨터 간의 데이터를 주고받기 위해서는 즉, 통신을 하기 위해서는 네트워크가 필요하다.

 

네트워크란 컴퓨터 노드들이 자원을 공유할 수 있도록 링크로 연결하여 하나의 망을 만든 것을 의미한다. 이러한 연결이 존재하기 때문에 노드(컴퓨터) 간의 데이터 교환이 가능하다.

 

다양한 컴퓨터 시스템 간의 통신을 위해서는 표준 프로토콜이 필요하게 되었고, 국제 표준화 기구(ISO)에서는 OSI Model을 정의하게 되었다. 최초로 정의된 모델은 7계층을 가지고 있었는데 표준이 지속적으로 갱신되면서 현재는 5계층 모델이 전 세계 표준으로 적용되고 있다.

HTTP

5계층 모델은 네트워크상의 프로토콜을 정의한 모델이고 인터넷상에서의 주요 프로토콜은 HTTP이다. HTTP는 하이퍼텍스트 전송 프로토콜로 TCP/IP 모델의 Application Layer에 속하는 프로토콜이다. 간단히 말하면 인터넷상에서 데이터 교환을 위한 규칙을 의미한다. HTTP의 특징은 다음과 같다.

  • 클라이언트가 요청을 생성하기 위한 연결을 연 다음 응답을 받을 때까지 대기하는 전통적인 클라이언트-서버 모델을 따른다. 이는 클라이언트에서 서버로의 단방향 통신을 의미하며, 서버는 클라이언트에게 요청하지 않고 요청에 대한 응답만 수행한다.
  • 무상태 프로토콜로 서버는 클라이언트의 상태를 저장하지 않는다. 이는 서버가 독립적으로 동작할 수 있게 하여 서버 단에 걸리는 부하가 줄어들어 서버의 성능을 높일 수 있는 장점이 된다.
  • 클라이언트의 요청에 대한 응답을 끝마치면 연결을 끊어버리는 비연결성의 특징을 가진다. 서버의 특성상 다수의 클라이언트를 가지게 되는데 비연결성을 통해 리소스를 효율적으로 관리할 수 있다. 하지만 요청이 필요할 때 매번 연결과정이 들어가는 오버헤드가 존재한다.

클라이언트 - 서버

데이터를 교환하는 통신에 대해 간단히 알아보았다. 이를 바탕으로 HTTP 요청과 응답에 대해 알아보려 한다.

Client

데이터를 주고받기 위해서는 프로토콜이라는 규칙안에서 이루어져야 하고 요청을 보내야 응답을 받을 수 있다는 것을 이해했다. 클라이언트는 요청과 응답을 간편하게 처리하기 위해서 웹 브라우저를 이용한다. 웹 브라우저란 웹 서버의 정보를 응답으로 받아 클라이언트가 보기 쉽도록 만들어주고, 문서 검색을 도와주는 응용프로그램이다.

 

URL을 입력하게 되면 HTTP를 바탕으로 요청 주소를 파악하게 되고 클라이언트가 입력한 데이터를 바탕으로 HTTP 요청 메시지를 만들어서 해당 주소로 요청을 보내게 된다. 이에 다양한 종류의 HTTP 요청 메시지가 존재하게 되는데 어떻게 파싱해서 해당 컨트롤러에 맞는 파라미터로 전달하게 되는 것일까? 또 컨트롤러의 반환 타입에 따라 어떻게 HTTP 응답 메시지를 만드는 걸까?

 

지금부터 Spring Server에서의 동작을 알아보도록 한다.

Request/Response

요청이 들어오면 프론트 컨트롤러에서 요청에 대한 공통적인 처리를 수행한 뒤, 해당 URL에 매핑된 핸들러를 찾고 핸들러 어댑터에게 역할을 위임한다. 핸들러 어댑터는 핸들러를 실행시키는데, 핸들러마다 필요로 하는 파라미터가 다르다. 파라미터 처리를 공통으로 수행할 수 있도록 구현된 인터페이스가 존재하는데 ArgumentResolver라고 한다.

 

도식화를 진행하면 다음과 같은 그림으로 이해할 수 있다.

출처 : https://www.inflearn.com/course/스프링-mvc-1/dashboard

핸들러 어댑터와 핸들러 사이에 존재하는 인터페이스로 핸들러가 필요로 하는 다양한 파라미터 값을 생성한다. 스프링은 30개가 넘는 ArgumentResolver 구현체를 기본적으로 제공한다.

실제 스프링에 존재하는 인터페이스는 HandlerMethodArgumentResolver이며, 다음의 구현이 필요하다.

  • supportsParameter
    • 해당 파라미터를 지원하는지 확인하고 결과에 따른 boolean 값을 반환한다.
  • resolveArgument
    • 지원 여부에 따라서 해당 객체를 생성한다.
    • 핸들러가 필요로 하는 파라미터가 모두 준비되고 나면 해당 핸들러를 호출하면서 값을 넘겨준다.

실습

public void requestBodyStringV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
    ServletInputStream inputStream = request.getInputStream();
    // 바이트 코드를 문자로 받을 때는 어떤 인코딩을 사용할지 항상 지정해줘야 한다.
    String message = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);

    log.info("message={}", message);

    response.getWriter().write("ok");
}

가장 원초적으로 요청을 처리하는 코드라고 볼 수 있다. HTTP 요청/응답을 편리하게 처리해주는 HttpServletRequest, HttpServletResponse 객체를 파라미터로 받아서 요청을 처리한다.

 

위의 과정을 통해 ArgumentResolver가 동작해 파라미터 객체들을 생성하여 핸들러에 전달하는 것을 이미 알고 있다.

 

request로부터 클라이언트 요청 이진 데이터를 얻을 수 있는 InputStream을 얻어내고 인코딩 작업을 통해 문자열로 변환한다. 또한 response로부터 출력스트림을 얻어 문자열을 출력한다.

 

이때 동작하는 ArgumentResolver 구현체는 다음과 같다.

  • ServletRequestMethodArgumentResolver

HttpServletRequest는 ServletRequest를 상속받아 구현되었는데 해당 파라미터를 지원하는지 확인한다.

  • ServletResponseMethodArgumentResolver

HttpServletResponse는 ServletResponse를 상속받아 구현되었는데 해당 파라미터를 지원하는지 확인한다.

public void requestBodyStringV2(InputStream inputStream, Writer responseWriter) throws IOException {
    String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);

    log.info("v2 messageBody={}", messageBody);

    responseWriter.write("ok");
}

위의 코드에서는 InputStreamWriter를 각각 HttpServletRequest, HttpServletResponse 객체에서 얻어와 사용했는데 이 과정마저 생략하고 싶다면 파라미터로 바로 받을 수 있다.

 

위의 사진을 확인하면 InputStreamWriter 클래스 지원도 확인하고 있기에 이러한 방식이 가능하다.

 

아직도 뭔가 불편한 점이 존재한다. InputStream을 받으면 인코딩 과정을 진행해야 하는데 이 과정마저 생략하고 간편하게 바로 메시지 바디의 내용을 확인할 방법은 없을까?

HttpEntity

SpringFramework에서 제공하는 클래스로 HTTP 헤더와 바디 정보로 구성되어 HTTP 요청과 응답 정보를 나타낼 수 있다. 아래 2개의 메서드가 대표적인 메서드이다.

 

getHeaders()

  • 헤더 정보들을 담는 HttpHeaders를 가져온다.
    • 헤더 정보에 포함되는 많은 항목을 담고 있는 클래스이다.
entity.getHeaders().getContentType();

getBody()

  • HTTP 메시지 바디의 내용을 가져온다.
entity.getBody();

RequestEntity

HttpEntity를 상속 받아 구현한 클래스이다. 클라이언트의 HttpRequest를 처리하기 위해 사용되며 요청 데이터를 포함하고 있다. HttpMethod, URI, Type 정보를 포함하고 있다. HttpEntity를 상속 받아 구현했기에 getBody() 메서드를 동일하게 사용할 수 있다.

ResponseEntity

HttpEntity를 상속 받아 구현한 클래스이다. 클라이언트에게 응답으로 보낼 데이터와 HTTP 상태 코드를 관리할 수 있다. HttpStatus, HttpHeaders, HttpBody를 포함하고 있다. 다음과 같이 사용할 수 있다.

public ResponseEntity<String> responseBodyV2() throws IOException {
    return new ResponseEntity<String>("ok", HttpStatus.OK);
}

@RequestBody

해당 어노테이션을 사용하면 더욱 간단하게 요청 값을 가져올 수 있다. 클라이언트에서 전달한 데이터와 받고자 하는 파라미터의 프로퍼티가 일치하면 별다른 설정 없이 해당 파라미터를 가져올 수 있다.

 

여기서 말하는 프로퍼티란 객체와 관련하여 이름 붙여진 속성을 말한다. 흔히 getter/setter를 묶어서 프로퍼티라 칭하고 있다.

public HelloData requestBodyJson(@RequestBody HelloData helloData) {

    log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());

    return helloData;
}

@Data
public class HelloData {
    private String username;
    private int age;
}

@Data 어노테이션을 사용하였기에 getter/setter가 존재하게 되고 JSON의 key를 username과 age로 설정한 뒤 값을 보내게 되면 자동으로 객체를 생성해서 파라미터로 넣어주게 된다.

@ResponseBody

일반 컨트롤러는 뷰의 논리적인 이름 반환을 통해 ViewResolver를 거쳐 클라이언트에게 View를 반환하게 된다. 뷰가 아닌 응답 데이터를 반환하고 싶은 경우 해당 어노테이션을 사용하면 된다. 반환되는 값이 ViewResolver를 거치지 않고 HTTP 응답 본문에 담겨 클라이언트에게 그대로 전송되게 된다. 물론 그 과정에서 클래스마다 알맞은 변환 과정을 거치게 된다.

@ResponseStatus(HttpStatus.OK)
@ResponseBody
@GetMapping("/response-body-json-v2")
public HelloData responseBodyJsonV2() {
    HelloData helloData = new HelloData();
    helloData.setUsername("userA");
    helloData.setAge(20);
    return helloData;
}

HttpMessageConverter

위와 같이 데이터의 변환을 신경 쓰지 않고 개발을 편리하게 진행할 수 있는 이유는 HttpMessageConverter가 이러한 역할을 수행하고 있기에 가능하다.

 

앞서 핸들러의 파라미터를 생성해주는 ArgumentResolver에 대해 알아보았다. 핸들러의 반환을 처리하는 과정에서도 ReturnValueHandler가 동작하는데 이것 또한 해당 반환을 지원하는지 확인 후 올바른 반환을 도와주는 객체이다.

 

클라이언트의 요청과 응답은 여러 형태로 나타날 수 있으므로 다양한 구현체가 필요로 하게 되고 따라서 HttpMessageConverter는 인터페이스를 사용한다. 이러한 다양한 컨버터 중 필요로 하는 컨버터를 찾는 방식은 요청에서는 Content-Type요청 데이터 타입을 통해 응답에서는 Accept반환 데이터 타입을 통해 선택된다. 우선순위가 존재하여 해당하지 않으면 다음 컨버터로 넘어가는 방식으로 탐색이 이루어진다.

HTTP 요청 데이터 읽기

@RequestBody나 HttpEntity를 사용하여 요청 데이터를 읽는 경우 적용되며, 다음과 같은 순서로 진행

1. HTTP 요청이 들어왔을 때 해당 핸들러에서 @RequestBody나 HttpEntity를 사용 중

2. 우선순위에 따라 메시지 컨버터에 구현되어 있는 canRead 호출

  • 대상 클래스 타입을 지원하는지, 해당 미디어 타입을 지원하는지 확인

3. 위의 조건을 만족한다면 read 호출해서 객체를 생성하고 반환

  • 만족하지 않는다면 다음 메시지 컨버터 탐색

4. ArgumentResolver는 해당 객체를 핸들러 파라미터로 전달

HTTP 응답 데이터 생성

@ResponseBody나 HttpEntity를 사용하여 응답 데이터를 생성하는 경우 적용되며, 다음과 같은 순서로 진행

1. 핸들러에서 @ResponseBody나 HttpEntity를 사용하여 응답 데이터를 반환

2. 우선순위에 따라 메시지 컨버터에 구현되어 있는 canWrite 호출

  • 대상 클래스 타입을 지원하는지, 해당 쓰기 미디어 타입을 지원하는지 확인

3. 위의 조건을 만족한다면 write 호출해서 객체를 생성하고 반환

  • 만족하지 않는다면 다음 메시지 컨버터 탐색

4. ReturnValueHandelr는 해당 객체를 핸들러 어댑터로 전달

대표적인 메시지 컨버터

ByteArrayHttpMessageConverter
  • byte[] 데이터를 처리한다.
  • 클래스 타입 : byte[]
  • 미디어타입 : /
  • 쓰기 미디어타입 : application/octet-stream
StringHttpMessageConverter
  • String 문자로 데이터를 처리한다.
  • 클래스 타입 : String
  • 미디어타입 : /
  • 쓰기 미디어타입 : text/plain
MappingJackson2HttpMessageConverter
  • 클래스 타입 : 객체 또는 HashMap
  • 미디어타입 : application/json
  • 쓰기 미디어타입 : application/json

'Spring > Spring Boot' 카테고리의 다른 글

복합키를 가지는 JPA 엔티티 생성하기  (1) 2022.09.14
[Spring Boot] 메시지, 국제화란  (0) 2022.07.30
[Error] 406 Not Acceptable HttpMediaTypeNotAcceptableException  (0) 2022.07.29
Logging  (0) 2022.06.16
MVC 어댑터 패턴  (1) 2022.06.16
댓글
최근에 올라온 글
최근에 달린 댓글
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Total
Today
Yesterday