반응형
Welcome 페이지
- 스프링 부트에 Jar 를 사용하면 /resources/static/index.hml 위치에 index.html 파일을 두면 Welcome 페이지로 처리해준다.
로깅
- 스프링 부트 라이브러리는 스프링 부트 로깅 라이브러리를 포함한다.
- SLF4J (Logback, Log4J, Log4J2 통합) - 인터페이스
- Logback - 구현체 (대부분 사용)
@Slf4j // 애노테이션 설정으로 편리하게 사용
@RestController // 디폴트의 뷰 반환이 아닌 문자 반환설정 (http 바디에 담아서 전송)
public class LogTestController {
// private final Logger log = LoggerFactory.getLogger(getClass()); //해당 클래스 log 설정
@RequestMapping("/log-test")
public String logTest() {
String name = "Spring";
// 로그 레벨별 : 밑으로 갈수록 심각, 운영서버는 주로 info 레벨 부터
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";
}
}
- 로그 레벨 설정 (TRACE > DEBUG > INFO > WARN > ERROR) - default : info레벨
- 개발서버 : debug
- 운영서버 : info
#hello.springmvc 패키지와 그 하위 로그 레벨 설정 , logging.level.root 로 전체 경로 설정도 가능
logging.level.hello.springmvc=info
- 올바른 로그 사용법
- log.debug("data="+data) - 사용 X
- 로그 출력 레벨을 info로 설정해도 해당 코드에 있는 "data="+data가 실제 실행이 되어 버린다. 결과적으로 문자 더하기 연산이 발생한다.
- log.debug("data={}", data)
- 로그 출력 레벨을 info로 설정하면 아무일도 발생하지 않는다. 따라서 앞과 같은 의미없는 연산이 발생하지 않는다.
- log.debug("data="+data) - 사용 X
- 로그 사용시 장점
- 파일, 네트워크등 로그를 별도로 남길 수 있다.
- 성능 자체가 System.out 보다 좋다. (내부 버퍼링, 멀티 쓰레드 등)
- 로그 레벨별 설정을 통해 상황에 맞게 로그 출력 가능
요청 매핑
@PathVariable
/**
* PathVariable 사용
* 변수명이 같으면 생략 가능
* @PathVariable("userId") String userId -> @PathVariable userId
* /mapping/userA
*/
@GetMapping("/mapping/{userId}") //http://localhost:8080/mapping/userA, 템플릿화
public String mappingPath(@PathVariable("userId") String data) {
log.info("mappingPath userId={}", data);
return "ok";
}
//http://localhost:8080/mapping/users/userA/orders/100
@GetMapping("/mapping/users/{userId}/orders/{orderId}")
public String mappingPath(@PathVariable String userId, @PathVariable Long
orderId) {
log.info("mappingPath userId={}, orderId={}", userId, orderId);
return "ok";
}
- 자주 사용하는 방식
- url 변수 값과 파라미터 변수 값이 다르면 @PathVariable("url 변수 값 지정"), 같으면 생략 가능하다.
파라미터 추가 매핑
/**
* 파라미터로 추가 매핑
* params="mode",
* params="!mode"
* params="mode=debug"
* params="mode!=debug" (! = )
* params = {"mode=debug","data=good"}
*/
//http://localhost:8080/mapping-param?mode=debug 추가 파라미터 매핑
@GetMapping(value = "/mapping-param", params = "mode=debug")
public String mappingParam() {
log.info("mappingParam");
return "ok";
}
- 추가된 params 정보가 있어야만 호출 된다. 사용할 일이 별로 없다.
특정 헤더 추가 매핑
/**
* 특정 헤더로 추가 매핑
* 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";
}
- 헤더에 추가 정보를 넣어서 요청해야 된다. (postman 사용)
미디어 타입 조건 매핑
/**
* Content-Type 헤더 기반 추가 매핑 Media Type
* consumes="application/json"
* consumes="!application/json"
* consumes="application/*"
* consumes="*\/*"
* MediaType.APPLICATION_JSON_VALUE
*/
@PostMapping(value = "/mapping-consume", consumes = "application/json")
public String mappingConsumes() {
log.info("mappingConsumes");
return "ok";
}
- HTTP 요청 Content-Type를 consume을 통해 조건을 매핑할 수 있다.
- consumes = {"text/plain", "application/*"}과 같이 지정 가능
/**
* Accept 헤더 기반 Media Type
* produces = "text/html"
* produces = "!text/html"
* produces = "text/*"
* produces = "*\/*"
*/
@PostMapping(value = "/mapping-produce", produces = "text/html")
public String mappingProduces() {
log.info("mappingProduces");
return "ok";
}
- HTTP 요청 Accept, produce - header 의 Accept(받아드릴 수 있는 타입)가 일치해야 된다.
예시)
produces = "text/plain"
produces = {"text/plain", "application/*"}
produces = MediaType.TEXT_PLAIN_VALUE
produces = "text/plain;charset=UTF-8"
요청 매핑 - API
회원 관리 API
- 회원 목록 조회: GET /users
- 회원 등록: POST /users
- 회원 조회: GET /users/{userId}
- 회원 수정: PATCH /users/{userId}
- 회원 삭제: DELETE /users/{userId}
@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;
}
}
- class 에 @RequestMapping으로 공통된 url을 관리할 수 있다.
HTTP 요청 - 기본, 헤더 조회
@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";
}
- locale : 우선순위가 가장 높은 언어
- headerMap={host=[localhost:8080], connection=[keep-alive], sec-ch-ua=["Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"] ~
- header host=localhost:8080
- MultiValueMap : MAP과 유사한데, 하나의 키에 여러 값을 받을 수 있다.
- HTTP header, HTTP 쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용한다
- map.get("key")를 하면 List를 반환한다.
HTTP 요청 - 쿼리 파라미터, HTML Form
- 클라이언트에서 서버로 요청 데이터 전달하는 주 3가지 방법
- GET - 쿼리 파라미터
- POST - HTML Form
- 메시지 바디에 쿼리파라미터 형식으로 전달 (x-www-form-urlencoded)
- Http messsage body에 데이터를 직접 담아서 요청
- HTTP API에서 주로 JSON 사용
- POST, PUT, PATCH
@Slf4j
@Controller
public class RequestParamController {
//http://localhost:8080/request-param-v1?username=hello&age=20
@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");
}
@ResponseBody // 클래스 레벨이 @Controller 인데 반환값을 데이터로 하고 싶을 때
@RequestMapping("/request-param-v2")
public String requestParamV2(
@RequestParam("username") String memberName,
@RequestParam("age") int memberAge
) {
log.info("username = {}, age = {},", memberName, memberAge);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-v3") //변수 이름이 같으면 name = 생략 가능
public String requestParamV3(
@RequestParam String username,
@RequestParam int age
) {
log.info("username = {}, age = {},", username, age);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-v4") //String, int , Integer 등 단순 타입이면 어노테이션도 생략 가능
public String requestParamV4(
String username,
int age
) {
log.info("username = {}, age = {},", username, age);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-required") //required 가 true이면 무조건 있어야한다. 디폴트 true
public String requestParamRequired(
@RequestParam(required = true) String username, //필수 값이여도 ""인경우는 빈문자로들어와서 ok, 아예없으면 배드리퀘스트 주의
@RequestParam(required = false) Integer age //Integer는 객체이기때문에 Null 가능, int 는 널일 수 없어서 객체로 선언
) {
log.info("username = {}, age = {},", username, age);
return "ok";
}
@ResponseBody
@RequestMapping("/request-param-default") // defaultValue를 사용하면 사실상 required가 필요없다. 비어있으면 채워주기 때문에 "" 경우도 기본값으로 변경처리
public String requestParamDefault(
@RequestParam(required = true, defaultValue = "guest") String username,
@RequestParam(required = false, defaultValue = "-1") int age
) {
log.info("username = {}, age = {},", username, age);
return "ok";
}
@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";
}
}
- 클래스가 @Controller 일 때 메서드에서 데이터를 반환하고 싶으면 메서드에 @ResponseBody를 사용할 수 있다.
- null과 ""는 다르다. ""는 빈문자로 인식
HTTP 요청 파라미터 - @ModelAttribute
@Data
public class HelloData {
private String username;
private int age;
}
------------------------------
@Slf4j
@Controller
public class RequestParamController {
...
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
log.info("username = {}, age = {},", helloData.getUsername(), helloData.getAge());
return "ok";
}
@ResponseBody
@RequestMapping("/model-attribute-v2")
public String modelAttributeV2(HelloData helloData) {
log.info("username = {}, age = {},", helloData.getUsername(), helloData.getAge());
return "ok";
}
}
- 스프링은 생략시 다음과 같은 규칙을 적용한다.
- String, int, Integer 같은 단순 타입 - @RequestParam
- 나머지 @ModelAttribute (argument resolver (HttpServletRequest 와 같은) 타입 제외)
HTTP 요청 메시지 - 단순 텍스트
- HTTP 메시지 바디를 통해 데이터가 직접 데이터가 넘어오는 경우는 @RequestParam, @ModelAttribute를 사용할 수 없다.
@Slf4j
@Controller
public class RequestBodyStringController {
@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");
}
@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");
}
@PostMapping("/request-body-string-v3") // 메시지 컨버터 기능
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) throws IOException {
String messageBody = httpEntity.getBody(); //http 메시지에있는 바디를 꺼냄
log.info("messageBody={}", messageBody);
return new HttpEntity<>("ok");
}
@ResponseBody
@PostMapping("/request-body-string-v4") // 메시지 컨버터 기능
public HttpEntity<String> requestBodyStringV4(@RequestBody String messageBody) throws IOException {
log.info("messageBody={}", messageBody);
return new HttpEntity<>("ok");
}
}
- 파라미터에 @RequestBody를 사용하면 HTTP 메시지 바디 정보를 편리하게 조회 가능
- 헤더 정보가 필요하면 HttpEntity 나 @RequestHeader를 사용
- 메시지 바디를 직접 조회하는 기능은 요청 파라미터를 조회하는 @RequestParam , @ModelAttribute 와는 전혀 관계가 없다.
HTTP 요청 메시지 - 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("messageBody={}", messageBody);
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
response.getWriter().write("ok");
}
@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";
}
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData helloData) throws IOException {
log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
return "ok";
}
@ResponseBody
@PostMapping("/request-body-json-v4") //HttpEntity 사용
public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity) throws IOException {
HelloData data = httpEntity.getBody();
log.info("username={}, age={}", data.getUsername(), data.getAge());
return "ok";
}
@ResponseBody
@PostMapping("/request-body-json-v5") // 반환형도 객체로 적용 가능 , 들어올 때 나갈 때 모두 적용됨
public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
log.info("username={}, age={}", data.getUsername(), data.getAge());
return data;
}
}
- @RequestBody 객체 파라미터 - 직접 만든 객체를 지정
- HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을 우리가 원하는 문자나 객체 등으로 변환해준다 (문자, json 등)
- 생략 불가능, 생략시에 규칙에 의해 @ModelAttribute가 적용이 되버린다.
- JSON 요청 -> HTTP 메시지 컨버터 -> 객체
- @ResponseBody
- 응답의 경우에도 해당 객체를 HTTP 메시지 바디에 직접 넣어줄 수 있다.
- HttpEntity를 사용해도 된다.
- 객체 -> HTTP 메시지 컨버터 -> JSON 응답
HTTP 응답 - 정적 리소스, 뷰 템플릿
- 스프링(서버)에서 응답 방법 주로 3가지
- 정적리소스
- 예) 웹 브라우저에 정적인 HTML, CSS, JS를 제공할 때
- 뷰 템플릿 사용
- 예) 웹 브라우저에 동적인 HTML을 제공할 때
- HTTP 메시지 사용
- 예) HTTP API를 제공하는 경우 데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보낸다.
뷰 템플릿
- 뷰 템플릿 경로 : src/main/resources/templates
@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") //권장하지 않는 방법 (불명확) - Controller의 경로와 view의 논리적 이름이 같은 경우 생략
public void responseViewV3(Model model) {
model.addAttribute("data", "hello!");
}
}
- String을 반환하는경우 @ResponseBody가 없으면 response/hello로 뷰 리졸버가 실행되어서 뷰를 찾고 렌더링,
- 있으면 뷰 리졸버를 실행하지 않고, HTTP 메시지 바디에 직접 response/hello라는 문자가 입력된다.
# 타임리프 설정
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
HTTP 응답 - HTTP API, 메시지 바디 직접 입력
- HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보냄
@Slf4j
@Controller
public class ResponseBodyController {
@GetMapping("/response-body-string-v1")
public void responseBodyV1(HttpServletResponse response) throws IOException {
response.getWriter().write("ok");
}
@GetMapping("/response-body-string-v2")
public ResponseEntity<String> responseBodyV2() {
return new ResponseEntity<>("ok", HttpStatus.OK);
}
@ResponseBody
@GetMapping("/response-body-string-v3")
public String responseBodyV3() {
return "ok";
}
@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);
}
@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 메시지 바디에 있는 데이터를 받아오게 되면ResponseEntity
와 같이 Http 상태 코드를 지정할 수 없다. 그렇기 때문에@ResponseStatus
를 통해 지정해야 한다.@ResponseStatus
는 애노테이션을 지정하기때문에 동적으로 변경할 수없다.- 동적으로 바꾸려면 ResponseEntity를 사용하면 된다.
@ResponseBody
를 클래스레벨에 지정하면 모든 메서드에 Http 메시지 바디에 있는 데이터를 받을 수 있다. 즉@RestController
가@ResponseBody
와@Controller
를 합친 기능이다.
HTTP 메시지 컨버터
- HTTP API처럼 JSON 데이터를 HTTP 메시지 바디에 직접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 사용하면 편리하다.
@ResponseBody 동작 과정
@ResponseBody
는viewResolver
대신에HttpMessageConverter
를 거쳐 처리 한다.- 기본 문자 처리 :
StringHttpMessageConverter
- 기본 객체 처리 :
MappingJackson2HttpMessageConverter
- byte 처리(
ByteArrrayHttpMessageConverter
) 등 여러 종류들이 등록되어 있다.
- 기본 문자 처리 :
스프링 MVC의 메시지 컨버터
- HTTP 요청 :
@RequestBody
,HttpEntity(RequestEntity)
- 메시지 컨버터가 읽을 수 있는지 확인하기 위해 canRead() 호출
- 대상 클래스 타입을 지원하는가
- 예)
@RequestBody
의 대상 클래스 (byte[], String, HelloData) - HTTP 요청의 Content-Type 미디어 타입을 지원하는가
- 예 ) text/plain, application/json , */*
- canRead() 조건을 만족하면 read()를 호출해서 객체를 생성하고 반환
- HTTP 응답 :
@ResponseBody
,HttpEntity(ResponseEntity)
- 메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해 canWrite() 호출
- 대상 클래스 타입을 지원하는가
- 예 ) return의 대상 클래스(byte[], String, HelloData)
- Http 요청의 Accept 미디어 타입을 지원하는가? (
@RequestMapping
의 produces) - 예 ) text/plain, application/json , */*
- canWrite()조건을 만족하면 write()로 호출해서 HTTP 응답 메시지 바디에 데이터를 생성
주요 메시지 컨버터 동작
- ByteArrayHttpMessageConverter : byte[] 데이터 처리
- 클래스 타입 : byte[] , 미디어타입 */*
- 요청 예 )
@RequestBody
byte[] data - 응답 예 )
@ResponseBody
return byte[] 쓰기 미디어타입application/octet-stream
- StringHttpMessageConverter : String 문자 처리
- 클래스 타입 : String, 미디어타입 */*
- 요청 예 ) @RequestBody String data
- 응답 예 ) @ResponseBody return "ok" , text/plain
- MappingJackson2HttpMessageConverter: application/json
- 클래스 타입 : 객체 또는 HashMap, 미디어타입 application/json
- 요청 예 ) : @RequestBody HelloData data
- 응답 예 ): @ResponseBody return HelloData 쓰기 미디어타입 application/json
0 = ByteArrayHttpMessageConverter
1 = StringHttpMessageConverter
2 = MappingJackson2HttpMessageConverter
-------------------------------------------------
content-type: application/json
@RequestMapping
void hello(@RequetsBody String data) {}
- 0 관련 없으므로 패스, 1 : String , */*이므로 채택 -> StringHttpMessageConverter 사용
content-type: application/json
@RequestMapping
void hello(@RequetsBody HelloData data) {}
- 0, 1 패스, 2: 객체, application/json 이므로 채택 -> MappingJackson2HttpMessageConverter 사용
content-type: text/html
@RequestMapping
void hello(@RequetsBody HelloData data) {}
- 0,1 패스 , 2: 객체 지만 , html 타입 이므로 탈락 -> 메시지컨버터 동작x
요청 매핑 핸들러 어댑터 구조
- Http 메시지 컨버터는 MVC 동작 과정에서 어디쯤 사용될까? 정답은
DispatcherServlet
에서RequestMapping
과정에서Argument Resolver
,ReturnValueHandler
에서 동작한다.
InputStream
,HttpServletRequest
,@RequestParam
,@ModelAttribute
등 다양한 파라미터를Argument Resolver
덕분이다.- 애노테이션 기반 컨트롤러 처리하는
RequestMappingHandlerAdaptor
는ArgumentResolver
를 호출해서 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터 값(객체)을 생성한다. - 요청의 경우
ArgumentResolver
을 통해 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성 - 응답의 경우
ReturnValueHandler
를 통해 HTTP 메시지 컨버터를 호출해서 응답 결과 생성 - 확장의 경우
WebMvcConfigurer
를 상속 받아 스프링 빈으로 등록하면된다.(실제로 사용할일 거의 없다)
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver>
resolvers) {
//...
}
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>>
converters) {
//...
}
};
}
본 포스팅은 인프런 김영한님 강의(스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술)를 토대로 정리한 내용입니다.
반응형