[스프링 MVC 1편] - 6

스프링 MVC - 기본 기ㅇ

시작

프로젝트 생성

Spring-Basic-capture1

Jar를 사용하면 항상 내장 서버(톰캣)을 사용하고 webapp경로도 사용하지 않습니다. 내장 서버 사용에 최적화 되어 있는 기능입니다.
War를 사용하면 내장 서버 사용도 가능하지만 주로 외부 서버에 배포하는 목적으로 사용합니다.

dependency

text
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

로깅 알아보기

실무에서는 System.out.println()을 로깅용도로 사용하지 않습니다.

스프링 부트 라이브러리를 사용하면 부트 로강 라이브러리spring-boot-starter-logging이 함께 포함됩니다.

  • SLF4J : http://www.slf4j.org
  • Logback http://logback.qos.ch

SLF4JLogback, Log4J, Log4J2등의 라이브러리를 통항해 인터페이스로 제공한다. Logback은 구현체이다. 실무에서는 기본으로 제공되는 Logback을 주로 사용한다.

로그 선언

  • private Logger log = LoggerFactory.getLogger(getClass());
  • private static final Logger log = LoggerFactory.getLogger(Xxx.class)
  • @Slf4j : 롬복 사용 가능 로그 호출
  • log.info("hello")
  • System.out.println("hello")
  • 시스템 콘솔로 직접 출력하는 것 보다 로그를 사용하면 다음과 같은 장점이 있다. 실무에서는 항상 로그를 사용해야 한다.
java
package hello.demo.springmvc.basic;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController // return 한 데이터를 그대로 응답으로 보냄.
public class LogTestController {

    // getLogger(현재 클래스);
    // Logger, getLogger 등은 slf4j 사용
    private final Logger log = LoggerFactory.getLogger(getClass());

    @RequestMapping("/log-test")
    public String logTest() {
        String name = "Spring";

        System.out.println("name = " + name);

        log.trace(" trace log = {}", name);
        // 개발서버에서 주로 사용
        log.debug(" debug log = {}", name);
        // 비즈니스/운영시스템에서 봐야하는 정보
        log.info(" info log = {}", name);
        // 경고
        log.warn(" warn log = {}", name);
        log.error(" error log = {}", name);

        return "ok";
    }
}

매핑 정보

  • @RestController
    • @Controller는 반환 값이 String이면 뷰 이름으로 인식.
    • @RestController는 반환 값이 HTTP 메시지 바디에 바로 입력한다.

실행하고 요청을 보내면 아래와 같이 로그가 표시되는것을 확인할 수 있습니다.

log
name = Spring
2023-08-26T07:34:59.506+09:00  INFO 2349 --- [nio-8080-exec-1] h.d.springmvc.basic.LogTestController    :  info log = Spring
2023-08-26T07:34:59.507+09:00  WARN 2349 --- [nio-8080-exec-1] h.d.springmvc.basic.LogTestController    :  warn log = Spring
2023-08-26T07:34:59.507+09:00 ERROR 2349 --- [nio-8080-exec-1] h.d.springmvc.basic.LogTestController    :  error log = Spring

다만 현재 위와 같이 로그 레벨은 INFO, WARN, ERROR만 확인할 수 있는데, 모든 로그 레벨을 표시하려면 src/main/resources/static/application.properties에 아래와 같이 명시해주면 모든 로그가 표시됩니다.

properties
# hello.springmvc 패키지와 그 하위 로그 레벨 설정
logging.level.hello.springmvc=trace(로그 레벨)

위와 같이 설정하면 모든 로그가 표시된다. 예로 info를 설정했다면 INFO, WARN, ERROR가 표시된다.

  • LEVEL : TRACE > DEBUG > INFO > WARN > ERROR
  • 기본 레벨은 INFO로 설정
  • 개발 서버는 debug 출력
  • 운영 서버는 info 출력

참고로 아래와 같이 lombok을 이용해 @Slf4j애너테이션을 명시해줌으로서 현재 클래스의 로그를 표시할 수 있습니다.

java
@Slf4j
@RestController // return 한 데이터를 그대로 응답으로 보냄.
public class LogTestController {

    // getLogger(현재 클래스);
    // Logger, getLogger 등은 slf4j 사용
//    private final Logger log = LoggerFactory.getLogger(getClass());

    @RequestMapping("/log-test")
    public String logTest() {
        String name = "Spring";

        System.out.println("name = " + name);

        log.trace(" trace log = {}", name);
        // 개발서버에서
        log.debug(" debug log = {}", name);
        // 비즈니스/운영시스템에서 봐야하는 정보
        log.info(" info log = {}", name);
        // 경고
        log.warn(" warn log = {}", name);
        log.error(" error log = {}", name);

        return "ok";
    }
}

올바른 로그 사용법

  • 아래와 같이 사용해도 사실은 문제가 없다.
java
// log.trace(" trace trace = {}", name)
log.trace(" trace trace = "+ name)
  • 다만 위와 같은 경우 실행과정에서 문제가 발생한다.
java
String name = "Spring";

//...
// log.trace(" trace trace = "+ name)

// 연산이 일어나게 된다.
log.trace(" trace trace = "+ "Spring") 
  • 연산이 일어남으로 메모리/CPU를 사용하게 된다. 로그 레벨이 info로 설정된 경우 trace를 사용하지도 않으면서 리소스를 사용하게 된다. 반드시 아래와 같은 방식을 사용해야한다.
    • log.trace(" trace trace = {}", name) : 파라미터만 넘김

로그 사용시 장점

  • 쓰레드 정보, 클래스 이름 같은 부가 정보를 함께 볼 수 있고, 출력 모양을 조정할 수 있다.
  • 로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하고, 운영서버에서는 출력하지 않는 등 로그를 상황에 맞게 조절(properties에 정의)할 수 있다.
  • 시스템 아웃 콘솔에만 출력하는 것이 아니라, 파일이나 네트워크 등, 로그를 별도의 위치에 남길 수 있다. 특히 파일로 남길 때는 일별, 특정 용량에 따라 로그를 분할하는 것도 가능하다.
  • 성능도 일반 System.out보다 좋다. (내부 버퍼링, 멀티 쓰레드 등등) 그래서 실무에서는 꼭 로그를 사용해야 한다.

요청 매핑

  • 요청 매핑이란 요청이 왔을때, 어떤 컨트롤러를 매핑해야하는가를 말한다.
java
@RestController
public class MappingController {

    private Logger log = LoggerFactory.getLogger(getClass());

    // /hello-basic 으로 요청한 경우 매핑되는 컨트롤러
    @RequestMapping("/hello-basic")
    // 아래와 같이 두 경로로 요청한 경우 매핑할 수 있다.
    // @RequestMapping("/hello-basic", "/hello-wow")
    public String helloBasic(){
        log.info("helloBasic");
        return "ok";
    }
}

둘다 허용 - 스프링 부트 3.0 이전 다음 두가지 요청은 다른 URL이지만, 스프링은 다음 URL 요청들을 같은 요청으로 매핑한다.

  • 매핑: /hello-basic
  • URL 요청: /hello-basic, /hello-basic/

아래와 같이 메소드에 따른 매핑을 할 수 있다.

java
@RequestMapping(value = "/hello-basic", method = RequestMethod.GET)
public String helloBasic(){
    log.info("helloBasic");
    return "ok";
}

만일 위 URL으로 POST요청하면 HTTP 405(Method Not Allowed)를 반환한다.

또는 아래와 같이 특정 메소드의 애너테이션으로 매핑할 수 있다.

java
@GetMapping(value = "/mapping-get-v2")
public String mappingGetV2() {
    log.info("mapping-get-v2");
    return "ok";
}

URL의 특정 경로를 변수로 사용

  • PathVariable를 이용해 특정 경로를 변수로 사용할 수 있다.
java
/**
 * PathVariable 사용
 * 변수면이 같으면 생략 가능
 * @PathVariable("userId") String userId -> @PathVariable userId
 * /mapping/userA
 */
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data){
    log.info("mappingPath userId={}", data);
    return "ok";
}

최근 HTTP API는 다음과 같이 리소스 경로에 식별자를 넣는 스타일을 선호한다.

  • /mapping/userA
  • /users/1
  • @RequestMapping 은 URL 경로를 템플릿화 할 수 있는데, @PathVariable 을 사용하면 매칭 되는 부분을 편리하게 조회할 수 있다.
  • @PathVariable 의 이름과 파라미터 이름이 같으면 생략할 수 있다.
java
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable String userId){
    log.info("mappingPath userId={}", userId);
    return "ok";
}
  • 아래와 같이 다중 사용도 가능하다.
java
@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId, @PathVariable Long orderId) {
    log.info("mappingPath userId={}, orderId={}", userId, orderId);
    return "ok";
}

특정 파라미터 조건 매핑

  • 잘 사용하진 않지만 아래와 같이 특정 파라미터 조건을 추가해 매핑할 수 있다.
java
/**
     * 파라미터로 추가 매핑
     * params="mode",
     * params="!mode"
     * params="mode=debug"
     * params="mode!=debug" (! = )
     * params = {"mode=debug","data=good"}
     */
    @GetMapping(value = "/mapping-param", params = "mode=debug")
    public String mappingParam() {
        log.info("mappingParam");
        return "ok";
    }
  • 특정 파라미터 조건 매핑 사용법
    Spring-Basic-capture2

특정 헤더 조건 매핑

  • 헤더 값을 이용해 조건에 따른 매핑도 가능하다.
java
/**
*특정 헤더로 추가 매핑
   * headers="mode",
   * headers="!mode"
   * headers="mode=debug"
   * headers="mode!=debug" (! = )
   */
  @GetMapping(value = "/mapping-header", headers = "mode=debug")
  public String mappingHeader() {
      log.info("mappingHeader");
      return "ok";
  }
  • 특정 헤더 조건 매핑 사용법
    Spring-Basic-capture3

미디어 타입 조건 매핑

  • 미디어 타입 조건을 이용해 조건에 따른 매핑도 가능하다. consumes을 사용해야 한다.
java
/**
* Content-Type 헤더 기반 추가 매핑 Media Type
   * consumes="application/json"
   * consumes="!application/json"
   * consumes="application/*"
   * consumes="*\/*"
   * MediaType.APPLICATION_JSON_VALUE
   */
  @PostMapping(value = "/mapping-consume", consumes = MediaType.APPLICATION_JSON_VALUE)
  public String mappingConsumes() {
      log.info("mappingConsumes");
      return "ok";
  }

미디어 타입 조건 매핑 - HTTP 요청 Accept, produce

  • Accept 헤더 기반 Media Type 조건에 따른 매핑도 가능하다. produces을 사용해야 한다.
java
  /**
  * Accept 헤더 기반 Media Type * produces = "text/html"
  * produces = "!text/html"
  * produces = "text/*"
  * produces = "*\/*"
  */
  @PostMapping(value = "/mapping-produce", produces = MediaType.TEXT_HTML_VALUE)
  public String mappingProduces() {
      log.info("mappingProduces");
      return "ok";
  }
  • 미디어 타입 조건 매핑 - HTTP 요청 Accept, produce 사용법
    Spring-Basic-capture4

요청 매핑 - API 예시

  • 회원 관리를 HTTP API로 만든다 생각하고 매핑을 어떻게 하는지 알아보자.

회원 관리 API

  • 회원 목록 조회: GET /users
  • 회원 등록: POST /users/
  • 회원 조회: GET /users/{userId}
  • 회원수정: PATCH /users/{userId}
  • 회원 삭제: DELETE /users/{userId}

MappingClassController

java
package hello.demo.springmvc.basic.requestMapping;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/mapping/users")
public class MappingClassController {

    /**
     * - 회원 목록 조회: GET `/users`
     * - 회원 등록: POST `/users/`
     * - 회원 조회: GET `/users/{userId}`
     * - 회원수정: PATCH `/users/{userId}`
     * - 회원 삭제: DELETE `/users/{userId}`
     */

    /**
     * GET /mapping/users
     */
    @GetMapping
    public String users() {
        return "get users";
    }
    /**
     * POST /mapping/users
     */
    @PostMapping
    public String addUser() {
        return "post user";
    }
    /**
     * GET /mapping/users/{userId}
     */
    @GetMapping("/{userId}")
    public String findUser(@PathVariable String userId) {
        return "get userId=" + userId;
    }
    /**
     * PATCH /mapping/users/{userId}
     */
    @PatchMapping("/{userId}")
    public String updateUser(@PathVariable String userId) {
        return "update userId=" + userId;
    }

    /**
     * DELETE /mapping/users/{userId}
     */
    @DeleteMapping("/{userId}")
    public String deleteUser(@PathVariable String userId) {
        return "delete userId=" + userId;
    }
}

@RequestMapping("/mapping/users")

  • 클래스 레벨에 매핑 정보를 두면 메서드 레벨에서 해당 정보를 조합해서 사용한다.

매핑 방법을 이해했으니, 이제부터 HTTP 요청이 보내는 데이터들을 스프링 MVC로 어떻게 조회하는지 알아보자.

HTTP 요청 - 기본, 헤더 조회

  • 애노테이션 기반의 스프링 컨트롤러는 다양한 파라미터를 지원한다. 이번에는 HTTP 헤더 정보를 조회하는 방법을 알아보자.
java
package hello.demo.springmvc.basic.request;

import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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 java.util.Locale;

@Slf4j
@RestController
public class RequestHeaderController {

    @RequestMapping("/headers")
    public String headers(HttpServletRequest request,
                          HttpServletResponse response,
                          HttpMethod httpMethod,
                          Locale locale,
                          // MultiValueMap -> 하나의 키에 여러 값을 가질 수 있다.
                          // Map의 경우 하나의 키에 하나의 값
                          @RequestHeader MultiValueMap<String, String> headerMap,
                          @RequestHeader("host") String host,
                          @CookieValue(value = "myCookie", required = false) String cookie) {
        // HttpServletRequest
        log.info("request={}", request);
        // HttpServletResponse
        log.info("response={}", response);
        // 요청 메서드
        log.info("httpMethod={}", httpMethod);
        // 우선순위가 높은 Locale locale=ko_KR...
        log.info("locale={}", locale);
        // 헤더 키, 값
        log.info("headerMap={}", headerMap);
        // 
        log.info("header host={}", host);
        log.info("myCookie={}", cookie);
        return "ok";
    }

}
  • 요청 결과
log
2023-08-26T08:49:22.106+09:00  INFO 3537 --- [nio-8080-exec-1] h.d.s.b.request.RequestHeaderController  : request=org.apache.catalina.connector.RequestFacade@722614da
2023-08-26T08:49:22.106+09:00  INFO 3537 --- [nio-8080-exec-1] h.d.s.b.request.RequestHeaderController  : response=org.apache.catalina.connector.ResponseFacade@57a79a75
2023-08-26T08:49:22.106+09:00  INFO 3537 --- [nio-8080-exec-1] h.d.s.b.request.RequestHeaderController  : httpMethod=GET
2023-08-26T08:49:22.106+09:00  INFO 3537 --- [nio-8080-exec-1] h.d.s.b.request.RequestHeaderController  : locale=ko_KR
2023-08-26T08:49:22.106+09:00  INFO 3537 --- [nio-8080-exec-1] h.d.s.b.request.RequestHeaderController  : headerMap={user-agent=[PostmanRuntime/7.32.3], accept=[*/*], postman-token=[b8d7d00e-f9c9-4525-92fe-5878ec40cc4f], host=[localhost:8080], accept-encoding=[gzip, deflate, br], connection=[keep-alive]}
2023-08-26T08:49:22.106+09:00  INFO 3537 --- [nio-8080-exec-1] h.d.s.b.request.RequestHeaderController  : header host=localhost:8080
2023-08-26T08:49:22.106+09:00  INFO 3537 --- [nio-8080-exec-1] h.d.s.b.request.RequestHeaderController  : myCookie=null

@RequestHeader MultiValueMap<String, String> headerMap

  • 모든 HTTP 헤더를 MultiValueMap 형식으로 조회한다. @RequestHeader("host") String host
  • 특정 HTTP 헤더를 조회한다.
  • 속성
    • 필수 값 여부: required
    • 기본 값 속성: defaultValue @CookieValue(value = "myCookie", required = false) String cookie
  • 특정 쿠키를 조회한다.
  • 속성
    • 필수 값 여부: required
    • 기본 값: defaultValue

MultiValueMap

  • MAP과 유사한데, 하나의 키에 여러 값을 받을 수 있다.
  • HTTP header, HTTP 쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용한다.
    • keyA=value1&keyA=value2
java
 MultiValueMap<String, String> map = new LinkedMultiValueMap();
  map.add("keyA", "value1");
  map.add("keyA", "value2");
  //[value1,value2]
  List<String> values = map.get("keyA");

@Slf4j

  • 다음 코드를 자동으로 생성해서 로그를 선언해준다. 개발자는 편리하게 log 라고 사용하면 된다.
java
private static final org.slf4j.Logger log =
  org.slf4j.LoggerFactory.getLogger(RequestHeaderController.class);

참고 사항 @Conroller 의 사용 가능한 요청 파라미터 목록은 다음 공식 메뉴얼에서 확인할 수 있다.

@Conroller 의 사용 가능한 응답 파라미터 목록은 다음 공식 메뉴얼에서 확인할 수 있다.

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

클라이언트에서 서버로 요청 데이터를 전달할 때는 주로 다음 3가지 방법을 사용한다.

  • GET - 쿼리 파라미터
    • /url?username=hello&age=20
    • 메시지 바디 없이, URL의 쿼리 파라미터에 데이터를 포함해서 전달 예) 검색, 필터, 페이징등에서 많이 사용하는 방식
  • POST - HTML Form
    • content-type: application/x-www-form-urlencoded
    • 메시지 바디에 쿼리 파리미터 형식으로 전달 username=hello&age=20 예) 회원 가입, 상품 주문, HTML Form 사용
  • HTTP message body에 데이터를 직접 담아서 요청
    • HTTP API에서 주로 사용, JSON, XML, TEXT
    • 데이터 형식은 주로 JSON 사용
    • POST, PUT, PATCH

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

HttpServletRequestrequest.getParameter() 를 사용하면 다음 두가지 요청 파라미터를 조회할 수 있다.

  • GET, 쿼리 파라미터 전송
    • http://localhost:8080/request-param?username=hello&age=20
  • POST, HTML Form 전송
html
POST /request-param ...
content-type: application/x-www-form-urlencoded
username=hello&age=20
  • GET 쿼리 파리미터 전송 방식이든, POST HTML Form 전송 방식이든 둘다 형식이 같으므로 구분없이 조회할 수 있다. 이것을 간단히 요청 파라미터(request parameter) 조회라 한다.
서블릿 - HttpServletRequest

다음 코드를 작성한다.

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

Form UI

html
<!-- /static/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>

Spring-Basic-capture5

결과

log
2023-08-26T09:10:42.123+09:00  INFO 3842 --- [nio-8080-exec-6] h.d.s.b.request.RequestParamController   : username=hello, age=20
2023-08-26T09:12:27.563+09:00  INFO 3842 --- [io-8080-exec-10] h.d.s.b.request.RequestParamController   : username=kim, age=24
@RequestParam 사용
java
@ResponseBody // return 한 값을 바디에 담아 응답한다.
@RequestMapping("/request-param-v2")
public String requestParamV2(
        @RequestParam("username") String memberName,
        @RequestParam("age") int memberAge) {
    log.info("username={}, age={}", memberName, memberAge);
    return "ok";
}

결과

log
2023-08-26T09:16:13.375+09:00  INFO 3957 --- [nio-8080-exec-2] h.d.s.b.request.RequestParamController   : username=hello, age=20
  • 아래와 같이 일부 생략 가능하다.
java
@ResponseBody // return 한 값을 바디에 담아 응답한다.
@RequestMapping("/request-param-v3")
public String requestParamV3(
        // 요청 파라미터 값이 변수명과 일치한다면 ("username")등 매핑 방법을 생략할 수 있다.
        @RequestParam String username,
        @RequestParam int age) {
    log.info("username={}, age={}", username, age);
    return "ok";
}
  • String , int , Integer 등의 단순 타입이면 @RequestParam 도 생략 가능하다. 하지만 @RequestParam정도는 있어야 요청 파라미터에서 데이터를 읽는 다는 것을 알 수 있다.
java
@ResponseBody // return 한 값을 바디에 담아 응답한다.
@RequestMapping("/request-param-v3")
public String requestParamV3(
        // 요청 파라미터 값이 변수명과 일치한다면 ("username")등 매핑 방법을 생략할 수 있다.
        String username, int age) {
    log.info("username={}, age={}", username, age);
    return "ok";
}
  • 다음과 같이 required속성으로 필요 여부를 설정할 수 있다.
java
@ResponseBody // return 한 값을 바디에 담아 응답한다.
@RequestMapping("/request-param-v5")
public String requestParamRequired(
        // 기본값은 required = true : 값이 넘어오지 않으면 400 응답
        @RequestParam(required = true) String username,
        // age의 경우 값이 넘어오지 않아도 됨. 다만 없는 경우. 서버 코드에서 조건 분기 처리 필요.
        // 타입이 int인 경우 NULL이 들어갈 수 없으므로 500 에러. Integer는 객체 타입으로 NULL이 들어갈 수 있다.
        @RequestParam(required = false)  Integer age
        // @RequestParam(required = false)  int age
    ) {
    log.info("username={}, age={}", username, age);
    return "ok";
}

그런데 만약 클라이언트가 다음과 같이 요청하면 어떻게 될까. http://서버 도메인/request-param-v5?username=

이 경우 결과는 다음과 같다. 에러는 발생하지 않지만 빈 값으로 들어온다. 이 점에 주의해야한다.

log
2023-08-26T09:16:13.375+09:00  INFO 3957 --- [nio-8080-exec-2] h.d.s.b.request.RequestParamController   : username=, age=20
  • defaultVlaue속성을 이용해 기본값을 설정할 수 있다.
java
@ResponseBody
@RequestMapping("/request-param-default")
public String requestParamDefault(
        @RequestParam(required = true, defaultValue = "guest") String username,
        // 값이 넘어오지 않으면 -1 을 가진 age 사용
        @RequestParam(required = false, defaultValue = "-1") int age) {
    log.info("username={}, age={}", username, age);
    return "ok";
}

사실 위 코드에서 기본값이 설정되어 있으므로 required는 명시하지 않아도 된다.

  • 파라미터를 Map으로 조회하기 - requestParamMap
java
@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";
}

파라미터를 Map, MultiValueMap으로 조회할 수 있다.

  • MultiValueMap
    • user=userA&user=userB
      • MultiValueMap(user=['userA', 'userB'])

HTTP 요청 파라미터 - @ModelAttribute

우선 회원 클래스를 생성한다.

java
package hello.demo.springmvc.basic;

import lombok.Data;

@Data // @Getter, @Setter, @ToString , @EqualsAndHashCode , @RequiredArgsConstructor 를 자동으로 적용해준다.
public class HelloData {
    private String username;
    private int age;
}

@ModelAttribute 적용

java
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) { // setUsername ... 호출
    log.info("username={}, age={}", helloData.getUsername(), helloData.getAge()); // getUsername ... 호출
    return "ok";
}

@ModelAttribute 클래스(HelloData) 를 사용하면 HelloData 객체가 생성되고, 요청 파라미터의 값도 모두 해당 클래스가 값에 들어가 있는 객체가 된다.

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

  • 다음과 같이 @ModelAttribute를 생략할 수도 있다.
java
@ResponseBody
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2( HelloData helloData) {
    log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
    return "ok";
}
  • 생략과정에서 다음 내용을 주의해야한다.
    • @ModelAttribute 는 생략할 수 있다. 그런데 @RequestParam 도 생략할 수 있으니 혼란이 발생할 수 있다.
    • 스프링은 해당 생략시 다음과 같은 규칙을 적용한다.
      • String , int , Integer 같은 단순 타입 = @RequestParam
      • 나머지 = @ModelAttribute @ModelAttribute 뒤에 붙는 클래스 타입. (argument resolver 로 지정해둔 타입(HttpServletResponse) 외)

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

프론트엔드와 팀워크시 클라이언트측에서는 데이터를 주로 다음과 같이 보낸다.

  • HTTP message body에 데이터를 직접 담아서 요청

    • HTTP API에서 주로 사용, JSON, XML, TEXT
    • 데이터 형식은 주로 JSON 사용
    • POST, PUT, PATCH
  • 요청 파라미터와 다르게, HTTP 메시지 바디를 통해 데이터가 직접 데이터가 넘어오는 경우 @RequestParam, @ModelAttribute를 사용할 수 없다.(물론 HTML Form 형식으로 전달되는 경우는 요청 파라미터로 인정된다.)

서블릿

java
@PostMapping("/request-body-string-v1")
public void requestBodyString(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");
}

다음과 같이 요청

Spring-Basic-capture6

결과

log
2023-08-26T09:56:43.733+09:00  INFO 4957 --- [nio-8080-exec-4] h.d.s.b.r.RequestBodyStringController    : messageBody={
    "data": "value"
}

inputStream 그대로 사용하기

서블릿을 사용하지 않고도 InputStream를 이용해 바디 메시지를 그대로 읽을 수 있다.

java
@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 이용하기

스프링에서 지원하는 HttpEntity을 사용하면 이전 과정에서 불편한 점을 해소할 수 있다.

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

스프링 MVC는 다음 파라미터를 지원한다.

  • HttpEntity: HTTP header, body 정보를 편리하게 조회
    • 메시지 바디 정보를 직접 조회
    • 요청 파라미터를 조회하는 기능과 관계 없음 @RequestParam X, @ModelAttribute X
  • HttpEntity는 응답에도 사용 가능
    • 메시지 바디 정보 직접 반환
    • 헤더 정보 포함 가능
    • view 조회X

HttpEntity 를 상속받은 다음 객체들도 같은 기능을 제공한다.

  • RequestEntity
    • HttpMethod, url 정보가 추가, 요청에서 사용
  • ResponseEntity
    • HTTP 상태 코드 설정 가능, 응답에서 사용
java
return new ResponseEntity<>("ok", HttpStatus.CREATED);

참고
스프링MVC 내부에서 HTTP 메시지 바디를 읽어서 문자나 객체로 변환해서 전달해주는데, 이때 HTTP 메시지 컨버터( HttpMessageConverter )라는 기능을 사용한다. 이것은 조금 뒤에 HTTP 메시지 컨버터에서 자세히 설명한다.

RequestBody 이용

  • 가장 사용하기 좋고 많이 쓰이는 방법이다.
java
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) {
    log.info("messageBody={}", messageBody);

    return "ok";
}

@RequestBody

  • @RequestBody 를 사용하면 HTTP 메시지 바디 정보를 편리하게 조회할 수 있다. 참고로 헤더 정보가 필요하다면 HttpEntity 를 사용하거나 @RequestHeader 를 사용하면 된다. 이렇게 메시지 바디를 직접 조회하는 기능은 요청 파라미터를 조회하는 @RequestParam , @ModelAttribute 와는 전혀 관계가 없다.
정리

요청 파라미터 vs HTTP 메시지 바디

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

@ResponseBody @ResponseBody 를 사용하면 응답 결과를 HTTP 메시지 바디에 직접 담아서 전달할 수 있다. 물론 이 경우에도 view를 사용하지 않는다.