제육's 휘발성 코딩
Published 2021. 8. 13. 09:44
스프링 MVC - 기본 기능 🔷 Spring/MVC
반응형

1. Welcome 페이지

  • 스프링 부트에 Jar 를 사용하면 /resources/static/index.hml 위치에 index.html 파일을 두면 Welcome 페이지로 처리해준다.

2. 로깅

  • 스프링 부트 라이브러리는 스프링 부트 로깅 라이브러리를 포함한다.
    • SLF4J (Logback, Log4J, Log4J2 통합) - 인터페이스
    • Logback - 구현체 (대부분 사용)
<code />
@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
<code />
#hello.springmvc 패키지와 그 하위 로그 레벨 설정 , logging.level.root 로 전체 경로 설정도 가능 logging.level.hello.springmvc=info
  • 올바른 로그 사용법
    • log.debug("data="+data) - 사용 X
      • 로그 출력 레벨을 info로 설정해도 해당 코드에 있는 "data="+data가 실제 실행이 되어 버린다. 결과적으로 문자 더하기 연산이 발생한다.
    • log.debug("data={}", data)
      • 로그 출력 레벨을 info로 설정하면 아무일도 발생하지 않는다. 따라서 앞과 같은 의미없는 연산이 발생하지 않는다.
  • 로그 사용시 장점
    • 파일, 네트워크등 로그를 별도로 남길 수 있다.
    • 성능 자체가 System.out 보다 좋다. (내부 버퍼링, 멀티 쓰레드 등)
    • 로그 레벨별 설정을 통해 상황에 맞게 로그 출력 가능

3. 요청 매핑

3.1. @PathVariable

<code />
/** * 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 변수 값 지정"), 같으면 생략 가능하다.

3.2. 파라미터 추가 매핑

<code />
/** * 파라미터로 추가 매핑 * 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 정보가 있어야만 호출 된다. 사용할 일이 별로 없다.

3.3. 특정 헤더 추가 매핑

<code />
/** * 특정 헤더로 추가 매핑 * 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 사용)

3.4. 미디어 타입 조건 매핑

<code />
/** * 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/*"}과 같이 지정 가능
<code />
/** * 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(받아드릴 수 있는 타입)가 일치해야 된다.
<code />
예시) produces = "text/plain" produces = {"text/plain", "application/*"} produces = MediaType.TEXT_PLAIN_VALUE produces = "text/plain;charset=UTF-8"

4. 요청 매핑 - API

4.1. 회원 관리 API

  • 회원 목록 조회: GET /users
  • 회원 등록: POST /users
  • 회원 조회: GET /users/{userId}
  • 회원 수정: PATCH /users/{userId}
  • 회원 삭제: DELETE /users/{userId}
<code />
@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을 관리할 수 있다.

5. HTTP 요청 - 기본, 헤더 조회

<code />
@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를 반환한다.

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

  • 클라이언트에서 서버로 요청 데이터 전달하는 주 3가지 방법
    • GET - 쿼리 파라미터
    • POST - HTML Form
      • 메시지 바디에 쿼리파라미터 형식으로 전달 (x-www-form-urlencoded)
    • Http messsage body에 데이터를 직접 담아서 요청
      • HTTP API에서 주로 JSON 사용
      • POST, PUT, PATCH
<code />
@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과 ""는 다르다. ""는 빈문자로 인식

7. HTTP 요청 파라미터 - @ModelAttribute

<code />
@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 와 같은) 타입 제외)

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

  • HTTP 메시지 바디를 통해 데이터가 직접 데이터가 넘어오는 경우는 @RequestParam, @ModelAttribute를 사용할 수 없다.
<code />
@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 와는 전혀 관계가 없다.

9. HTTP 요청 메시지 - JSON

<code />
@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 응답

10. HTTP 응답 - 정적 리소스, 뷰 템플릿

  • 스프링(서버)에서 응답 방법 주로 3가지
  1. 정적리소스
    • 예) 웹 브라우저에 정적인 HTML, CSS, JS를 제공할 때
  2. 뷰 템플릿 사용
    • 예) 웹 브라우저에 동적인 HTML을 제공할 때
  3. HTTP 메시지 사용
    • 예) HTTP API를 제공하는 경우 데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보낸다.

10.1. 뷰 템플릿

  • 뷰 템플릿 경로 : src/main/resources/templates
<code />
@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라는 문자가 입력된다.
<code />
# 타임리프 설정 spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html

11. HTTP 응답 - HTTP API, 메시지 바디 직접 입력

  • HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보냄
<code />
@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를 합친 기능이다.

12. HTTP 메시지 컨버터

  • HTTP API처럼 JSON 데이터를 HTTP 메시지 바디에 직접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 사용하면 편리하다.

12.1. @ResponseBody 동작 과정

sec6  사진1

  • @ResponseBodyviewResolver 대신에 HttpMessageConverter를 거쳐 처리 한다.
    • 기본 문자 처리 : StringHttpMessageConverter
    • 기본 객체 처리 : MappingJackson2HttpMessageConverter
    • byte 처리(ByteArrrayHttpMessageConverter) 등 여러 종류들이 등록되어 있다.

12.2. 스프링 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 응답 메시지 바디에 데이터를 생성

12.3. 주요 메시지 컨버터 동작

  • 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
<code />
0 = ByteArrayHttpMessageConverter 1 = StringHttpMessageConverter 2 = MappingJackson2HttpMessageConverter ------------------------------------------------- content-type: application/json @RequestMapping void hello(@RequetsBody String data) {}
  • 0 관련 없으므로 패스, 1 : String , */*이므로 채택 -> StringHttpMessageConverter 사용
<code />
content-type: application/json @RequestMapping void hello(@RequetsBody HelloData data) {}
  • 0, 1 패스, 2: 객체, application/json 이므로 채택 -> MappingJackson2HttpMessageConverter 사용
<code />
content-type: text/html @RequestMapping void hello(@RequetsBody HelloData data) {}
  • 0,1 패스 , 2: 객체 지만 , html 타입 이므로 탈락 -> 메시지컨버터 동작x

13. 요청 매핑 핸들러 어댑터 구조

  • Http 메시지 컨버터는 MVC 동작 과정에서 어디쯤 사용될까? 정답은 DispatcherServlet에서 RequestMapping 과정에서 Argument Resolver , ReturnValueHandler에서 동작한다.

sec6  사진2

  • InputStream,HttpServletRequest, @RequestParam, @ModelAttribute 등 다양한 파라미터를 Argument Resolver 덕분이다.
  • 애노테이션 기반 컨트롤러 처리하는 RequestMappingHandlerAdaptorArgumentResolver를 호출해서 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터 값(객체)을 생성한다.
  • 요청의 경우 ArgumentResolver을 통해 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성
  • 응답의 경우 ReturnValueHandler를 통해 HTTP 메시지 컨버터를 호출해서 응답 결과 생성
  • 확장의 경우 WebMvcConfigurer를 상속 받아 스프링 빈으로 등록하면된다.(실제로 사용할일 거의 없다)
<code />
@Bean public WebMvcConfigurer webMvcConfigurer() { return new WebMvcConfigurer() { @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { //... } @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { //... } }; }

본 포스팅은 인프런 김영한님 강의(스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술)를 토대로 정리한 내용입니다.

반응형
profile

제육's 휘발성 코딩

@sasca37

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요! 맞구독은 언제나 환영입니다^^