Lined Notebook

[스프링 MVC - 백엔드 웹 개발 기술] 14. HTTP 요청 파라미터 - 쿼리 스트링과 HTML Form, @RequestParam, @ModelAttribute, 텍스트, JSON 요청 방식

by ymkim

01. HTTP 요청 파라미터 - QueryString, HTML Form

  • 이번 시간에는 쿼리 스트링HTML Form 데이터 전송 방식에 대해 알아본다.

01-1. 클라이언트 → 서버 → 데이터 전송 방식

  • 클라이언트 → 서버데이터 전송하는 방식은 아래 3가지 존재
  • GET - 쿼리 스트링(Query String)
    • /user?username=hello&age=20
    • URL 쿼리 파라미터에 데이터 포함 후 전달
    • 예) 검색, 필터, 페이징 등에서 사용
  • POST - HTML Form
    • Content-type : application/x-www-form-urlencoded
    • 메시지 바디에 쿼리 파라미터 형식으로 전달
    • username=hello&age=20
  • HTTP Message Body
    • HTTP API에서 사용 - JSON, XML, TEXT
    • 데이터 형식은 주로 JSON

01-2. 쿼리스트링, HTML Form 전송 방식 실습

  • HttpServletRequest의 request.getParameter() 사용하면 아래 2가지 요청 받을 수 있음

GET - 쿼리 파라미터 전송

POST - HTML Form 전송

POST /request-param ...
contenst-type: application/www-form-urlencoded

username=hello&age=20
  • 쿼리 파라미터, HTML Form 이든 구분없이 조회 가능
  • 위와 같은 2가지 방식을 요청 파라미터 방식 이라 한다

RequestParamController

package hello.springmvc.basic.request;

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

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

@Slf4j
@Controller
public class RequestParamController {

    @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");
    }
}
  • 쿼리 스트링, HTML Form 전송의 파라미터를 받기 위해 해당 컨트롤러 생성
  • 두 요청 방식에 상관없이 **request.getParameter(”인자”)**를 통해 값을 받는다

/static/basic/hello-form.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/request-param-v1" method="post">
    username: <input type="text" name="username" />
    age: <input type="text" name="age" />
    <button type="submit">전송</button>
</form>
</body>
</html>
  • HTML Form 전송 방식을 위해 해당 HTML 생성
  • /static/* 경로는 외부에서 바로 접근이 가능하기에 해당 경로에 HTML 파일 생성

02. HTTP 요청 파라미터 - @RequestParam

  • 스프링이 제공하는 @RequestParam을 사용하면 요청 파라미터 쉽게 사용 가능

02-1. requestParamV2 - @RequestParam 사용

package hello.springmvc.basic.request;

...

@Slf4j
@Controller
public class RequestParamController {

    @ResponseBody // @RestController와 같은 효과, Body에 HTTP Message를 반환
    @RequestMapping("/request-param-v2")
    public String requestParamV2(
            @RequestParam("username") String memberName,
            @RequestParam("age") int memberAge
    ) {
        log.info("memberName = {}, memberAge = {}", memberName, memberAge);
        return "ok";
    }
}
  • @RequestParam : 파라미터 이름 바인딩
  • @ResponseBody : View 조회 무시, HTTP Message body 직접 입력
  • @RequestParam(”username”) String memberName은 아래와 동일
    • → request.getParameter(”username”);

02-2. requestParamV3 - @RequestParam(”변수명”) 제거

@ResponseBody // @RestController와 같은 효과, Body에 HTTP Message를 반환
@RequestMapping("/request-param-v3")
public String requestParamV3(
        @RequestParam String username,
        @RequestParam int age
) {

    log.info("username = {}, age = {}", username, age);
    return "ok";
}
  • @RequestParam의 ‘(”username”)’ 은 생략이 가능하다
  • 생략 하려면 client에서 전송하는 변수명과 반드시 동일해야 함

02-3. requestParamV4 - @RequestParam 자체 제거

@ResponseBody // @RestController와 같은 효과, Body에 HTTP Message를 반환
@RequestMapping("/request-param-v4")
public String requestParamV4(String username, int age) {

    log.info("username = {}, age = {}", username, age);
    return "ok";
}
  • String, int, Integer 등의 단순 타입이면 @RequestParam 어노테이션 자체 제거 가능
  • 생략 하려면 client에서 전송하는 변수명과 동일해야 함
  • @RequestParam 어노테이션 자체를 없애는것은 그렇게 좋은 것 같지는 않음

02-4. required 사용

@ResponseBody
@RequestMapping("/request-param-required")
public String requestParamRequired(
        @RequestParam(required = true) String username,
        @RequestParam(required = false) int age
) {

    log.info("username = {}, age = {}", username, age);
    return "ok";
}
  • @RequestParam.required
    • 파라미터 필수값 여부
    • 기본값 true

02-5. defaultValue 사용

@ResponseBody
@RequestMapping("/request-param-default")
public String requestParamDefault(
        @RequestParam(required = true, defaultValue = "guest") String username,
        @RequestParam(required = false, defaultValue = "-1") int age
) {

    log.info("username = {}, age = {}", username, age);
    return "ok";
}
  • default value를 통해 값이 없는 경우에 대해 처리
  • 빈 문자의 경우에도 설정값이 적용이 된다

02-6. Param Map

@ResponseBody
@RequestMapping("/request-param-map")
public String requestParamMap(@RequestParam Map<String, Object>paramMap) {

    log.info("username = {}, age = {}", paramMap.getOrDefault("username", ""), paramMap.getOrDefault("age", 0));
    return "ok";
}
  • @RequestParam Map<String, Object> paramMap 을 통해 모든 파라미터를 매핑할 수 있음
  • Map은 Casting 등의 불필요한 연산이 필요하기에 웬만하면 지양하는 것이 좋음

03. @ModelAttribute

  1. 실제 개발을 하면 요청 파라미터(@RequestParam)를 받는다
  2. 객체를 생성하고, 해당 객체에 파라미터 값을 셋팅한다
  3. 보통 코드는 다음과 같이 작성
@RequestParam String username;
@RequestParam int age;

HelloData data = new HelloDate();
data.setUsername(username);
data.setAge(age);
  • 스프링은 위와 같은 작업을 자동화해주는 @ModelAttribute 제공
  • 먼저 요청 파라미터를 바인딩 받을 객체를 생성한다

03-1. HelloData

package hello.springmvc.basic;

import lombok.Data;

@Data
public class HelloData {
    private String username;
    private int age;
}
  • @Data : 아래 어노테이션 자동 생성
    • @Getter
    • @Setter
    • @ToString
    • @EqualsAndHashCode
    • @RequiredArgsConstructor
  • 기존에 파라미터 바인딩 방식은 다음 코드와 같다

03-2. @RequestParam 바인딩

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

    log.info("username = {}, age = {}", helloData.getUsername(), helloData.getAge());
    log.info("helloData = {}", helloData);
    return "ok";
}
  • @RequestParam을 통해 파라미터 바인딩
  • HelloData 객체 생성 후 setter를 통해 객체에 값 셋팅
  • 하지만 @ModelAttribute를 통해 위 과정 자동화 가능

03-3. @ModelAttribute 바인딩

@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
    log.info("username = {}, age = {}", helloData.getUsername(), helloData.getAge());
    log.info("helloData = {}", helloData);
    return "ok";
}
  • @RequestParam 어노테이션 제거 효과
  • @ModelAttribute 실행 방식
    • HelloData 객체 생성
    • 요청 파라미터 이름(username, age)으로 HelloData 객체의 프로퍼티를 찾는다
      • 즉, 요청 파라미터(username, age)의 getter/setter를 찾는다
    • 해당 프로퍼티setter를 호출하여 파라미터 값 바인딩
      • ex) 파라미터명 username → setUsername() 호출 후 바인딩
  • @ModelAttribute 바인딩 오류

03-4. @ModelAttribute 바인딩 생략

@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(HelloData helloData) {
    log.info("username = {}, age = {}", helloData.getUsername(), helloData.getAge());
    log.info("helloData = {}", helloData);
    return "ok";
}
  • @ModelAttribute 생략 가능
  • @RequestParam 생략 가능
  • argument resolver 지정 타입모두 @ModelAttribute로 지정이 된다
    • ex) argument resolver : HttpServletRequest, HttpServletResponse

03-5. @ModelAttribute vs @RequestParam vs @RequestBody

[Spring] @RequestParam, @RequestBody, @ModelAttribute의 차이

  1. 클라이언트의 요청을 객체로 바인딩 하는 3가지 방법
    1. @RequestParam
    2. @RequestBody
    3. @ModelAttribute
바인딩 종류 특징
@RequestParam - 1개의 HTTP 요청 파라미터를 얻기 위해 사용, 기본값 지정 가능
- 필수 여부 true이기에 없으면 required=false 선언
@ModelAttribute - 폼 형태(form) HTTP Body와 요청, 파라미터 객체 바인딩
- 기본적으로 생성자로 값 설정, 생성자로 설정되지 않은 필드 setter로 설정
- 생성자 혹은 setter가 없으면 값 저장이 안됨
- Body 값 받기 위해서 multipart/form-data 형식 전송 필요
- application/x-www-form-urlencoded 받을 수 있는데?
- 바인딩 타입에 맞춰 자동 검증
@RequestBody - application/json 형태 HTTP Body → MessageConverter → Java 객체 변환
  - HTTP 요청 본문 → Java 객체 → ObjectMapper 사용
  - 위 작업을 위해 → MappingJackson2HttpMessageConverter → Spring 도움 받음
- 메시지 변환 과정에서 객체 기본 생성자를 통해 객체 생성
- 내부적으로 Reflection을 사용해 값을 할당
- 값을 주입하기 위한 생성자, Setter 필요 없음
  • Content-Type : application/json
    • @RequestBody을 통해 바인딩
  • Content-Type : application/x-www-form-urlencoded
    • HTML Form(default type)
    • AJAX(default type)
      • @RequestParam을 통해 바인딩
      • @ModelAttribute을 통해 바인딩
  • Content-Type : multipart/form-data
    • 파일이나 이미지 같은 바이너리 데이터가 포함된 데이터 전송 시 사용
    • 클라이언트가 요청 시 폼 데이터를 여러 부분으로 나눠서 전송

지금까지 알아본 내용은 “요청 파라미터” 에 관한 내용이다. 다음에는 HTTP RequestBody에 데이터가 넘어오는 경우에 대해 알아본다.

 

04. HTTP 요청 메시지 - 텍스트

  • 서블릿에서 학습한 내용을 떠올리면 다음과 같다
  • HTTP message body에 데이터를 직접 담아 요청
    • HTTP API에서 주로 사용, JSON, XML, TEXT
    • 데이터 형식주로 JSON 사용
    • POST, PUT, PATCH
  • 요청 파라미터와 다르게 HTTP 메시지 바디에 데이터가 넘어오는 경우
    • @RequestParam | @ModelAttribute 사용 불가
      • 물론 HTML Form 형식 데이터는 데이터로 인정
    • 하지만 여기서의 포인트는 → @RequestBody 사용해야함
  • 먼저 가장 단순한 텍스트 메시지를 HTTP message body에 담아 전송하고 읽어보자
  • HTTP 메시지 바다 내용은 InputStream 을 통해 읽을 수 있다

04-1. RequestBodyStringController

HTTP Text 응답 후 출력 - V1

/**
 * HTTP Body에 TEXT 데이터 요청
 */
@PostMapping("/request-body-string-v1")
public void requestBodyString(HttpServletRequest request, HttpServletResponse response) throws IOException {
    ServletInputStream inputStream = request.getInputStream(); // body 정보를 읽는다
    String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);

    log.info("message = {}", messageBody);
    response.getWriter().write("ok");
}
  • HttpServletRequest, HttpServletResponse를 통해 HTTP Message Body 내용 출력
  • 이전에 서블릿에서 했던 내용과 크게 다른 부분은 없음

InputStream, Writer 사용 - V2

/**
 * 파라미터 인자에 InputStream, Writer를 통해 값 반환
 */
@PostMapping("/request-body-string-v2")
public void requestBodyStringV2(
        InputStream inputStream,
        Writer responseWriter) throws IOException {

    String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);

    log.info("message = {}", messageBody);
    responseWriter.write("ok");
}
  • InputStream, Writer를 통해 요청 값 출력

HttpEntity 사용 - V3

/**
 * HttpEntity 사용
 */
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) throws IOException {

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

    return new HttpEntity<>("ok");
}
  • InputStream, Writer와 같은 부분도 자동화 하고 싶은 경우 사용
  • HttpMessageConvert가 내부적으로 동작하여 위 부분을 자동화
    • HTTP Body안에 있는 내용을 문자로 바꿔서 HttpEntity에 저장해준다 (스프링)
  • 스프링은 다음 파라미터 지원
    • HttpEntity
      • HTTP Header, Body 정보 간단히 조회
      • 메시지 바디 정보 직접 조회
      • 요청 파라미터를 조회하는 기능과 관계 없음, @RequestParam x, @ModelAttribute x
    • HttpEntity응답에도 사용 가능
      • 메시지 바디 직접 반환
      • 헤더 정보 포함 가능
      • view 조회 x
    • HttpEntity상속받은 객체도 같은 기능 제공
      • RequestEntity<T>
        • HttpMethod, url 정보가 추가, 요청에서 사용
      • ResponseEntity<T>
        • HTTP 상태 코드 설정 가능, 응답에서 사용
        • return new ResponseEntity<String>(”Hello World”, responseHeaders, HttpStatus.CREATED);

스프링 MVC 내부 → HttpMessageConverter 사용 → HTTP message body 파싱 → 문자 or 객체로 변환

@RequestBody, @ResponseBody 사용 - V4

/**
 * @RequestBody, @ResponseBody 사용
 */
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) throws IOException {

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

    return "ok";
}
  • @RequestBody
    • HTTP message body를 편리하게 조회 가능
    • 참고로 헤더 정보가 필요하면 @RequestHeader를 사용하면 됨
    • 이렇게 바디를 직접 조회하는 기능은 @RequestParam, @ModelAttribute와 상관이 없음
  • @ResponseBody
    • 응답 결과를 HTTP message body에 담아 전달
    • 물론 이 경우에도 view를 사용하지 않음
  • 자동으로 해준다는 의미?
    • InputStream, Writer… 등등
    • 위와 같은 부분은 HttpMessageConverter를 통해 객체, 문자로 자동 변환

04-2. 요청 파라미터 vs HTTP 메시지 바디

  • 요청 파라미터를 조회하는 기능 : @RequestParam, @ModelAttribute
  • HTTP 메시지 바디 조회 : @RequestBody

05. HTTP 요청 메시지 - JSON

  • 이전에는 HTTP 요청 메시지 Body - TEXT 형태 조회
  • 이번에는 HTTP 요청 메시지 Body - JSON 형태 조회

05-1. RequestBodyJsonController

HttpServletRequest, HttpServletResponse - V1

@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 = mapper.readValue(messageBody, HelloData.class); // json to object -> 역직렬화
    log.info("username = {}, age = {}", helloData.getUsername(), helloData.getAge());

    response.getWriter().write("ok");
}
  • 이전과 달라지는 부분은 Request(요청)으로 JSON 데이터가 들어옴
  • JSON 데이터 파싱을 위해 ObjectMapper 객체 사용

@RequestBody - String - V2

@ResponseBody
@PostMapping("/request-body-json-v2")
public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {

    log.info("messageBody = {}", messageBody);
    HelloData helloData = mapper.readValue(messageBody, HelloData.class); // json to object -> 역직렬화
    log.info("username = {}, age = {}", helloData.getUsername(), helloData.getAge());

    return "ok";
}
  • @RequestBody를 통해 Body 데이터 출력
  • 이전과 달라지는 부분은 없음

@RequestBody - HelloData DTO - V3

@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData helloData) {

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

    return "ok";
}
  • @RequestBody에 HelloData 객체를 매개변수로 받는다
  • 어떻게 이렇게 했는데 값이 셋팅되는 걸까??
    • @RequestBody HelloData data
    • @RequestBody에 직접 만든 객체 지정 가능
    • 이렇게 변환되는 이유는 아래와 같다
  • HttpEntity, @RequestBody를 사용하면 HTTP Message Converter가 HTTP 메시지 바디의 내용을 우리가 원하는 문자나 객체로 자동 변환시킨다
    • MappingJackson2HttpMessageConverter
  • HTTP Message Converter는 JSON 객체도 변환
    • V2에서 했던 내용을 대신 처리
    • HelloData data = mapper.readValue(data, HelloData.class); → **역직렬화**
  • @RequestBody는 메서드의 매개변수 영역에서 생략하면 @ModelAttribute로 처리한다
    • 스프링은 @ModelAttribute, @RequestParam 생략시 다음 규칙 적용
      • String, int, Integer 같은 단순 타입은 @RequestParam로 치환한다
      • 나머지는 @ModelAttribute로 치환된다

HTTP 요청 시 content-type이 application/json인지 확인해야함 확인하지않으면 MessageConverter에 의해 변환이 되지 않음.

HttpEntity - V4

@ResponseBody
@PostMapping("/request-body-json-v4")
public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity) {
    HelloData data = httpEntity.getBody();
    log.info("username = {}, age = {}", data.getUsername(), data.getAge());
    return "ok";
}
  • HttpEntity의 사용방법은 이전과 동일

@RequestBody / @ResponseBody Message Converter - V5

@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
    log.info("username = {}, age = {}", data.getUsername(), data.getAge());
    return data;
}
  • @RequestBody가 있으면 들어올때도 HttpMessageConverter 적용
    • JSON 요청 → HTTP Message Converter → 객체
  • @ResponseBody가 있으면 나갈때도 HttpMessageConverter 적용
    • 객체 → HTTP Message Converter → JSON
  • 직렬화, 역직렬화 자동 수행

 

 

블로그의 정보

기록하고, 복기하고

ymkim

활동하기