티스토리 뷰
개발환경
프로젝트 생성 : https://start.spring.io/
GitHub Repository : https://github.com/jinyoungchoi95/learn_servlet
- Gradle 6.8.3
- Java 11
- Spring Boot 2.4.5
- Packaging : Jar
- Dependency : Spring web, Lombok, Thymeleaf
Jar를 통해 내장 톰캣을 사용하며 이전에 썼던 webapp
경로도 사용하지 않게 된다. JSP를 이제 사용하지 않을 것이기 때문에 Jar 방식으로 진행을 이어간다.
스프링 부트 Jar 에서의 Welcome Page
스프링 부트에서 Jar를 사용한다면 /resource/static/index.html
파일은 항상 Welcome 페이지로 처리한다. "/"
요청 즉, https:://localhost:8080/
에 아무런 매핑정보가 없더라도 해당 파일을 디스플레이해주는 것이다.
"/"
요청에 대해서 아무런 처리가 없음에도 /resource/static/index.html
위치의 파일이 그대로 클라이언트에 표시되었음을 확인할 수 있다.
요청 매핑
@RequestController, @RequestMapping
package hello.springmvc.basic.requestmapping;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
@RestController
public class MappingController {
private Logger log = LoggerFactory.getLogger(getClass());
@RequestMapping({"/hello-basic", "/hello-go"})
public String helloBasic() {
log.info("helloBasic");
return "ok";
}
@RestController
: 전까지 사용했던@Controller
는 String을 반환하면 논리 뷰네임으로 보고 물리 뷰 경로를 만들어서 뷰를 랜더링해줬었다. 하지만@RestController
는 뷰를 찾는 것이 아니라 HTTP 바디에 바로 입력한다. 따라서 "ok"를 리턴했다면 다음과 같이 "ok"가 HTTP 바디에 들어간다.
@RequestMapping
: 매핑할 url 정보를 담고있다.{"/hello-basic", "/hello-go"}
으로 여러개의 url 정보를 담을 수 있다.
HTTP 메소드 지정
@RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
public String mappingGetV1() {
log.info("mappingGetV1");
return "ok";
}
@GetMapping("/mapping-get-v2")
public String mappingGetV2() {
log.info("mappingGetV2");
return "ok";
}
@RequestMapping
은 HTTP 요청 메소드에 따라 상관없이 다 받을 수 있으나 method 옵션으로 원하는 메소드만 받을 수 있다. 옵션 지정 시 다른 HTTP 요청 메소드가 들어오면 405 메소드 에러가 발생한다.
메소드 옵션을 지정하지 않고 @GetMapping
으로 각각의 메소드 명 + Mapping으로 어노테이션을 지원하기도 한다. 크게 차이있는 것은 아니고 @GetMapping
코드를 보면 알 수 있듯이 @RequestMapping
에 Get 메소드 옵션을 달아놓고 편하게 쓰는 것 뿐이다.
@PathVariable 경로 변수
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data) {
log.info("mappingPath userId={}", data);
return "ok";
}
@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPathDouble(@PathVariable String userId, @PathVariable Long orderId){
log.info("mappingPath userId={}, orderId={}", userId, orderId);
return "ok";
}
url 경로 내에서 변수를 찾아서 사용하는 방식이다. REST API 방식의 URL을 설계할 때 많이 사용하는 방식이다.
@PathVariable
이 URL 경로에서 매칭되는 부분을 편하게 조회할 수 있으며 @PathVariable
의 이름과 파라메터의 이름이 같으면 생략할 수 있다.
특정 파라메터 조건 매핑
@GetMapping(value = "/mapping-param", params = "mode=debug")
public String mappingParam() {
log.info("mappingParam");
return "ok";
}
// https://localhost8080:/mapping-param?mode=debug
파라메터에 특정 값이 포함되어야 동작할 수 있다. 여기서는 mode=debug
가 포함되어야 한다.
특정 헤더 조건 매핑
@GetMapping(value = "/mapping-header", headers = "mode=debug")
public String mappingHeader() {
log.info("mappingHeader");
return "ok";
}
이번엔 헤더에 포함되어야 동작할 수 있다.
Content-type 조건 매핑
@PostMapping(value = "/mapping-consume", consumes = "application/json")
public String mappingConsumes() {
log.info("mappingConsumes");
return "ok";
}
Content-type 또한 헤더에 있는 값이기 때문에 위와 동일하게 쓴다는 생각을 할 수도 있지만 스프링 내부에서 consumes을 가지고 처리하는 부분이 있기 때문에 headers가 아닌 consumes를 사용해야 한다.
produce 조건 매핑
@PostMapping(value = "/mapping-produce", produces = "text/html")
public String mappingProduces() {
log.info("mappingProduces");
return "ok";
}
방금 전의 content-type 조건 매핑은 서버가 http 요청을 받을 때 해당 content-type이어야만 받을 수 있었다.
지금은 클라이언트가 서버로부터 결과를 받을 때 해당 content-type이어야만 받을 수 있는 조건이다. 이는 accept를 보면 알 수 있으며 기본적으로는 */*
로 대부분의 content-type을 지원한다.
요청 매핑 - API 예시
회원 관리 기능을 API로 만든다고 생각하면 어떻게 만들까?
- 회원 목록 : GET
/users
- 회원 등록 : POST
/users
- 회원 조회 : GET
/users/{userId}
- 회원 수정 : POST
/users/{userId}
- 회원 삭제 : DELETE
/users/{userId}
package hello.springmvc.basic.requestmapping;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/mapping/users")
public class MappingClassController {
@GetMapping
public String user() {
return "get users";
}
@PostMapping
public String addUser() {
return "post user";
}
@GetMapping("/{userId}")
public String findUser(@PathVariable String userId) {
return "get userId=" + userId;
}
@PatchMapping("/{userId}")
public String updateUser(@PathVariable String userId) {
return "update userId=" + userId;
}
@DeleteMapping("/{userId}")
public String deleteUser(@PathVariable String userId) {
return "delete userId=" + userId;
}
}
실제 기능 구현은 하지않고 매핑이 되는지 안되는지를 위해 단순 구현만 해두었다.
HTTP 요청 - 기본, 헤더 조회
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";
}
}
HttpMethod
: Http 메소드 정보를 조회한다. Get 메소드로 전송했다면GET
이 출력된다.Locale
: Locale 정보를 조회한다. 추가 조건 없이 요청한다면 가장 첫 Locale은ko_KR
가 출력될 것이다.@RequestHeader MultiValueMap<String, String> headerMap
: 모든 HTTP 헤더 정보를 출력한다.@RequestHeader("host") String host
: HTTP 헤더 정보에서 특정 값을 출력할 수 있다.@CookieValue(value = "myCookie", required = false) String cookie)
: 특정 쿠키를 조회한다.
HTTP 요청 파라미터 - @RequestParam
클라이언트에서 요청 데이터를 보낼 때 이를 조회하는 방법은 총 3가지였다.
- GET 쿼리스트링
- POST HTML Form
- HTTP message body에 담아서 요청
이렇게 들어오면 어떻게 처리하는지 파라미터 처리부터 알아보자.
@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("usernmae={}, age={}", username, age);
response.getWriter().write("ok");
}
초기에 우리는 HttpServletRequest
에서 getParameter()
로 직접 꺼냈다.
@ResponseBody
@RequestMapping("/request-param-v2")
public String requestParamV2(
@RequestParam("username") String memberName,
@RequestParam("age") int memberAge) {
log.info("usernmae={}, age={}", memberName, memberAge);
return "ok";
}
굳이 HttpServletRequest
를 받아와서 getParameter()
만 쓰기에는 뭔가 어색해보인다.
Spring에서는 파라메터 이름으로 바로 바인딩해서 받아올 수 있는 @RequestParam
어노테이션을 지원한다.
@RequestParam("username") String memberName
String memberName = request.getParameter("username")
둘 다 똑같은 동작을 한다. 심지어 int 형으로 맞추어두면 자동으로 캐스팅해준다.
@ResponseBody
앞전에 @Controller
어노테이션을 사용해서 컨트롤러를 만들면 반환값은 View를 반환한다고 했었다. @Controller
만을 사용하면 반환한 값을 가지고 View Resolver로 전달해서 그에 맞는 View를 찾아서 랜더링하는 것이다.
근데 @ResponseBody
가 붙은 요청은 반환값을 그대로 Http message body 에 넣어서 반환한다.
@ResponseBody
@RequestMapping("/request-param-v3")
public String requestParamV3(
@RequestParam String username,
@RequestParam int age) {
log.info("usernmae={}, age={}", username, age);
return "ok";
}
만약 파라메터의 변수명과 동일하게 받으면 굳이 ("username")
과 같이 입력하지 않아도 알아서 받아준다.
@ResponseBody
@RequestMapping("/request-param-v4")
public String requestParamV4(String username, int age) {
log.info("usernmae={}, age={}", username, age);
return "ok";
}
더 심하게는 @RequestParam
까지 생략해도 동작하지만 너무 생략되어도 좋은 방법은 아니며, 개발 설계시 정해놓고 사용하는 것이 좋다.
단, 파라메터가 무조건 들어와야하는 (required = true) 조건이 default이지만 @RequestParam
을 생략했을 경우 required=false로 설정된다. 이는 아래에서 설명한다.
username과 age를 파라메터로 받는다고 가정했었는데 만약 인자 하나가 안들어오면 어떻게 될까?
http://localhost:8080/request-param?username=hello
일단 가장 처음했던 HttpServletRequest
를 사용하는 컨트롤러는 원하는 대로 동작하진 않는다. HttpServletRequest
에서 해당 파라메터를 getParameter()
를 해야하는데 비어있어서 에러가 나기 때문이다. 그럼 파라메터 값이 요구되는 값이 들어와야하는데 안전하게 옵션으로 변경할 방법이 없을까?
@ResponseBody
@RequestMapping("/request-param-required")
public String requestParamRequired(
@RequestParam(required = true) String username,
@RequestParam(required = false) Integer age) {
log.info("usernmae={}, age={}", username, age);
return "ok";
}
@RequestParam
에서 required = true
옵션을 걸어 놓으면 해당 파라메터가 반드시 들어와야하며 들어오지 않았을 경우 400에러가 발생한다. (required = true
는 기본 default로 @RequestParam
을 사용했을 때 required 옵션을 생략하면 true로 동작한다.)
하지만 required = false
옵션을 걸어 놓으면 들어오지 않아도 알아서 null로 처리해준다. 단, 이때 default는 "항상" null
이 들어온다는 사실을 주의해야한다.
우리는 @RequestParam
에 기본 타입을 설정해도 되었다. int와 같이 말이다. 근데 required = false
를 걸어놓고 int에 null을 받는다면? 당연히 타입이 맞지않아서 에러가 발생한다.
따라서 required = false
를 사용하였을 때는 null이 들어온다는 사실을 인지하고 Interger로 변경하거나 아래에서 설명할 값이 비었을때 default 설정을 조정해주어야 한다.
@ResponseBody
@RequestMapping("/request-param-default")
public String requestParamDefault(
@RequestParam(defaultValue = "guest") String username,
@RequestParam(defaultValue = "-1") int age) {
log.info("usernmae={}, age={}", username, age);
return "ok";
}
만약 @RequestParam
이 들어왔는데 값이 null이 들어온다면 default로 지정할 값을 지정해주는 옵션이다.
기존에 아래와 같이 호출한다면 어떤 값이 들어올까?
http://localhost:8080/request-param-required?username=&age=
usernmae=, age=null
둘 다 null이 들어온다. 즉, 일단 defaultValue의 default는 null이다. 이걸 원하는 값인 "guest"나 -1처럼 바꾸어주면
usernmae=guest, age=-1
원하는 값이 들어온다.
이때 요즘은 Integer를 int로 사용해주었다는 점이다.
앞전에 required = false
옵션을 int age
에 사용해준다면 age 파라메터를 쓰지않아도 값이 들어오지만, 기본 값이 null로 들어오기 때문에 int와 타입이 맞지않아 에러가 났고 Integer로 사용해야 가능했었다.
하지만 지금은 defaultValue = "-1"
로 지정해주었기 때문에 null이 들어오지 않아 int를 사용해도 에러가 나지 않는다.
또한 defaultValue를 사용하면 이미 기본값이 적용된 상태이기 때문에 required
옵션이 의미가 없어지고 항상 defaultValue로 적용된 값이 default이다.
@ResponseBody
@RequestMapping("/request-param-map")
public String requestParamMap(@RequestParam Map<String, Object> paramMap) {
log.info("usernmae={}, age={}", paramMap.get("username"), paramMap.get("age"));
return "ok";
}
요청 파라메터이야기를 할때 같은 이름의 파라메터가 여러개 요청왔을 때도 이야기한 적이 있다.
http://localhost:8080/request?username=hello&username=hello2&age=20
얘는 그냥 getParameter()
를 사용하면 가장 처음 선언된 파라메터만 받아오고 여러 개의 파라메터를 배열로 받아오는 방법으로 String[] usernames = request.getParameterValues("username");
의 형태로 다 받아올 수 있었다.
@RequestParam
에서는 모든 파라메터를 Map 하나에 한번에 받을 수 있는 방법 또한 제공한다.
그냥 매개변수를 Map<String, Object> paramMap
으로 받아오면 Map 내부에 파라메터 네임, 파라메터 값을 받아온다. 단 위와 같이 username이 중복되었을 경우에는 String 값이 겹치기 때문에 중복된다. 따라서 MultiValueMap
을 사용한다.
HTTP 요청 파라미터 - @ModelAttribute
실제로 받은 파라메터를 사용할 때 그대로 사용하진 않고 객체를 만들어서 그 객체 안에 넣고 사용을 할 것이다. 그러면 지금과 같이 짰을 때 어떻게 될까?
HelloData data = new HelloData();
data.setUsername(username);
data.setAge(age);
길다... 이걸 스프링에서는 @ModelAttribute
어노테이션을 통해 자동으로 받아준다.
일단 데이터를 담을 객체를 만든다.
package hello.springmvc.basic;
import lombok.Data;
@Data
public class HelloData {
private String username;
private int age;
}
@Data
는 @Getter
, @Setter
, @ToString
, @EqualsAndHashCode
, @RequiredArgsContructor
를 합쳐서 제공해준다.
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
log.info("hellodata={}", helloData.toString());
return "ok";
}
HelloData 객체 안에 username과 age 모두 잘 들어가있음을 확인할 수 있다.
@ModelAttribute
ModelAttribute
는 호출되면 다음과 같이 동작한다.
HelloData
객체를 생성한다.- 요청 파라메터의 이름으로
HelloData
객체의 프로퍼티를 찾는다. 그리고 객체프로퍼티의 setter를 통해 값을 바인딩한다.
파라미터 이름이 username
이면 setUsername()
을 찾아서 호출하면서 값을 입력한다.
@ResponseBody
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2(HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
log.info("hellodata={}", helloData.toString());
return "ok";
}
물론 @ModelAttribute
또한 생략이 가능하다. 근데 @RequestParam
도 생략이 가능한데 스프링에서는 생략시 다음과 같은 규칙을 적용한다.
- String, int, Integer 단순 타입 >
@RqeustParam
- 나머지 argument resolver 지정한 것 외 >
@ModelAttribute
HTTP 요청 메시지 - 단순 텍스트
HTTP message body에 데이터를 담아서 요청하는 방법도 서블릿에서 다루었었다.
HTTP message body를 통해서 데이터가 직접 넘어오는 경우 @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");
}
먼저 HttpServletRequest
를 이용한 방법이다. Servlet에서 했던 방식과 똑같은 방식이다.
@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");
}
스프링에서는 다양한 상황에 대한 매개변수들을 모두 미리 저장해두었기 때문에 InputStream
으로 들어와도 사용이 가능하며 심지어는 Writer 역시 지원한다.
- InpustStream(Reader) : HTTP 요청 메시지 바디 내용을 조회
- OutputStream(Writer) : 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");
}
이 둘을 하나로 사용할 수 있는 매개변수인 HttpEntity<String> httpEntity
를 스프링에서는 지원한다. HTTP body 뿐만 아니라 header의 정보도 조회하며 응답 또한 반환 가능하다.
단, HTTP 요청 메시지를 읽을 경우 요청파라미터에서 사용했던 @RequestParam
과 @ModelAttribute
는 사용할 수 없다. response를 내보낼 때도 HttpEntity<>
를 사용해서 message body에 정보를 직접 반환할 수 있다.
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) {
log.info("messageBody={}", messageBody);
return "ok";
}
앞전에 사용했던 @RequestBody
를 사용하면 HTTP 메시지 정보를 쉽게 조회할 수 있다. 단 헤더 정보는 @RequestBody
에 담기는 것이 아닌 @RequestHeader
를 사용하면 된다.
HTTP 요청 메시지에 대해서 간략하게 정리하면 요청 파라메터를 사용하는 것과는 별개의 작업이라는 것이다.
- 요청 파라메터 :
@RequestParam
,@ModelAttribute
- HTTP 요청 메시지 :
@RequestBody
또한 HTTP 요청 메시지 바디에 직접 담아서 결과를 내보낼 수가 있는데, 이때는 view를 사용하지 않는다. 이는 아래 response에서 다시 다루기로 한다.
HTTP 요청 메시지 - JSON
요즘은 HTTP 요청 메시지에 String으로 그대로 넣는것이 아닌 JSON 데이터 형태로 발송한다.
@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("messgaeBody={}", messageBody);
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
}
이전 포스팅에서 했던 방식과 동일한 방식이다.
messagebody를 직접 받아서 ObjectMapper를 사용해서 객체로 변환할 수 있다.
@ResponseBody
@PostMapping("/request-body-json-v2")
public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
log.info("messgaeBody={}", messageBody);
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={], age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
@RequestBody
를 사용해서 message body를 받은 뒤 동일한 방식으로 ObjectMapper를 사용해서 객체로 변환한다. 입력은 @RequestBody
를 사용했지만 객체화하는 것도 간편화할 수 없을까?
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
물론 @RequestBody
를 그대로 사용하면서 객체로 바로받으면 ObjectMapper를 생략하고 객체로 데이터를 받을 수 있다.
어떻게 이렇게 객체를 받아도 알아서 매핑이 될까? @Requestbody
를 들어가보면 다음과 같은 설명이 있다.
즉 @RequestBody
를 사용하면 HTTP message 컨버터가 HTTP message body의 내용을 입력한 객체의 형태로 반환해 주는 것이다. 물론 문자 뿐만 아니라 JSON으로 들어와도 객체로 변환해준다. 단, application/json
형태의 content-type으로 들어와야 JSON으로 처리할 수 있다.
생략 불가능
앞서 @RequestParam
, @ModelAttribute
는 어노테이션을 생략해도 알아서 해당 어노테이션으로 처리를 해줬다.
@RequestParam
: String, Integer, int 등의 단순 타입@ModelAttribute
: argument resolver로 지정해준 타입 외 나머지
따라서 어노테이션을 생략하고 객체를 바로 받을 경우 @RequestBody
가 적용되는 것이 아닌 @ModelAttribute
가 걱용되어버리기 때문에 잘 판단하여서 어노테이션을 사용해야 한다.
@ResponseBody
@PostMapping("/request-body-json-v4")
public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity) {
HelloData helloData = httpEntity.getBody();
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
물론 HttpEntity를 사용해도 가능하다.
@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData helloData) {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return helloData;
}
응답할 때에도 메소드 단에 @ResponseBody
를 걸어줌으로써 해당 객체를 HTTP message body에 직접 넣어줄 수 있다. 이 과정에서도 message 컨버터가 동작을 한다.
@RequestBody
: JSON 요청 > HTTP message 컨버터가 객체로 변환 > 객체로 받음@ResponseBody
: 객체 전달 > HTTP message 컨버터가 JSON으로 변환 > JSON 전달
HTTP 응답 - 정적 리소스, 뷰 템플릿
스프링에서 응답 데이터를 만들어 보내는 방법은 총 3가지다
- 정적리소스
- 뷰 템플릿
- HTTP message(JSON)
정적리소스는 말그대로 정적인 페이지를 말하며 뷰 템플릿은 JSP같은 동적인 페이지이다.
정적리소스
스프링 부트에서는 클래스 패스의 다음 디렉토리에 있는 정적 리소스를 자동으로 제공한다.
/static
, /public
, /resources
, /META-INF/resources
/resources
의 경우 리소스를 보관하는 곳이며 또한 클래스패스의 시작 경로이다. 따라서 해당 디렉토리에 넣어두면 부트가 정적 리소스로 서비스를 제공한다.
예를들어 src/main/resources/static/basic/hello.html
에 파일이 들어있으면 http://localhost:8080/basic/hello.html
로 실행할 수 있다. 즉, 파일의 변경없이 정적인 파일을 그대로 서비스하는 방법이다.
뷰 템플릿
뷰 템플릿을 거쳐서 HTML이 생성되고 뷰가 응답을 만들어서 전달하는 방식이다. 주로 방금전 정적이던 HTML을 동적으로 생성하는 용도로 사용한다.
부트는 뷰 템플릿 또한 경로로 제공한다.
src/main/resources/templates
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p th:text="${data}">empty</p>
</body>
</html>
package hello.springmvc.basic.response;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class ResponseViewController {
@RequestMapping("/response-view-v1")
public ModelAndView responseViewV1() {
ModelAndView mav = new ModelAndView("response/hello")
.addObject("data", "hello!");
return mav;
}
@RequestMapping("/response-view-v2")
public String responseViewV2(Model model) {
model.addAttribute("data", "hello");
return "response/hello";
}
@RequestMapping("/response/hello")
public void responseViewV3(Model model) {
model.addAttribute("data", "hello");
}
}
ModelAndView 객체 반환
ModelAndView 객체 내에 View 경로와 Model을 담아서 그대로 전송하는 방법이다.
String 반환
@ResponseBody
가 없으면 해당 경로로("response/hello"
) 들어가서 뷰 리졸버를 실행하여 뷰를 찾고 렌더링한다.
단, @ResponseBody
가 있으면 HTTP message body에 "response/hello"
를 String으로써 전달한다.
void 반환
@Controller
를 사용하고, Http ServletResponse
, OutputStream(Writer)
같은 HTTP message body를 처리하는 파라메터가 없으면 요청된 url을 참고해서 논리 뷰 이름으로 사용한다.
즉 지금은 ("/response/hello")를 요청 url로 받았기 때문에 이를 뷰 리졸버가 받아 물리 뷰로 전달한다.
HTTP 응답 - HTTP API, 메시지 바디에 직접 입력
HTTP API의 형태로 제공하는 경우 HTML이 아니라 데이터의 형태로 제공하며 요즘의 대부분의 API는 JSON 데이터 형태로 많이 보낸다.
@GetMapping("/response-body-string-v1")
public void responsBodyV1(HttpServletResponse response) throws IOException {
response.getWriter().write("ok");
}
가장 기본적인 서블릿을 사용할 때는 HttpServletResponse 객체를 통해서 HTTP message body에 직접 메시지를 전달한다.
@GetMapping("/response-body-string-v2")
public ResponseEntity<String> responsBodyV2() {
return new ResponseEntity<>("ok", HttpStatus.OK);
}
ResponseEntity<>
는 앞전에 사용했던 HttpEntity
를 상속받았는데 HttpEntity
는 HTTP message의 헤더와 바디 정보를 담고, 이를 반환할 수 있었는데 ResponseEntity
는 HTTP 응답코드까지 전송할 수 있다.
@ResponseBody
@GetMapping("/response-body-string-v3")
public String responsBodyV3() {
return "ok";
}
@ResponseBody
를 사용하면 뷰로 사용되지 않고 직접 HTTP message에 담아 전송할 수 있다.
@GetMapping("/response/body-json-v1")
public ResponseEntity<HelloData> responseBodyJsonV1() {
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return new ResponseEntity<>(helloData, HttpStatus.OK);
}
ResponseEntity
에 객체의 형태로 반환을 한다면 JSON 형태로 변환되어 반환한다.
@ResponseStatus(HttpStatus.OK)
@ResponseBody
@GetMapping("/response/body-json-v2")
public HelloData responseBodyJsonV2() {
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return helloData;
}
@ResponseBody
를 사용할 때 객체를 반환하면 해당 객체로 HTTP message 컨버터가 변환을 해서 반환한다.
단, HTTP 응답 코드를 같이 담아서 보낸 ResponseEntity
와는 다르게 @ResponseStatus
어노테이션을 사용해서 응답 코드를 전달 할 수 있다 . 단, 동적으로는 변환할 수 없기 때문에 프로그램 조건에 따라서 응답 코드를 동적으로 전달하고 싶다면 ResponseEntity
를 사용해야 한다.
@RestController
vs @Controller
이 둘의 차이는 단순히 뷰를 반환하느냐 아니냐의 차이이다.
@ResponseBody
를 사용하면 @Controller
가 뷰를 반환하는 것이 아니라 HTTP message body에 데이터를 그대로 넣는다고했다. 즉 @RestController
는 이 둘을 합친 것으로 코드를 보면 단순하게 이해할 수 있다.
HTTP message body에 데이터를 입력하여 반환하는 말 그대로 Rest API를 만들 때 사용하는 컨트롤러이다.
HTTP 메시지 컨버터
지금까지 HTTP message body 내부에 JSON 형태로 내보내거나 JSON 형태로 받을 때 HTTP message 컨버터를 이용해서 변환해주었다. 그럼 이 Http 메시지 컨버터가 어떻게 동작하는지 알아보자.
@ResponseBody
를 사용한다면 HTTP message body에 문자 내용을 그대로 반환하므로 viewResolver
대신 HttpMessageConverter
가 동작했었다. 스프링은 이 외에도 다음과 같은 경우 HTTP 메시지 컨버터를 사용한다.
- HTTP request :
@RequestBody
,HttpEntity(RequestEntity)
- HTTP response :
@ResponseBody
,HttpEntity(ResponseEntity)
HttpMessageConverter는 요청과 응답이 들어왔을 때 이를 어떻게 사용해야할지 체크를 하는데
canRead()
,canWrite()
를 통해서 메시지 컨버터가 해당 클래스, 미디어 타입을 지원할 수 있는지 체크하고read()
,write()
를 통해서 메시지를 읽고쓴다.
부트에서는 기본적으로 다양한 메시지 컨버터를 제공하는데 앞전에 스프링MVC 구조에서 url 요청이 들어왔을 때 HandlerMapping과 HandlerAdapter를 찾을 때 우선순위가 있던 것처럼 메시지 컨버터도 우선순위를 통해서 먼저 검색한다.
0 = ByteArrayHttpMessageConverter
1 = StringHttpMessageConverter
2 = MappingJackson2HttpMEssageConverter
...
물론 모든 컨버터가 HttpMessageConverter
인터페이스를 상속한 클래스이다.
각각의 주요 내용을 알아보면
ByteArrayHttpMessageConverter
:byte[]
데이터 처리- 클래스 타입 :
byte[]
, 미디어 타입 :*/*
- 요청)
@RequestBody byte[] data
- 응답)
@ResponseBody
,return byte[]
, 미디어 타입 :application/octet-stream
- 클래스 타입 :
StringHttpMessageConverter
:String
데이터 처리- 클래스 타입 :
String
, 미디어 타입 :*/*
- 요청)
@RequestBody String data
- 응답)
@ResponseBody
,return String
, 미디어 타입 :text/plain
- 클래스 타입 :
MappingJackson2HttpMessageConverter
:Json
데이터 처리- 클래스 타입 :
객체
orHashMap
, 미디어 타입 :application/json
- 요청)
@RequestBody HelloData data
- 응답)
@ResponseBody
,return data
, 미디어 타입 :application/json
- 클래스 타입 :
각각의 되는 경우와 안되는 경우를 알아보자
StringHttpMessageConverter
content-type : application/json
@RequestMapping
void hello(@ReqeustBody String data) {}
이 경우 byte는 아니기 때문에 String 컨버터로 넘어온다. 클래스 타입이 String이고 content-type이 json으로 들어왔지만 String 컨버터는 모든 미디어 타입을 허용하기 때문에 String 컨버터가 동작한다.
MappingJackson2HttpMessageConverter
content-type : application/json
@RequestMapping
void hello(@RequestBody HelloData data) {}
byte도 아니고 클래스타입이 객체이기 때문에 String 컨버터도 아니다. 따라서 content-type이 json인 덕분에 json 컨버터가 동작한다.
?
content-type : text/html
@ReqeustMapping
void hello(@ReqeustBody HelloData data) {}
byte도 아니고 클래스타입이 객체이기 때문에 String 컨버터도 아니다. 또한 content-type이 json이 아니기 때문에 json 컨버터도 아니다. 따라서 올바른 값을 받아내지 못한다.
HTTP 컨버터가 동작하는 과정을 정리하면 다음과 같다.
HTTP Request 읽기
- HTTP 요청이 오면 컨트롤러에서
@ReuqestBody
,HttpEntity
파라미터를 사용한다. - 메시지 컨버터가 해당 메시지를 읽을 수 있는지 어떤 컨버터를 사용해야하는지
canRead()
로 확인한다. canRead()
조건을 만족하는 컨버터를 가져와서read()
를 호출해 객체를 생성하고 반환한다.
HTTP Response 응답
- 컨트롤러에서
@ResponseBody
,@HttpEntity
로 값이 반환된다. - 메시지 컨버터가 해당 메시지를 쓸 수 있는지 어떤 컨버터를 사용해야하는지
canWrite()
로 확인한다. canWrite()
조건을 만족하는 컨버터를 가져와서@write()
를 호출해 HTTP message body에 데이터를 생성해 반환한다.
* 김영한님의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술을 듣고 기록한 학습 자료입니다.
'Spring > Spring MVC' 카테고리의 다른 글
[스프링 MVC 1편] 스프링 MVC - 구조 이해 (0) | 2021.04.23 |
---|---|
[스프링 MVC 1편] MVC 프레임워크 만들기 (0) | 2021.04.21 |
[스프링 MVC 1편] 서플릿, JSP, MVC 패턴 (0) | 2021.04.19 |
[스프링 MVC 1편] 서블릿 (0) | 2021.04.12 |
[스프링 MVC 1편] 웹 애플리케이션의 이해 (1) | 2021.04.07 |
- Total
- Today
- Yesterday