Web/Spring

[Inflearn] 스프링 MVC (5) HTTP 요청

UL :) 2022. 1. 28. 18:44

김영한 강사님의 '스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술' 강의 정리

2022.01.28 진행

강의를 듣고 개인적으로 정리한 글입니다. 코드와 그림 출처는 김영한 강사님께 있습니다. 문제 있을 시 알려주세요.

💡 실무 핵심 정리

요청 파라미터 조회 (GET, POST) :  @RequestParam, @ModelAttribute 
HTTP 메시지 바디를 직접 조회 : @RequestBody

1. HTTP 요청

클라이언트에서 서버로 요청 데이터를 전달하는 방식은 주로 3가지가 있다.

  1. GET - 쿼리 파라미터 전송
  2. POST - HTML Form 전송 (메시지 Body에 쿼리 파라미터 형식으로 담음)
  3. HTTP 메시지 바디에 데이터를 직접 담아서 요청 HTTP API에서 주로 사용 (주로 JSON)

 

2. HTTP 요청 헤더 조회

HttpMethod, Locale, @RequestHeader, @CookieValue 를 사용해서 헤더를 조회할 수 있다.

package hello.springmvc.basic.request;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

@Slf4j
@RestController
public class RequestHeaderController {

    @RequestMapping("/headers")
    public String headers(HttpServletRequest request,
                          HttpServletResponse response,
                          HttpMethod httpMethod,
                          Locale locale,
                          @RequestHeader MultiValueMap<String, String> headerMap,
                          @RequestHeader("host") String host,
                          @CookieValue(value= "myCookie", required = false) String cookie
                        ){
        log.info("request={}", request);
        log.info("response={}", response);
        log.info("httpMethod={}", httpMethod);
        log.info("locale={}", locale);
        log.info("headerMap={}", headerMap);
        log.info("header host={}", host);
        log.info("myCookie={}", cookie);
        return "ok";
    }
}

실행결과 - 로그

 

3. GET, POST 요청 파라미터 조회

3.1. 기본

HttpServletRequestrequest.getParameter()를 사용하면 GET,POST 방식의 요청 파라미터를 조회 할 수 있다.

package hello.springmvc.basic.request;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Slf4j
@Controller
public class RequestParamController {

    /**
     * HttpServletRequest 가 제공하는 방식으로 요청 파라미터 조회
     */
    @RequestMapping("/request-param-v1")
    public void requestParamV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));
        log.info("username={}, age={}", username, age);

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

 

3.2. @RequestParam ✔

@RequestParam을 사용해서 파라미터를 더! 간단하게 조회하는 법을 알아보자.

    /**
     * @RequestParam 사용해서 조회
     */
    @ResponseBody //RestController랑 같은 효과
    @RequestMapping("/request-param-v2")
    public String requestParamV2(
            @RequestParam("username") String memberName,
            @RequestParam("age") int memberAge){

        log.info("username={}, age={}", memberName, memberAge);
        return "ok";
    }

    /**
     * 파라미터이름과 변수이름이 같으면
     * ("파라미터이름") 생략 가능
     */
    @ResponseBody
    @RequestMapping("/request-param-v3")
    public String requestParamV3(
            @RequestParam String username,
            @RequestParam int age){

        log.info("username={}, age={}", username, age);
        return "ok";
    }

    /**
     * 변수가 기본형이고
     * 파라미터이름과 변수이름이 같으면
     * @RequestParam 애노테이션까지 생략 가능하긴 함
     */
    @ResponseBody
    @RequestMapping("/request-param-v4")
    public String requestParamV4(String username, int age){
        log.info("username={}, age={}", username, age);
        return "ok";
    }

 

파라미터 필수 여부 설정 - required

  • reauired속성은 기본적으로 true로 설정되어있다.
    • 즉 파라미터에 값이 없는 경우 400 에러가 발생한다.
  • false로 설정하면 파라미터에 값이 없어도 작동하지만, HTTP 스펙 상 파라미터에 값이 없으면 에러처리를 하는 것이 맞다.
    /**
     *  파라미터 필수여부
     *  required = true(default)
     */
    @ResponseBody
    @RequestMapping("/request-param-required")
    public String requestParamRequired(
            @RequestParam(required = true) String username,
            @RequestParam(required = false) Integer age){

        log.info("username={}, age={}", username, age);
        return "ok";
    }
}
  • 파라미터 username의 값이 없으면 400 에러가 발생한다.
  • ※ 주의 기본형에 null 입력 불가능
int a = null; //int는 기본형 null 입력 불가능
Integer a = null;

int 대신 Integer를 사용하거나 defaultValue를 사용해서 해결한다.

  • ※ 주의 파라미터 이름만 사용 시 빈문자로 통과

http://localhost:8080/request-param-required?username=

파라미터 기본값 설정 - defaultValue

  • 파라미터에 값이 없는 경우 설정한 기본 값을 적용한다.
  • 빈 문자의 경우에도 적용된다.

파라미터를 Map으로 조회하기

    /**
     * @RequestParam Map, MultiValueMap
     * Map(key=value)
     * MultiValueMap(key=[value1, value2, ...] ex) (key=userIds, value=[id1, id2])
     */
    @ResponseBody
    @RequestMapping("/request-param-map")
    public String requestParamMap(
            @RequestParam Map<String, Object> paramMap) {
        log.info("username={}, age={}", paramMap.get("username"), paramMap.get("age"));
        return "ok";
    }

key 하나당 value가 여러개일 수 있는 MultiValueMap으로도 조회할 수 있다. 하지만 보통 파라미터 값은 1개이다.

 

3.3. @ModelAttribute ✔

@ModelAttribute는 요청 파라미터를 받아서 필요한 객체를 생성하고 값을 넣어주는 과정을 스프링이 자동화해주는 기능이다.

그 과정은 다음과 같다.

  • 객체를 생성한다.
  • 요청 파라미터의 이름으로 객체의 프로퍼티를 찾는다.
    • 예) username
  • 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 넣어준다.

프로퍼티란 : 객체에 getXxx() 라는 메서드가 있으면 이 객체는 xxx라는 프로퍼티를 가지고 있는 것이다.

package hello.springmvc.basic;

import lombok.Data;

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

    @ResponseBody
    @RequestMapping("/model-attribute-v1")
    public String modelAttributeV1(@ModelAttribute HelloData helloData){
//        HelloData helloData = new HelloData();
//        helloData.setUsername(username);
//        helloData.setAge(age);

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

스프링은 String, int, Integer 같은 단순 타입은 @RequestParam이 붙어 생략할 수 있고
나머지 클래스는 @ModelAttribute가 붙어 생략이 가능하다. (argument reslover로 지정해둔 타입을 제외하고)

    @ResponseBody
    @RequestMapping("/model-attribute-v2")
    public String modelAttributeV2(HelloData helloData){
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
        return "ok";
    }

 

4. HTTP 요청 메시지 조회

HTTP 메시지 바디를 통해 직접 데이터가 넘어오는 경우는 @RequestParam, @ModelAttribute를 사용할 수 없다.

4.1. 단순 텍스트

  • InputStream을 사용해서 메시지 바디를 직접 읽을 수 있지만 매우 번거롭다.
  • 스프링 MVC는 @Controller에서 InputStream(Reader), OutputStream(Writer)를 파라미터로 지원한다.
  • 이를 사용해 메시지 바디의 내용을 직접 조회/출력 할 수 있다.
@Slf4j
@Controller
public class RequestBodyStreamController {

    @PostMapping("/request-body-string-v1")
    public void requestBodyString(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ServletInputStream inputStream = request.getInputStream();

        //Stream은 bytecode이다. bytecode로 문자를 받을 때는 항상 인코딩을 지정해야한다.
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
        log.info("messageBody={}",messageBody);
        response.getWriter().write("ok");
    }

    @PostMapping("/request-body-string-v2")
    public void requestBodyStringV2(InputStream inputStream, Writer responseWriter) throws IOException {

        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
        log.info("messageBody={}",messageBody);
        responseWriter.write("ok");
    }

HttpEntity, RequestEntity, ResponseEntity

  • 스프링 MVC는 HttpEntity를 파라미터로 지원한다.
    • 메시지 바디 정보를 직접 조회할 수 있다. 헤더또한 조회 가능하다.
    • 메시지 바디 정보를 직접 반환하는 등 응답에도 사용 가능하다. (헤더 정보 포함 가능)
    @PostMapping("/request-body-string-v3")
    public HttpEntity requestBodyStringV3(HttpEntity<String> httpEntity) {

        String messageBody = httpEntity.getBody();
        log.info("messageBody={}",messageBody);

        return new HttpEntity<>("ok");
    }
  • HttpEntity를 상속받은 RequestEntity, ResponseEntity 는 더 많은 기능을 제공한다.
    • 요청: HttpMethod, url 정보 추가 / 응답: HTTP 상태 코드 설정

스프링 MVC 내부에서 HttpMessageConverter라는 기능을 사용해서, HTTP 메시지 바디를 읽은 후 문자나 객체로 변환해서 전달해준 것이다.

@RequestBody, @ResponseBody ✔

실무에서 이 방식을 제일 많이 쓴다. HTTP 메시지 바디 정보를 직접 조회, 반환 할 수 있는 가장 편리한 방식이다.

    @ResponseBody
    @PostMapping("/request-body-string-v4")
    public String requestBodyStringV3(@RequestBody String messageBody) {
        log.info("messageBody={}",messageBody);
        return "ok";
    }

 

4.2. JSON

  • JSON은 Javascript Object Notation의 약자로 이름과 같이 자바스크립트의 객체처럼 키:값으로 구성되며, 클라이언트와 서버 간 HTTP 통신을 위한 텍스트 데이터 포맷이다.
    • 키와 값을 ""로 감싸야 한다.
    • 각 객체를 배열로 묶을 수 있다.

  • 가장 기본적으로는 InputStream을 사용해서 메시지 바디를 직접 읽고, 읽은 JSON 데이터를 Jackson 라이브러리에서 제공하는 ObjectMapper 클래스를 이용해서 역직렬화한다. (com.fasterxml.jackson.databind.ObjectMapper)
    • 스트링부트는 spring-boot-starter-web에서 Jackson 라이브러리를 포함한다.
  • 단순 텍스트를 처리할 때와 마찬가지로 @RequestBody, @ResponseBody 를 사용하면 메시지 바디를 더 편리하게 조회할 수 있다.

직렬화(Serialize)와 역직렬화(Deserialize)

package hello.springmvc.basic.request;

@Slf4j
@Controller
public class RequestBodyJsonController {

    private ObjectMapper objectMapper = new ObjectMapper();

    @PostMapping("/request-body-json-v1")
    public void requestBodyJsonV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ServletInputStream inputStream = request.getInputStream();
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);

        log.info("messageBody={}", messageBody);
        HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());

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

    /* @RequestBody, @ResponseBody 사용 */
    @ResponseBody
    @PostMapping("/request-body-json-v2")
    public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
        log.info("messageBody={}", messageBody);
        HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
        log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());

        return "ok";
    }

 }

 

@RequestBody - 자동 직렬&역직렬화

  • HttpEntity@RequestBody를 사용하면 스프링의 HttpMessageConverter가 HTTP 메시지 바디의 내용을 문자나 객체로 변환해준다.
  • HttpMessageConverter는 HTTP 요청의 content-typeapplication/json 일 경우 JSON을 우리가 원하는 객체로 변환해준다. 즉 V2에서 했던 역직렬화 작업을 대신 처리해준다.
    • @RequestBody를 생략하면 @ModelAttribute가 적용되기 때문에 HTTP 바디가 아닌 요청 파라미터를 처리하게 된다, 따라서 생략이 불가능하다.
/* @RequestBody 생략 불가능(@ModelAttribute 가 적용되어 버림)
 * HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter
 */
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData data) {
    log.info("username={}, age={}", data.getUsername(), data.getAge());
    return "ok";
}

/* HttpEntity 사용 */  
@ResponseBody  
@PostMapping("/request-body-json-v4")  
public String requestBodyJsonV3(HttpEntity<HelloData> httpEntity) {  
     HelloData data = httpEntity.getBody();  
     log.info("username={}, age={}", data.getUsername(), data.getAge());  

     return "ok";  
}
  • 뿐만 아니라 다시 우리가 만든 객체를 직렬화하여 HTTP 메시지 바디에 넣어 JSON 응답을 보낼 수 있다.
    • 이때 HTTP 요청의 Acceptapplication/json 이어야 한다.
@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
    log.info("username={}, age={}", data.getUsername(), data.getAge());
    return data; //HelloData 객체가 HTTP메시지컨버터에 의해 JSON으로 바뀌어서 HTTP 메시지 응답에 넣어져 나간다.
}