Go, Vantage point
가까운 곳을 걷지 않고 서는 먼 곳을 갈 수 없다.
Github | https://github.com/overnew/
Blog | https://everenew.tistory.com/
티스토리 뷰
* 김영한님의 스프링 MVC 1편 강좌를 수강하며 정리한 글입니다. *
이전 글에서는 스프링에서 MVC 패턴에 대해 공부하였다.
이번에는 기능에 대해 알아보자.
기초 기능 활용
Welcome Page
Jar 파일로 세팅한 스프링 부트에서는 /resource/static/index.html 파일을 Welcome Page(기본 페이지)로 만들어 준다.
Logging
서버에서는 System.out 과 같이 항상 출력되는 메서드를 사용 하지 않고, 레벨에 따른 로그 메시지를 남긴다.
로그 메시지는 실행 정보들도 같이 출력되기 때문에 디버깅에 유용하다.
특히 별도의 파일로도 남겨둘 수 있다.
스프링 부트 스타터에는 Logging 라이브러리(Logback)를 포함하고 있다.
여러 로그 라이브러리를 통합하는 인터페이스는 SLF4J이므로, Logback는 인터페이스의 구현체이다.
로그의 레벨은 trace -> debug -> info -> warn -> error 순으로 높다.
만약 가장 하위 레벨인 trace 로그까지 출력하도록 설정하면, 이보다 상위 레벨인 모든 로그가 출력된다.
info 레벨까지로 세팅하면 info, warn, error 까지만 출력된다.
보통 개발 서버가 debug까지, 운영 서버는 로그가 많기 때문에 info까지 출력한다.
@Slf4j //lombok이 지원
@RestController
public class LogTestController {
//getClass()는 현재 클래스를 의미, lombok이 log 인스턴스를 자동으로 만들어줌
//private final Logger log = LoggerFactory.getLogger(getClass());
@GetMapping("/log-test")
public String logTest(){
String name = "spring";
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는 REST API 스타일을 지원하기 위해 만들어진 어노테이션이다.
이를 적용하면 맵핑된 메서드의 문자열 반환값이 그대로 HTTP Body에 담겨 응답 메시지로 넘겨진다.
추가적으로 최적화를 위해 로그에는 문자열을 + 연산으로 합쳐 출력하지 않는다.
문자열끼리 +연산을 진행시 문자열 버퍼 인스턴스가 생성되서 새로운 문자열을 만들어 낸다.
따라서 불필요한 성능 낭비가 발생하는데, 서버에서 수 많은 로그들이 모두 +연산을 사용한다면 출력되지 않는 로그들도 연산이 진행된다.
하지만 {}, param으로 작성하면 출력 시점에 파라미터 값이 넘어가서 lazy한 연산이 구현된다.
Request Mapping
이전까지는 요청에 대한 서비스는 단순 Url로만 매칭을 했지만, 여러 요소를 조합해서 매칭도 가능하다.
@RequestMapping({/url1, /url2, ...}) 로 여러개의 url을 매핑 시킬 수 있다.
만약 method 속성으로 HTTP 메서드를 지정하지 않는다면, 어떤 메서드로 요청이 와도 호출이 된다.
method를 지정한다면 다른 메서드로 접근시 오류메시지를 반환한다.
@PathVariable
url 경로에 값이 담겨 있다면 다음과 같이 @PathVariable 어노테이션으로 추출도 가능하다.
@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data){
log.info("mappingPath userId = {}",data);
return "ok";
}
이러한 변수를 경로 변수라고 한다.
변수 명이 같다면 그대로 @PathVariable String userId로 사용해도 자동으로 인식해 준다.
@PathVariable 은 다중으로도 사용 가능하다.
//특정 유저의 특정 주문을 조회
@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId, @PathVariable String orderId){
log.info("mappingPath userId = {}, orderId = {}",userId,orderId);
return "ok";
}
@RequestMapping(params = "key=value" , headers = "key=value")로 특정 파라미터나 헤드값이 오는 경우 매핑시킬 수도 있다.
HTTP 본문의 타입인 Content-Type에 따른 분류도 가능하다.
@PostMapping(value = "/mapping-consume", consumes = "application/json")
public String mappingConsumes(){
log.info("mappingConsumes");
return "ok";
}
consumes는 데이터를 소비하는 서버의 입장에서의 타입이다.
produces는 응답 데이터를 생산하는 타입을 의미하는데, 클라이언트는 자신이 Accept할 수 있는 타입을 요청시 HTTP 헤더에 명시한다.
만약 text/html 만을 Accept한다면 @RequestMapping(produces ="text/html")로만 해당 요청을 받을 수 있다.
이제 같은 url 요청이라도 method로 구분해 매핑하면 API를 설계할 수 있다.
@GetMapping("/mapping/users") //회원 목록 조회 역할
public String user(){
return "get users";
}
@PostMapping("/mapping/users") //회원 등록 역할
public String addUser(){
return "post user";
}
HTTP Request
1. 헤더 조회
어노테이션을 적용하는 것은 명시된 매개변수만 받을 수 있는 인터페이스의 구현과는 다르게, 원하는 매개변수를 모두 받아올 수 있다.
@RequestMapping("/header")
public String headers(
HttpServletRequest request,
HttpServletRequest response,
HttpMethod httpMethod, // http의 methode
Locale locale,
@RequestHeader MultiValueMap<String, String> headerMap, // 모든 헤더의 key,value
@RequestHeader("host") String host, //특정 헤더의 value
@CookieValue(value = "myCookie", required = false) String cookie // 쿠키가 없다면 null
...
){
...
}
특히 MultiValueMap에는 하나의 key로 여러 value를 저장 할 수 있다. (키의 중복이 가능)
사용 가능한 수 많은 종류의 매개변수는 스프링의 공식 문서를 확인하자.
2. 파라미터
HTTP에서 요청으로 데이터를 보내는 3가지 방법에대해서는 저번 글에서 정리하였다.
@RequestParam
GET 방식이 전달하는 쿼리 파라미터와 POST 방식이 전달하는 HTML Form은 둘다 쿼리 파라미터로 전송되므로 구분없이 조회가 가능하다.
저번 글에서는 request.getParameter("paramName") 으로 파라미터를 꺼내 사용했었다.
하지만 이제 서블릿이 아닌 @RequestMapping 어노테이션을 적용해 사용하고 있으므로 지원하는 매개변수는 모두 받아와서 사용할 수 있다.
@RequestParam("paramName") Type name 을 사용하면 매개변수에서 바로 파라미터를 받아올 수 있다.
결국 request.getParameter("paramName") 과 동일한 역할을 해준다.
이때 paraName과 매개변수의 이름을 같데 해주면 다음과 같이 생략이 가능하다.
@RequestParam Type paramName
만약 기본 타입(String, int, ...)이라면 @RequestParam 어노테이션마저 생략이 가능하다. (자동으로 적용됨.)
@RequestParam의 옵션으로 required = false 가 존재한다.
default 값은 true로써, 반드시 해당 파라미터가 넘어와야함을 의미한다.
반대로 false는 해당 파라미터가 없어도 호출된다.
ture인 값을 넘겨주지 않는다며 400 에러(Bad Request)가 발생한다.
디폴트 값을 설정해 두는 옵션도 있다.
defaultValue = "디폴트" , 이 옵션을 적용하면 파라미터가 전달되지 않아도 디폴트 값을 사용하기 때문에 required와 사용할 필요가 없다.
모든 파라미터를 가져오고 싶다면 Map자료구조를 사용하면 된다.
@RequestParam Map<String, Object> paramMap
@ModelAttribute
프런트 컨트롤러 패턴을 공부할때 Model에 데이터를 저장해 전달한 것을 기억할 것이다.
스프링은 @ModelAttribute 어노테이션으로 이를 해결한다.
@ModelAttribute 어노테이션이 적용된 매개변수는 해당 객체를 생성하고, 파라미터의 이름으로 해당 객체의 setter를 찾아 값이 바인딩된다.
예를 들어 파라미터로 age가 넘어왔다면, @ModelAttribute 적용된 객체의 setAge()를 찾아서 실행한다.
만약 age가 정수형이라면, 문자열이 넘어왔을때 setAge("문자열")은 호출이 불가능하여 BindingException이 발생한다.
@Data // getter, setter, ToString... 자동 생성
public class User {
private String username;
private int age;
}
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute User userData){
log.info("username= {}, age ={}", userData.getUsername(), userData.getAge());
return "ok";
}
사실 @ModelAttribute도 생략이 가능하지만 벌써부터 어지러우니 모두 적용하는 습관을 들이자.
@RequestParam은 자바의 기본 타입들, @ModelAttribute은 그 이외의 타입(새로 만든 클래스)의 파라미터 처리에 사용된다.
3. HTTP Message
텍스트
@RequestParam와 @ModelAttribute는 쿼리 파라미터로 전달되는 값을 꺼내는 방식이였다.
하지만 HTTP의 메시지 본문에는 쿼리 파리미터 이외의 값들이 전달될 수 있다.
메시지 바디의 텍스트를 InputStream을 통해 읽어는 것은 [Spring] 13. Servlet, 서블릿에서 확인 했었다.
스프링 MVC는 매개변수로 InputStream과 Writer 인스턴스도 받아올 수 있다.
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("messageBody={}", messageBody);
responseWriter.write("ok");
}
스프링이 지원하는 HttpEntity는 Http의 헤더와 바디 정보를 편하게 조회할수 있게 만들어 준다.
HTTP 요청 데이터는 매개변수를 통해 받아올 수 있고 반환 값으로 응답 데이터를 보낼 수도 있다.
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) throws IOException {
String messageBody = httpEntity.getBody(); // 또는 getHeader()
log.info("messageBody={}", messageBody);
return new HttpEntity<>("첫 파라미터가 바디 메시지로 바로 전송됨", 두번째는 상태코드);
}
스프링은 거기에다가 @RequestBody와 @ResponseBody 어노테이션을 제공한다.
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) throws IOException {
log.info("messageBody={}", messageBody);
return "return 문자열을 바로 응답 메시지로";
}
@RequestBody는 매개변수에 바로 메세지의 바디를 받아올 수있게 해주고, (헤더는 @RequestHeader)
@ResponseBody는 반환하는 문자열을 바로 응답 메시지의 바디로 전달해준다.
JSON
이전에는 메시지 본문의 json 데이터를 inputStream으로 읽어와서 ObjectMapper로 자바 객체로 만들 수 있었다.
하지만 @RequestBody를 쓰면 바로 본문 데이터를 읽어 올 수도 있고, json 데이터를 바로 객체로 변환할 수 있다.
@RequestBody의 이런 작업들은 HTTP 메시지 컨버터가 자동으로 원하는 객체로 변환해 주기 때문에 사용할 수 있다.
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody User userData) throws IOException {
log.info("username ={}, age={}", userData.getUsername(), userData.getAge());
return "okidoki";
}
사실 HTTP 메시지 컨버터도 결국 내부에서는 읽어온 데이터를 ObjectMapper로 변환하는 것은 동일하다.
(헤더의 Content-Type을 통해 메시지 컨버터가 json 데이터를 인식한다.)
@ResponseBody는 객체도 반환형으로 설정이 가능하다. 이때 객체의 필드가 HTTP 메시지 컨버터에 의해, Json 데이터로 변환되어 http 본문에 담기게된다.
HTTP Response
[Spring] 12. 웹 어플리케이션 서버와 쓰레드 풀에서 서버의 3가지 응답 방식에 대해 살펴보았다.
정적 리소스 반환
서버가 저장 중인 파일(HTML, 이미지, 영상)을 바로 제공
뷰 템플릿
WAS가 요청에 따라 동적인 HTML을 생성해 반환한다.
예를 들어 유튜브는 클라이언트마다 다른 영상 추천을 동적으로 만들어 HTML로 제공한다.
HTTP API
HTTP를 사용하면 본문에 HTML이 아닌 JSON과 같은 데이터를 포함시켜 전송이 가능하다.
웹 브라우징에 http:// 태그 URL에 들어가기 때문에 HTML을 보낼 때만 이용한다고 생각할 수 있지만,
HTTP는 범용적인, 네트워크의 애플리케이션 계층의 전송 프로토콜이기 때문에 서버-서버, 클라이언트 - 서버 간의 데이터 전송에도 이용된다.
정적 리소스 반환
스프링 부트가 제공하는 정적 리소스의 경로는 다음과 같다. 해당 폴더까지의 경로는 클라이언트의 접근 경로에는 포함되지 않는다.
/static /public /resources /META-INF/resources
hello-form.html 에 접근하려면, ~/basic/hello-form.html 로 접근할 수 있다.
이러한 정적 리소스는 파일이 그대로 반환된다.
뷰 템플릿
뷰 템플릿은 HTML 코드를 동적으로 생성해 반환한다.
스프링에서 사용하는 템플릿 엔진은 thymeleaf이다.
이전에 글에서 정리했던 내용이기 때문에 추가적인 내용만 정리하면,
이전에는 @RequestMapping 만 사용하였기 때문에 ModelAndView 또는 view의 이름을 문자열로 반환하였다.
만약 @ResponseBody가 적용된다면 반환되는 문자열로 뷰의 이름 찾는 것이 아니라 그대로 메시지 본문에 실린다.
이때 뷰 파일들이 저장되는 폴더는 resouces/templates/이다.
스프링 부트는 자동으로 thymeleaf를 설정한다.
이때 설정 정보 중에 템플릿의 prefix경로가 /templates/ 로, suffix가 .html로 세팅되기 때문에 뷰의 이름에서 이를 생략할 수 있다.
3. HTTP API (REST API)
HTML 이외의 데이터를 응답 메시지 본문에 싣는 경우를 말한다.
이는 @ResponseBody를 사용하는 위의 내용과 동일하다.
추가적으로 ResponseEntity<>를 이용하는 방식은 상태 코드도 세팅할 수 있었다.
return new ResponseEntity<>(객체, HttpStatus.OK);
하지만 @ResponseBody에는 이러한 기능 없는데, 이를 위한 @ResponseStatus(HttpStatus) 어노테이션이 있다.
이를 적용하면 미리 상태 코드를 세팅해둘 수 있다.
동적인 세팅을 원한다면 ResponseEntity를 이용하자.
모든 메서드에 @ResponseBody의 세팅을 하고싶다면 클래스 레벨에 @ResponseBody를 붙이는 것으로 대체할 수 있다.
클래스 레벨의 @ResponseBody와 @Controller를 합친 것이 REST API를 의미하는 @RestController이다.
HTTP Message Converter
@ResponseBody, @RequestBody, HttpEntity<>를 사용하면 view Resolver가 아닌 HTTP 메시지 컨버터가 동작한다.
메시지 컨버터는 반환하는 타입을 json(객체) 또는 텍스트(string) 형식으로 변환해 HTTP에 담아준다.
이 메시지 컨버터는 mvc 패턴 중에 어디서 실행 되는 걸까?
어노테이션으로 수많은 종류의 매개변수를 지원해주는 핸들러 어댑터는 사실, Argument Resolver를 통해 필요한 객체들을 생성한다.
모든 매개변수가 준비가 되면 핸들러(컨트롤러)에 넘어가면서 호출 된다.
거기에다 여러가지 반환 형도 지원하는데, 이런 것들 변환 처리해주는 것은 ReturnValueHandler이다.
이때 Argument Resolver는 HTTP에서 메시지나 헤더를 읽어와야하고, ReturnValueHandler는 데이터를 HTTP로 가공해야한다. 여기서 동작하는것이 HTTP 메시지 컨버터이다.
'개발 > Spring' 카테고리의 다른 글
[Java] MapStruct로 Entity와 DTO 변환하기(@Mapper) (0) | 2022.09.06 |
---|---|
[Spring] 16. Spring MVC (0) | 2022.07.11 |
[Spring] 15. 프런트 컨트롤러 패턴, 어댑터 패턴 (0) | 2022.07.10 |
[Spring] 14. 템플릿 엔진과 MVC 패턴 (0) | 2022.07.08 |
[Spring] 13. Servlet, 서블릿 (0) | 2022.07.06 |