관리 메뉴

나구리의 개발공부기록

스프링 MVC - 기본기능, HTTP요청 (기본/헤더 조회), HTTP 요청 파라미터(쿼리파라미터/HTML Form/@RequestParam/@ModelAttribute), HTTP 요청메시지(단순텍스트/JSON) 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

스프링 MVC - 기본기능, HTTP요청 (기본/헤더 조회), HTTP 요청 파라미터(쿼리파라미터/HTML Form/@RequestParam/@ModelAttribute), HTTP 요청메시지(단순텍스트/JSON)

소소한나구리 2024. 3. 3. 20:42

  출처 : 인프런 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 (유료) / 김영한님  
  유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용  

https://inf.run/Gmptq


1. HTTP 요청 - 기본, 헤더 조회

1) HTTP 헤더 정보 조회

(1) RequestHeaderController

  • basic하위에 request패키지를 만들어서 작성
  • HttpMethod: HTTP 메서드를 조회
  • Locale: Locale 정보를 조회
  • @RequestHeader MultiValueMap<String, String> headerMap: 모든 HTTP헤더를 MultiValueMap형식으로 조회
  • @RequestHeader("host") String host: 특정 HTTP 헤더를 조회
  • @CookieValue(value = "myCookie", required = false) String cookie: 특정 쿠키를 조회
  • @RequestHeader와 @CookieValue에는 required옵션으로 필수 값 여부를 줄 수 있으며 기본 값은 defaultValue임
package hello.springmvc.basic.request;

@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("host={}", host);
        log.info("cookie={}", cookie);
        return "ok";
    }
}

 

(2) MultiValueMap

  • MAP과 유사한데, 하나의 Key에 여러 값을 받을 수 있음
  • HTTP header, HTTP쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용 (keyA=value1&keyA=value2)
  • 반환시 배열로 반환
MultiValueMap<String, String> map = new LinkedMultiValueMap();
map.add("keyA", "value1");
map.add("keyA", "value2");

//[value1, value2]
List<String> values = map.get("keyA");

 

** 참고


2. HTTP 요청 파라미터 - 쿼리 파라미터, HTML Form

1) HTTP 요청 데이터 조회

(1) 클라이언트에서 서버로 요청 데이터를 전달할 때 주로 사용하는 3가지 방식

  • GET - 쿼리 파라미터: 메시지 바디 없이 URL의 쿼리 파라미터에 데이터를 포함해서 전달
  • POST - HTML Form: 메시지 바디에 쿼리 파라미터 형식으로 전달
  • POST,PUT,PATCH - HTTP message body에 데이터를 직접 담아 요청: HTTP API에서 주로 사용하고 데이터 형식은 주로 JSON을 사용

(2) 요청 파라미터 - 쿼리 파라미터, HTML Form 

  • HttpServletRequest의 request.getParameter()를 사용하면 GET, 쿼리 파라미터 전송 방식과 POST, HTML Form 요청 파라미터를 조회할 수 있음
  • GET 쿼리 파라미터 전송 방식과 POST HTML Form 전송 방식이 둘다 형식이 같아서 구분없이 조회가 가능한데 이것을 간단히 요청 파라미터 조회(request parameter)라고 함

(3) RequestParamController

  • 스프링으로 요청 파라미터를 조회하는 컨트롤러
  • requestParamV1 메서드에서는 단순히 HttpServletRequest가 제공하는 방식으로 요청 파라미터를 조회함
package hello.springmvc.basic.request;

@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");
    }
}

 

(4) Post Form 페이지 생성, hello-form.html

  • 테스트용 HTML Form
  • resources/static/basic 경로에 hello.form.html을 생성
  • 애플리케이션을 실행하여 localhost:8080/basic/hello-form.html로 접속하면 해당 입력 폼이 뜸
<!DOCTYPE html>
<html lang="en">
<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>

 

** 참고

  • Jar를 사용하면 webapp 경로를 사용할 수 없으므로 정적 리소스는 /resources/static 아래에 두면 스프링 부트가 자동으로 인식함

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

1) @RequestParam

(1) requestParamV2 - @RequestParam 적용

  • @RequestParam : 파라미터 이름으로 바인딩, request.getParameter()와 같은 효과
  • @ResponseBody : View 조회를 무시하고 HTTP message body에 직접 해당 내용을 입력
  • @RequestParam의 ("value") 속성이 파라미터 이름으로 사용됨
@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";
}

 

(2) requestParamV3 - 파라미터이름생략

  • HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam(name = "xxx")에 속성값을 생략할 수 있음
/**
 * HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam(name="xxx")의 name을 생략 가능
 */
@ResponseBody
@RequestMapping("/request-param-v3")
public String requestParamV3(@RequestParam String username,
                             @RequestParam int age) {
    log.info("username={}, age={}", username, age);
    return "ok";
}

 

(3) requestParamV4 - 완전생략

  • 타입이 String, int, Integer등의 단순 타입이고 변수명이 같으면 @RequestParam도 생략이 가능함
  • @RequestParam 애노테이션을 생략하면 required=false로 적용 됨
  • 이렇게 애노테이션을 완전히 생략해도 되지만 너무 없는 것도 과할 수 있으니 같이 협업하는 개발자들과의 성향에 맞게 사용
/**
 * String, int등의 단순타입이면서 매개변수명과 요청 파라미터 명이 일치할 경우 @RequestParam도 생략이 가능
 */
@ResponseBody
@RequestMapping("/request-param-v4")
public String requestParamV4(String username, int age) {
    log.info("username={}, age={}", username, age);
    return "ok";
}

 

** 참고 - 스프링 부트 3.2 부터 파라미터 이름 인식 문제가 발생함

 java.lang.IllegalArgumentException: Name for argument of type [java.lang.String]
 not specified, and parameter name information not found in class file either.
  • @RequestParam, @PathVariable 사용시 위 예외가 발생하면 애노테이션에 이름을 생략하지 않고 항상 이름을 적어주거나 인텔리제이 세팅에서 Build and run using을 Gradle로 설정하여 사용하면 됨

(4) requestParamRequired - 필수값 설정

  • 파라미터 필수 여부를 설정(required=true가 기본값)
  • 필수 값 여부를 true로 설정했는데 필수 값 없이 요청이 들어오면 400예외가 발생함

** 주의!

  •  username이 필수 값으로 설정 되어있는데도 불구하고 /request-param-required?username= 처럼 파라미터 이름만 있고 값은 없이 요청이 오면 username의 값이 빈문자열로 오기 때문에 통과가 됨(null과 "" 는 다름)
  • 기본형에 null이 입력하는 것은 불가능 하기 때문에(500예외 발생) Integer등의 래퍼 클래스를 사용하거나 defaultValue를 사용해야함
/**
 * 요청파라미터에 필수 값을 설정 required = true -> 기본값
 * 만약 요청이 ?username= 으로 온다면 빈문자("")로 통과됨(null이 아님)
 */
@ResponseBody
@RequestMapping("/request-param-required")
public String requestParamRequired(
        @RequestParam(required = true) String username,
        @RequestParam(required = false) Integer age) { // int사용 불가(int에 null을 입력할 수 없음)
    log.info("username={}, age={}", username, age);
    return "ok";
}

 

(5) requestParamDefault - 기본값 설정

  • 파라미터에 값이 없는 경우 기본 값을 적용할 수 있음
  • 이미 기본값이 있기 때문에 required는 의미가 없어서 생략해도 됨
  • 빈 문자열("")인 경우에도 설정한 기본 값이 적용되며, 기본 값 덕분에 null이 올 수 없으므로 기본형 타입을 사용해도 됨
/**
 * 요청 파라미터에 값이 없을 경우 디폴트 값을 설정 할 수 있음
 * 빈 문자의 경우에도 적용 -> ?username=
 */
@ResponseBody
@RequestMapping("/request-param-default")
public String requestParamDefault(@RequestParam(defaultValue = "guest") String username,
                                  @RequestParam(defaultValue = "-1") int age) {
    log.info("username={}, age={}", username, age);
    return "ok";
}

 

(6) requestParamMap - Map으로 조회

  • 파라미터를 Map(파라미터의 값이 1개일 경우, 대부분의 경우)이나 MultiValueMap(파라미터의 값이 여러개인 경우, 거의없음)으로 조회가능
  • Map은 key=value의 형태로 MultiValueMap은 배열의 형태로 조회됨
/**
 * Map으로 조회 -> Map(key=value)
 * MultiValueMap로도 조회 가능(파라미터의 값이 여러개일 경우) -> MultiValueMap(key=[value1,value2...])
 */
@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";
}

4. HTTP 요청 파라미터 - @ModelAttribute

1) @ModelAttribute

  • 실제 개발을 해보면 요청 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어주어야하는데, 스프링은 이 과정을 완전히 자동화 해주는 기능을 제공함

(1) HelloData

  • basic패키지에 요청 파라미터를 바인딩 받을 객체를 생성
  • 롬복 @Data: @Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor를 자동으로 적용해줌
@Data
public class HelloData {
    private String username;
    private int age;
}

 

(2) modelAttributeV1 - @ModelAttribute 사용 전

  • 요청 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어주어야 함
/**
 * @ModelAttribute 적용 전
 */
@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={}", username, age);
    log.info("helloData={}", helloData);
    return "ok";
}

 

(3) modelAttributeV1 - @ModelAttribute 사용 후

  • 매개변수에 HelloData를 입력하면 알아서 객체가 생성이 되고 요청 파라미터의 값도 모두 들어가있음
/**
 * @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";
}

 

(4) 스프링 MVC의 @ModelAttribute의 실행 과정

  • HelloData객체 생성(바인딩 받을 객체 생성)
  • 요청 파라미터의 이름으로 HelloData 객체의 프로퍼티를 찾고 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩)
  • 예를 들어 파라미터 이름이 username이면 setUserName()메서드를 찾아서 호출하면서 값을 입력

** 프로퍼티란?

  • 객체에 getUsername(), setUsername() 메서드가 있을 때 해당 객체는 username이라는 프로퍼티를 가지고 있다고 함
  • 이때 username의 프로퍼티의 값을 변경하면 setUsername()이 호출되고, 조회하면 getUsername()이 호출됨

** 바인딩 오류

  • int타입으로 설정 되어있는 age에 문자 등을 넣으면 BindException이 발생함
  • 이런 바인딩 오류를 처리하는 방법은 추후 검증 부분에서 다룸

(5-1) @ModelAttribute 적용 V2 - 생략

  • @ModelAttribute도 생략을 할 수 있음
  • 그러나 @RequestParam도 생략할 수 있어 혼란이 있을 수 있음

(5-2) @ModelAttribute 애노테이션 생략시 스프링은 다음과 같이 동작함

  • String, int, Integer등과 같은 단순 타입은 @RequestParam으로 동작
  • 나머지는 @ModelAttribute로 동작(단, argument resolver로 지정해둔 타입은 제외하며 argument resolver는 추후에 다룸)
/**
 * @ModelAttribute도 생략이 가능
 */
@ResponseBody
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2(HelloData helloData) {
    log.info("username={}, age={}",helloData.getUsername(), helloData.getAge());
    log.info("username={}",helloData);
    return "ok";
}

5. HTTP  요청 메시지 - 단순텍스트

1) 단순 텍스트

(1) HTTP message body에 데이터를 직접 요청

  • HTTP API에서 주로 사용 : JSON(주로사용), XML, TEXT
  • POST, PUT PATCH
  • 요청 파라미터와 다르게 HTTP 메시지 바디를 통해 데이터가 직접 넘어오는 경우는 @RequestParam, @ModelAttribute를 사용할 수 없음(HTML Form형식은 요청 파라미터로 인정 됨)
  • Postman을 활용하여 테스트

(2) RequestBodyStringController - requestBodyStringV1

  • request.getInputStream()을 사용해서 직접 읽는 가장 단순한 형식
package hello.springmvc.basic.request;

@Slf4j
@Controller
public class RequestBodyStringController {

    @PostMapping("/request-body-string-v1")
    public void requestBodyStringV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ServletInputStream inputStream = request.getInputStream();
        String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
        log.info("messageBody={}", messageBody);
        response.getWriter().write("ok");
    }
}

 

(3) Input, Output 스트림, Reader - requestBodyStringV2

  • InputStream(Reader) : HTTP 요청 메시지 바디의 내용을 직접 조회
  • OutputStream(Writer) : HTTP 응답 메시지 바디에 직접 결과 출력
// InputStream(Reader), OutStream(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("messageBody={}", messageBody);

    responseWriter.write("ok");
}

 

(4) HttpEntity<> - requestBodyStringV3

  • 스프링 MVC가 지원하는 HttpEntity : HTTP header, body 정보를 편리하게 조회 할 수 있음
  • HttpEntity는 메시지 바디 정보를 직접 조회하며 요청 파라미터를 조회하는 기능(@RequestParam, @ModelAttribute)과 관계가 없으며 응답에서도 사용 가능함
    (응답에서 사용시 - 메시지 바디 정보 직접 반환, 헤더 정보 포함 가능, view조회 X)
  • HttpEntity를 상속받은 객체들도 추가 기능을 제공함
    - RequestEntity : HttpMethod, url정보 추가, 요청에서 사용
    - ResponseEntity : HTTP 상태코드 설정 가능, 응답에서 사용

** 참고

  • HTTP 메시지 컨버터(HttpMessageConverter): 스프링 MVC 내부에서 HTTP 메시지 바디를 읽어서 문자나 객체로 변환해서 전달해줄때 사용하는 기능
// HTTP 메시지 컨버터 활용 - HttpEntity<타입> : 스프링이 HTTP 바디에 있는것을 타입으로 바꿔서 컨버터가 동작함
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) throws IOException {
    String messageBody = httpEntity.getBody();
    log.info("messageBody={}", messageBody);

    return new HttpEntity<>("ok");
    
    // ResponseEntity사용 예시
    // return new ResponseEntity<String>("Hello World", HttpStatus.CREATED);
}

 

(5) @RequestBody, @ResponseBody - requestBodyStringV4(실무에서 가장많이 사용)

  • @RequestBody를 사용하면 HTTP 메시지 바디 정보를 매우 편리하게 조회가 가능함
  • 헤더 정보가 필요하면 @RequestHeader나 HttpEntity를 사용하면 됨
  • 마찬가지로 요청 파라미터를 조회하는 @RequestParam, @ModelAttribute와 관계가 없음
  • @ResponseBody로 응답 결과를 HTTP 메시지 바디에 직접 담아서 전달 할 수 있으며 이 경우에도 뷰는 사용하지 않음
// @RequestBody, @ResponseBody 애노테이션 사용 - 실무에서 제일 많이 씀
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) {
    log.info("messageBody={}", messageBody);
    return "ok";
}

 

(6) 요청 파라미터 VS HTTP 메시지 바디

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

6. HTTP 요청 메시지 - JSON

1) JSON

(1) RequestBodyJsonController - requestBodyJsonV1

  • 서블릿에서 사용했던 방식처럼 HttpServletRequest의 InputStream을 사용하여 직접 HTTP 메시지 바디에서 데이터를 읽어와서 문자로 변환
  • 문자로된 JSON 데이터를 Jackson 라이브러리인 objectMapper를 사용해서 자바 객체로 변환후 응답
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 data = objectMapper.readValue(messageBody, HelloData.class);
        log.info("username={}, age={}", data.getUsername(), data.getAge());
        response.getWriter().write("ok");
    }
}

 

(2) @RequestBody 문자 변환, @ResponseBody - requestBodyJsonV2

  • @RequestBody를 활용해서 HTTP 메시지에서 데이터를 꺼낸 후 messageBody에 저장하고 objectMapper를 통해 자바 객체로 변환 후 반환
  • 그러나 아직도 문자로 변환하고 다시 json으로 변환하는 과정이 불편함
/**
 * @ResponseBody
 * HttpMessageConverter 사용 -> StringHttpMessageConverter 적용
 */
@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";
}

 

(3) @RequestBody 객체 변환 - requestBodyJsonV3(가장 편리)

  • @RequestBody HelloData data : @RequestBody에 직접 만든 객체를 지정할 수 있음
  • HttpEntity나 @RequestBody를 사용하면 HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을 우리가 원하는 문자나 객체로 변환해주는데, JSON도 객체로 변환해줌
  • 즉, V2에서 objectMapper.readValue() 코드의 작업을 대신 처리해줌
  • @RequestBody는 생략이 불가능: 생략하면 @ModelAttribute가 적용되어 요청파라미터를 찾는 코드가 되어버려서 기본값이 반환됨
/**
 * @RequestBody 생략 불가능(생략하면 @ModelAttribute가 적용됨)
 * HttpMessageConverter 사용 -> MappingJackson2HttpMessageConverter (content-type:application/json)
 */
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData data) throws IOException {
    log.info("messageBody={}", data);
    log.info("username={}, age={}", data.getUsername(), data.getAge());
    return "ok";
}

 

(4) HttpEntity 사용 - requestBodyJsonV4

  • HttpEntity를 사용해도 되지만 메시지 바디의 내용을 직접 꺼내야하는 코드가 필요함
@ResponseBody
@PostMapping("/request-body-json-v4")
public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity) {
    HelloData data = httpEntity.getBody();
    log.info("messageBody={}", data);
    log.info("username={}, age={}", data.getUsername(), data.getAge());
    return "ok";
}

 

(5) Json으로 반환 - requestBodyJsonV5

  • @ResponseBody로 해당 객체를 HTTP 메시지 바디에 직접 넣어줄 수 있음
  • 해당 경우에도 HttpEntity사용 가능(V4 참고)
  • JSON 요청 -> HTTP 메시지 컨버터 -> 객체 -> HTTP 메시지 컨버터 -> JSON 응답
@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
    log.info("messageBody={}", data);
    log.info("username={}, age={}", data.getUsername(), data.getAge());
    return data;
}