스프링 MVC - 기본 기ㅇ
시작
프로젝트 생성
Jar
를 사용하면 항상 내장 서버(톰캣)을 사용하고 webapp
경로도 사용하지 않습니다. 내장 서버 사용에 최적화 되어 있는 기능입니다.
War
를 사용하면 내장 서버 사용도 가능하지만 주로 외부 서버에 배포하는 목적으로 사용합니다.
dependency
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
SLF4J
는 Logback, 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")
시스템 콘솔로 직접 출력하는 것 보다 로그를 사용하면 다음과 같은 장점이 있다. 실무에서는 항상 로그를 사용해야 한다.
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 메시지 바디에 바로 입력
한다.
실행하고 요청을 보내면 아래와 같이 로그가 표시되는것을 확인할 수 있습니다.
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
에 아래와 같이 명시해주면 모든 로그가 표시됩니다.
# hello.springmvc 패키지와 그 하위 로그 레벨 설정
logging.level.hello.springmvc=trace(로그 레벨)
위와 같이 설정하면 모든 로그가 표시된다. 예로 info
를 설정했다면 INFO
, WARN
, ERROR
가 표시된다.
- LEVEL :
TRACE > DEBUG > INFO > WARN > ERROR
- 기본 레벨은
INFO
로 설정 - 개발 서버는 debug 출력
- 운영 서버는 info 출력
참고로 아래와 같이 lombok
을 이용해 @Slf4j
애너테이션을 명시해줌으로서 현재 클래스의 로그를 표시할 수 있습니다.
@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";
}
}
올바른 로그 사용법
- 아래와 같이 사용해도 사실은 문제가 없다.
// log.trace(" trace trace = {}", name)
log.trace(" trace trace = "+ name)
- 다만 위와 같은 경우 실행과정에서 문제가 발생한다.
String name = "Spring";
//...
// log.trace(" trace trace = "+ name)
// 연산이 일어나게 된다.
log.trace(" trace trace = "+ "Spring")
- 연산이 일어남으로 메모리/CPU를 사용하게 된다. 로그 레벨이
info
로 설정된 경우 trace를 사용하지도 않으면서 리소스를 사용하게 된다. 반드시 아래와 같은 방식을 사용해야한다.log.trace(" trace trace = {}", name)
: 파라미터만 넘김
로그 사용시 장점
- 쓰레드 정보, 클래스 이름 같은 부가 정보를 함께 볼 수 있고, 출력 모양을 조정할 수 있다.
- 로그 레벨에 따라 개발 서버에서는 모든 로그를 출력하고, 운영서버에서는 출력하지 않는 등 로그를 상황에 맞게 조절(
properties에 정의
)할 수 있다. - 시스템 아웃 콘솔에만 출력하는 것이 아니라, 파일이나 네트워크 등, 로그를 별도의 위치에 남길 수 있다. 특히 파일로 남길 때는 일별, 특정 용량에 따라 로그를 분할하는 것도 가능하다.
- 성능도 일반 System.out보다 좋다. (내부 버퍼링, 멀티 쓰레드 등등) 그래서 실무에서는 꼭 로그를 사용해야 한다.
요청 매핑
요청 매핑
이란 요청이 왔을때, 어떤 컨트롤러를 매핑해야하는가를 말한다.
@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/
아래와 같이 메소드에 따른 매핑을 할 수 있다.
@RequestMapping(value = "/hello-basic", method = RequestMethod.GET)
public String helloBasic(){
log.info("helloBasic");
return "ok";
}
만일 위 URL으로 POST
요청하면 HTTP 405(Method Not Allowed)
를 반환한다.
또는 아래와 같이 특정 메소드의 애너테이션으로 매핑할 수 있다.
@GetMapping(value = "/mapping-get-v2")
public String mappingGetV2() {
log.info("mapping-get-v2");
return "ok";
}
URL의 특정 경로를 변수로 사용
- PathVariable를 이용해 특정 경로를 변수로 사용할 수 있다.
/**
* 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
의 이름과 파라미터 이름이 같으면 생략할 수 있다.
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable String userId){
log.info("mappingPath userId={}", userId);
return "ok";
}
- 아래와 같이 다중 사용도 가능하다.
@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId, @PathVariable Long orderId) {
log.info("mappingPath userId={}, orderId={}", userId, orderId);
return "ok";
}
특정 파라미터 조건 매핑
- 잘 사용하진 않지만 아래와 같이 특정 파라미터 조건을 추가해 매핑할 수 있다.
/**
* 파라미터로 추가 매핑
* 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";
}
특정 파라미터 조건 매핑 사용법
특정 헤더 조건 매핑
- 헤더 값을 이용해 조건에 따른 매핑도 가능하다.
/**
*특정 헤더로 추가 매핑
* 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";
}
특정 헤더 조건 매핑 사용법
미디어 타입 조건 매핑
- 미디어 타입 조건을 이용해 조건에 따른 매핑도 가능하다.
consumes
을 사용해야 한다.
/**
* 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
을 사용해야 한다.
/**
* 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 사용법
요청 매핑 - API 예시
- 회원 관리를 HTTP API로 만든다 생각하고 매핑을 어떻게 하는지 알아보자.
회원 관리 API
- 회원 목록 조회: GET
/users
- 회원 등록: POST
/users/
- 회원 조회: GET
/users/{userId}
- 회원수정: PATCH
/users/{userId}
- 회원 삭제: DELETE
/users/{userId}
MappingClassController
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 헤더 정보를 조회하는 방법을 알아보자.
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";
}
}
- 요청 결과
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
MultiValueMap<String, String> map = new LinkedMultiValueMap();
map.add("keyA", "value1");
map.add("keyA", "value2");
//[value1,value2]
List<String> values = map.get("keyA");
@Slf4j
- 다음 코드를 자동으로 생성해서 로그를 선언해준다. 개발자는 편리하게
log
라고 사용하면 된다.
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
HttpServletRequest
의 request.getParameter()
를 사용하면 다음 두가지 요청 파라미터를 조회할 수 있다.
- GET, 쿼리 파라미터 전송
http://localhost:8080/request-param?username=hello&age=20
- POST, HTML Form 전송
POST /request-param ...
content-type: application/x-www-form-urlencoded
username=hello&age=20
- GET 쿼리 파리미터 전송 방식이든, POST HTML Form 전송 방식이든 둘다 형식이 같으므로 구분없이 조회할 수 있다. 이것을 간단히 요청 파라미터(request parameter) 조회라 한다.
서블릿 - 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");
}
Form UI
<!-- /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>
결과
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 사용
@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";
}
결과
2023-08-26T09:16:13.375+09:00 INFO 3957 --- [nio-8080-exec-2] h.d.s.b.request.RequestParamController : username=hello, age=20
- 아래와 같이 일부 생략 가능하다.
@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
정도는 있어야 요청 파라미터에서 데이터를 읽는 다는 것을 알 수 있다.
@ResponseBody // return 한 값을 바디에 담아 응답한다.
@RequestMapping("/request-param-v3")
public String requestParamV3(
// 요청 파라미터 값이 변수명과 일치한다면 ("username")등 매핑 방법을 생략할 수 있다.
String username, int age) {
log.info("username={}, age={}", username, age);
return "ok";
}
- 다음과 같이
required
속성으로 필요 여부를 설정할 수 있다.
@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=
이 경우 결과는 다음과 같다. 에러는 발생하지 않지만 빈 값으로 들어온다. 이 점에 주의해야한다.
2023-08-26T09:16:13.375+09:00 INFO 3957 --- [nio-8080-exec-2] h.d.s.b.request.RequestParamController : username=, age=20
defaultVlaue
속성을 이용해 기본값을 설정할 수 있다.
@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
@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
우선 회원 클래스를 생성한다.
package hello.demo.springmvc.basic;
import lombok.Data;
@Data // @Getter, @Setter, @ToString , @EqualsAndHashCode , @RequiredArgsConstructor 를 자동으로 적용해준다.
public class HelloData {
private String username;
private int age;
}
@ModelAttribute 적용
@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
를 생략할 수도 있다.
@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 형식으로 전달되는 경우는 요청 파라미터로 인정된다.)
서블릿
@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");
}
다음과 같이 요청
결과
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
를 이용해 바디 메시지를 그대로 읽을 수 있다.
@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
을 사용하면 이전 과정에서 불편한 점을 해소할 수 있다.
@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 상태 코드 설정 가능, 응답에서 사용
return new ResponseEntity<>("ok", HttpStatus.CREATED);
참고
스프링MVC 내부에서 HTTP 메시지 바디를 읽어서 문자나 객체로 변환해서 전달해주는데, 이때 HTTP 메시지 컨버터( HttpMessageConverter )라는 기능을 사용한다. 이것은 조금 뒤에 HTTP 메시지 컨버터에서 자세히 설명한다.
RequestBody 이용
- 가장 사용하기 좋고 많이 쓰이는 방법이다.
@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를 사용하지 않는다.