스프링 MVC 1편 - 섹션5~섹션7 후기

soso·2023년 4월 24일
0

김영한의 스프링 완전 정복 로드맵
스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술
섹션5~섹션7 정리입니다.

섹션5. 스프링 MVC - 구조 이해

스프링 MVC 전체 구조

# 직접 만든 MVC 프레임워크와 스프링 MVC를 비교


# 지금까지 직접 구현해본 프레임 워크와 스프링 프레임

• FrontController → DispatcherServlet
• handlerMappingMap → HandlerMapping
• MyHandlerAdapter → HandlerAdapter
• ModelView → ModelAndView
• viewResolver → ViewResolver
• MyView → View

# DispatcherServlet 구조

org.springframework.web.servlet.DispatcherServlet

  • 스프링 MVC도 프론트 컨트롤러 패턴으로 구현
  • 디스패처 서블릿(DispatcherServlet)이 스프링 MVC의 프론트 컨트롤러

# DispacherServlet 서블릿 등록

  • DispacherServlet 도 부모 클래스에서 HttpServlet 을 상속 받아서 사용하고, 서블릿으로 동작
    • DispatcherServlet FrameworkServlet HttpServletBean HttpServlet
  • 스프링 부트는 DispacherServlet 을 서블릿으로 자동으로 등록하면서 모든 경로( urlPatterns="/" )에 대해서 매핑

📌 더 자세한 경로가 우선순위가 높음. 그래서 기존에 등록한 서블릿도 함께 동작

# 요청 흐름

  • 서블릿이 호출되면 HttpServlet이 제공하는 serivce()가 호출
  • 스프링 MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service()를 오버라이드해둠
  • FrameworkServlet.service()를 시작으로 여러 메서드가 호출되면서 DispacherServlet.doDispatch()가 호출된다

# 동작 순서

  1. 핸들러 조회 : 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회
// 1. 핸들러 조회
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
  noHandlerFound(processedRequest, response);
  return;
}
  1. 핸들러 어댑터 조회 : 핸들러를 실행할 수 있는 핸들러 어댑터를 조회
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
  1. 핸들러 어댑터 실행 : 핸들러 어댑터 실행
  2. 핸들러 실행 : 핸들러 어댑터가 실제 핸들러를 실행
  3. ModelAndView 반환 : 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환
// 3. 핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 핸들러 실행 -> 5. ModelAndView 반환
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
	processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
  1. viewResolver 호출 : 뷰 리졸버를 찾고 실행
    • JSP의 경우: InternalResourceViewResolver가 자동 등록되고, 사용된다.
  2. View 반환 : 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환
    • JSP의 경우 InternalResourceView(JstlView)를 반환하는데, 내부에 forward() 로직이 있음
// 6. 뷰 리졸버를 통해서 뷰 찾기, 7. View 반환
view = resolveViewName(viewName, mv.getModelInternal(), locale, request); 
  1. 뷰 렌더링 : 뷰를 통해서 뷰 렌더링
// 8. 뷰 렌더링
view.render(mv.getModelInternal(), request, response);

# 주요 터페이스

스프링 MVC의 큰 강점은 DispatcherServlet 코드의 변경 없이, 원하는 기능을 변경하거나 확장할 수 있음

핸들러 매핑 : org.springframework.web.servlet.HandlerMapping
핸들러 어댑터 : org.springframework.web.servlet.HandlerAdapter
뷰 리졸버 : org.springframework.web.servlet.ViewResolver
뷰 : org.springframework.web.servlet.View

핸들러 매핑과 핸들러 어댑터

# Controller 인터페이스

📌 Controller 인터페이스는 @Controller 애노테이션과는 전혀 다르다

뷰 리졸버

다시 작성

스프링 MVC - 시작하기

# @RequestMapping

  • RequestMappingHandlerMapping
  • RequestMappingHandlerAdapter

# SpringMemberFormControllerV1 - 회원 등록 폼

@Controller
public class SpringMemberFormControllerV1 {
      @RequestMapping("/springmvc/v1/members/new-form")
      public ModelAndView process() {
   	return new ModelAndView("new-form");
  }
}
  • @Controller
    • 스프링이 자동으로 스프링 빈으로 등록 (내부에 @Component 애노테이션이 있어서 컴포넌트
      스캔의 대상이 됨)
    • 스프링 MVC에서 애노테이션 기반 컨트롤러로 인식
  • @RequestMapping
    • 요청 정보를 매핑
    • 해당 URL이 호출되면 이 메서드가 호출
    • 애노테이션을 기반으로 동작하기 때문에, 메서드의 이름은 임의로 지으면 됨
  • ModelAndView
    • 모델과 뷰 정보를 담아서 반환

스프링 MVC - 컨트롤러 통합

다시 작성

스프링 MVC - 실용적인 방식

다시 작성

섹션6. 스프링 MVC - 기본 기능

프로젝트 생성

Packaging는 : Jar 선택

📌 JSP를 사용하지 않기 때문에 Jar를 사용하는 것이 좋음

  • 스프링 부트를 사용하면 이 방식을 주로 사용
  • Jar를 사용하면 항상 내장 서버(톰캣 등)를 사용, 최적화 되어 있음
  • webapp 경로도 사용하지 않음
  • War를 사용하면 내장 서버도 사용가능 하지만, 주로 외부 서버에 배포 목적

Welcome 페이지

스프링 부트에 Jar를 사용하면 /resources/static/ 위치에 index.html 파일을 두면 Welcome 페이지로 처리해준다.
(스프링 부트가 지원하는 정적 컨텐츠 위치에 /index.html 이 있으면 된다

로깅 간단히 알아보기

  • 스프링 부트 라이브러리를 사용하면 스프링 부트 로깅 라이브러리spring-boot-starter-logging가 함께 포함
  • SLF4J 라이브러리 (인터페이스)
    • Logback 라이브러리(구현체)
    • Log4J 라이브러리
    • Log4J2 라이브러리

# 로그 선언

  • private Logger log = LoggerFactory.getLogger(getClass());
  • private static final Logger log = LoggerFactory.getLogger(Xxx.class)
  • @Slf4j : 롬복 사용 가능
 private final Logger log = LoggerFactory.getLogger(getClass());

# 로그 호출

//@Slf4j
@RestController
public class LogTestController {
   private final Logger log = LoggerFactory.getLogger(getClass());
   @RequestMapping("/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);
     //로그를 사용하지 않아도 a+b 계산 로직이 먼저 실행됨, 이런 방식으로 사용하면 X
     log.debug("String concat log=" + name);
     return "ok";
   }
}

# 매핑 정보

  • @Controller
    반환 값이 String이면 뷰 이름으로 인식, 뷰를 찾고 뷰가 랜더링 된다
  • @RestController
    반환 값으로 뷰를 찾는 것이 아니라, HTTP 메시지 바디에 바로 입력, 따라서 실행 결과로 ok 메세지를 받을 수 있다

# 테스트

  • 로그가 출력되는 포멧 확인
    • 시간, 로그 레벨, 프로세스 ID, 쓰레드 명, 클래스명, 로그 메시지
  • 로그 레벨
    • LEVEL: TRACE > DEBUG > INFO > WARN > ERROR
    • 개발 서버는 debug 출력
    • 운영 서버는 info 출력
  • @Slf4j 로 변경

# 로그 레벨 설정(application.properties)

  • 전체 로그 레벨 설정(기본 info) logging.level.root=info
  • 패키지와 그 하위 로그 레벨 설정 logging.level.hello.springmvc=debug

# 올바른 로그 사용법

  • log.debug("data="+data)
    • 로그 출력 레벨을 info로 설정해도 해당 코드에 있는 "data="+data가 실제 실행이 되어 버린다. 결과적으로 문자 더하기 연산이 발생한다
  • log.debug("data={}", data)
    • 로그 출력 레벨을 info로 설정하면 아무일도 발생하지 않음. 앞과 같은 의미없는 연산이발생하지 않는다.

# 로그 사용시 장점

  • 쓰레드 정보, 클래스 이름 같은 부가 정보를 함께 볼 수 있고, 출력 모양을 조정할 수 있음
  • 로그 레벨에 따라 개발/운영서버 각각 출력하고 싶은 로그만 출력 수 있음
  • 시스템 아웃 콘솔에만 출력하는 것이 아니라, 파일이나 네트워크 등, 로그를 별도의 위치에 남길 수 있음
  • 파일로 남길 때는 일별, 특정 용량에 따라 로그를 분할하는 것도 가능
  • 성능(내부 버퍼링, 멀티 쓰레드 등등)도 일반 System.out보다 좋음

요청 매핑

# HTTP 메서드

@RequestMapping 에 method 속성으로 HTTP 메서드를 지정하지 않으면 HTTP 메서드와 무관하게 호출
모두 허용 GET, HEAD, POST, PUT, PATCH, DELETE

# HTTP 메서드 매핑

Get만 허용
(만약 POST 요청을 하면 스프링 MVC는 HTTP 405 상태코드(Method Not Allowed)를 반환)

@RequestMapping(value = "/mapping-get-v1", method = RequestMethod.GET)
public String mappingGetV1() {
   log.info("mappingGetV1");
   return "ok";
}

# HTTP 메서드 매핑 축약

@GetMapping(value = "/mapping-get-v2")
public String mappingGetV2() {
 log.info("mapping-get-v2");
 return "ok";
}

# PathVariable(경로 변수) 사용

@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable("userId") String data) {
   log.info("mappingPath userId={}", data);
   return "ok";
}

#변수명이 같으면 생략 가능
ex)@PathVariable("userId") String userId -> @PathVariable userId

@GetMapping("/mapping/{userId}")
public String mappingPath(@PathVariable userId ) {
   log.info("mappingPath userId={}", userId);
   return "ok";
}

# PathVariable(경로 변수) 다중 사용

@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";
}

#특정 헤더 조건 매핑

파라미터 매핑과 비슷하지만, HTTP 헤더를 사용

/**
 * 특정 헤더로 추가 매핑
 * 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";
}

# 미디어 타입 조건 매핑 - HTTP 요청 Content-Type, consume

HTTP 요청의 Content-Type 헤더를 기반으로 미디어 타입으로 매핑
만약 맞지 않으면 HTTP 415 상태코드(Unsupported Media Type)을 반환한다

/**
 * 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 요청 Accept, produce

HTTP 요청의 Accept 헤더를 기반으로 미디어 타입으로 매핑
만약 맞지 않으면 HTTP 406 상태코드(Not Acceptable)을 반환한다

/**
 * 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";
}

요청 매핑 - API 예시

• 회원 관리 API
• 회원 목록 조회: GET /users
• 회원 등록 : POST /users
• 회원 조회 : GET /users/{userId}
• 회원 수정 : PATCH /users/{userId}
• 회원 삭제 : DELETE /users/{userId}

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 ){
  • HttpServletRequest

  • HttpServletResponse

  • HttpMethod

    • HTTP 메서드를 조회 org.springframework.http.HttpMethod
      -Locale
    • Locale 정보를 조회
  • @RequestHeader MultiValueMap<String, String> headerMap

    • 모든 HTTP 헤더를 MultiValueMap 형식으로 조회

    • 📌 MultiValueMap은 하나의 키에 대해 여러 개의 값을 가질 수 있기 때문에, 다중 값 매핑을 표현하기에 적합

  • @RequestHeader("host") String host

    • 특정 HTTP 헤더를 조회
    • 속성(필수 값 여부: required, 기본 값 속성: defaultValue)
  • @CookieValue(value = "myCookie", required = false) String cookie

    • 특정 쿠키를 조회
    • 속성 (필수 값 여부: required, 기본 값: defaultValue)

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

HTTP 요청 파라미터 - @RequestParam

# requestParamV2

  • @RequestParam : 파라미터 이름으로 바인딩
    • @RequestParam("username") String memberName
    • → request.getParameter("username"
  • @ResponseBody : View 조회를 무시하고, HTTP message body에 직접 해당 내용 입력
@ResponseBody
@RequestMapping("/request-param-v2")
public String requestParamV2(
     @RequestParam("username") String memberName,
     @RequestParam("age") int memberAge) {
   log.info("username={}, age={}", memberName, memberAge);
   return "ok";
}

# requestParamV3

HTTP 파라미터 이름이 변수 이름과 같으면 @RequestParam(name="xx") 생략 가능

@ResponseBody
@RequestMapping("/request-param-v3")
public String requestParamV3(
     @RequestParam String username,
     @RequestParam int age) {
   log.info("username={}, age={}", username, age);
   return "ok";
}

# requestParamV4

  • String , int , Integer 등의 단순 타입이면 @RequestParam도 생략 가능
  • @RequestParam이 있으면 명확하게 요청 파리 미터에서 데이터를 읽는다는 것을 알 수 있기 때문에 사용하는 것을 권장
@ResponseBody
@RequestMapping("/request-param-v4")
public String requestParamV4(String username, int age) {
   log.info("username={}, age={}", username, age);
   return "ok";
}

# 파라미터 필수 여부 - requestParamRequired

  • @RequestParam.required
    • 파라미터 필수 여부
    • 기본값이 파라미터 필수( true )이다.
  • /request-param 요청
    • username 이 없으므로 400 예외가 발생한다
  • 📌주의! - 파라미터 이름만 사용
    /request-param?username=
    • 파라미터 이름만 있고 값이 없는 경우 → 빈문자로 통과
  • 📌주의!-기본형(primitive)에 null 입력
    @RequestParam(required = false) int age
    • null 을 int 에 입력하는 것은 불가능(500 예외 발생)
    • 따라서 null 을 받을 수 있는 Integer 로 변경하거나, 또는 다음에 나오는 defaultValue 사용
@ResponseBody
@RequestMapping("/request-param-required")
public String requestParamRequired(
     @RequestParam(required = true) String username,
     @RequestParam(required = false) Integer age) {
   log.info("username={}, age={}", username, age);
   return "ok";
}

# 기본 값 적용 - requestParamDefault

  • 파라미터에 값이 없는 경우 defaultValue 를 사용하면 기본 값을 적용 가능
  • 이미 기본 값이 있기 때문에 required 는 의미가 없음
  • defaultValue 는 빈 문자의 경우에도 설정한 기본 값 적용
@ResponseBody
@RequestMapping("/request-param-default")
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";
}

# 파라미터를 Map으로 조회하기 - requestParamMap

  • 파라미터를 Map, MultiValueMap으로 조회할 수 있다
  • 파라미터의 값이 1개가 확실하다면 Map 사용, 그렇지 않다면 MultiValueMap 사용 권장
@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";
}

HTTP 요청 파라미터 - @ModelAttribute

  • 요청 파라미터를 받아서 필요한 객체를 만들고 그 객체에 값을 넣어주어야 함
  • @ModelAttribute 사용하면, HelloData 객체가 생성되고, 요청 파라미터의 값도 모두 들어가 있음
@ResponseBody
@RequestMapping("/model-attribute-v1")
public String modelAttributeV1(@ModelAttribute HelloData helloData) {
	 log.info("username={}, age={}", helloData.getUsername(),
helloData.getAge());
	 return "ok";
}
  • 스프링MVC는 @ModelAttribute 가 있으면 다음을 실행

    • 해당 객체를 생성한다.
    • 요청 파라미터의 이름으로해당 객체의 프로퍼티를 찾는다
    • 그리고 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩) 한다
  • 프로퍼티

    • 객체에 getxxx() , setxxx() 메서드가 있으면, 이 객체는 xxx 이라는 프로퍼티를 가지고 있다.
    • xxx 프로퍼티의 값을 변경하면 setxxx()이 호출되고, 조회하면 getx()이 호출된다
  • 바인딩 오류

    • age=abc 처럼 숫자가 들어가야 할 곳에 문자를 넣으면 BindException 이 발생

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

# HTTP message body

  • 요청 파라미터와 다르게, HTTP 메시지 바디를 통해 데이터가 직접 넘어오는 경우는 @RequestParam, @ModelAttribute를 사용할 수 없음 (HTML Form 형식으로 전달되는 경우는 요청 파라미터로 인정)

# requestBodyStringV1

   @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");
 }

# requestBodyStringV2- Input, Output 스트림, Reader

  • InputStream(Reader): HTTP 요청 메시지 바디의 내용을 직접 조회
  • OutputStream(Writer): HTTP 응답 메시지의 바디에 직접 결과 출력
@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");
}

# requestBodyStringV3 - HttpEntity

  • HttpEntity: HTTP header, body 정보를 편리하게 조회
    • 메시지 바디 정보를 직접 조회
    • 요청 파라미터를 조회하는 기능과 관계 없음 @RequestParam X, @ModelAttribute X
  • HttpEntity는 응답에도 사용 가능
    • 메시지 바디 정보 직접 반환
    • 헤더 정보 포함 가능
    • view 조회X
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) {
   String messageBody = httpEntity.getBody();
   log.info("messageBody={}", messageBody);
  
  return new HttpEntity<>("ok");
}
  • RequestEntity
    • HttpMethod, url 정보가 추가, 요청에서 사용
  • ResponseEntity
    • HTTP 상태 코드 설정 가능, 응답에서 사용
    • return new ResponseEntity("Hello World", responseHeaders, HttpStatus.CREATED)

# @RequestBody - requestBodyStringV4

  • @RequestBody
    • HTTP 메시지 바디 정보를 편리하게 조회 가능
    • 헤더 정보가 필요하다면 HttpEntity를 사용하거나 @RequestHeader를 사용
    • @RequestParam, @ModelAttribute와 전혀 관계가 없음
  • @ResponseBody
    • 응답 결과를 HTTP 메시지 바디에 직접 담아서 전달
    • 해당 경우에도 view 사용X
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) {
   log.info("messageBody={}", messageBody);
   return "ok";
}

# 요청 파라미터 vs HTTP 메시지 바디

  • 요청 파라미터를 조회하는 기능: @RequestParam , @ModelAttribute
  • HTTP 메시지 바디를 직접 조회하는 기능: @RequestBody

HTTP 요청 메시지 - JSON

  • HttpServletRequest를 사용해서 직접 HTTP 메시지 바디에서 데이터를 읽어와서, 문자로 변환
  • 문자로 된 JSON데이터를 Jackson 라이브러리인 objectMapper를 사용, 자바 객체로 변환

# requestBodyJsonV1

@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 data = objectMapper.readValue(messageBody, HelloData.class);
   log.info("username={}, age={}", data.getUsername(), data.getAge());
 
   response.getWriter().write("ok");
 }  

# requestBodyJsonV2 - @RequestBody 문자 변환

  • @RequestBody를 사용해서 HTTP 메시지에서 데이터를 꺼내고 messageBody에
    저장
  • 문자로 된 JSON 데이터인 messageBody를 objectMapper를 통해 자바 객체로 변환
@ResponseBody
@PostMapping("/request-body-json-v2")
public String requestBodyJsonV2(@RequestBody String messageBody) throws
IOException {
 HelloData data = objectMapper.readValue(messageBody, HelloData.class);
 log.info("username={}, age={}", data.getUsername(), data.getAge());
 return "ok";
}

# requestBodyJsonV3 - @RequestBody 객체 변환

  • @RequestBody 에 직접 만든 객체를 지정
    • HttpEntity , @RequestBody를 사용하면 HTTP 메시지 컨버터가 HTTP 메시지 바디의 내용을 우리가 원하는 문자나 객체(JSON 객체) 등으로 변환
  • @RequestBody는 생략 불가능
    • @RequestBody를 생략하면 @ModelAttribute 적용됨(HTTP 메시지 바디가 아니라 요청 파라미터를 처리하게 됨)
@ResponseBody
@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData data) {
   log.info("username={}, age={}", data.getUsername(), data.getAge());
   return "ok";
}

# requestBodyJsonV4 - HttpEntity

  • HttpEntity 사용가능
@ResponseBody
@PostMapping("/request-body-json-v4")
public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity) {
   HelloData data = httpEntity.getBody();
   log.info("username={}, age={}", data.getUsername(), data.getAge());
   return "ok";
}

# requestBodyJsonV5

  • @ResponseBody
    • 응답의 경우에도 @ResponseBody를 사용하면 해당 객체를 HTTP 메시지 바디에 직접 넣어줄 수 있음(물론 HttpEntity 사용가능)
  • @RequestBody 요청
    • JSON 요청 HTTP 메시지 컨버터 객체
  • @ResponseBody 응답
    • 객체 HTTP 메시지 컨버터 JSON 응답
@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
   log.info("username={}, age={}", data.getUsername(), data.getAge());
   return data;
}

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

스프링(서버)에서 응답 데이터를 만드는 방법은 크게 3가지

  • 정적 리소스
    • 예) 웹 브라우저에 정적인 HTML, css, js를 제공할 때는, 정적 리소스를 사용한다.
  • 뷰 템플릿 사용
    • 예) 웹 브라우저에 동적인 HTML을 제공할 때는 뷰 템플릿을 사용한다.
  • HTTP 메시지 사용
    • HTTP API를 제공하는 경우에는 HTML이 아니라 데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보낸다.

정적 리소스

  • 스프링 부트는 클래스패스의 다음 디렉토리에 있는 정적 리소스를 제공
    • /static,/public,/resources,/META-INF/resources
  • src/main/resources는 리소스를 보관하는 곳이면서, 클래스패스의 시작 경로
  • 스프링 부트가 정적 리소스로 서비스를 제공한다

뷰 템플릿

  • 뷰 템플릿을 거쳐서 HTML이 생성, 뷰가 응답을 만들어서 전달
  • 일반적으로 HTML을 동적으로 생성하는 용도로 사용
  • 스프링 부트는 기본 뷰 템플릿 경로를 제공
    • src/main/resources/templates
  • 뷰 템플릿 생성
    • src/main/resources/templates/response/hello.html

# responseViewV1

  • ModelAndView
 @RequestMapping("/response-view-v1")
 public ModelAndView responseViewV1() {
 	ModelAndView mav = new ModelAndView("response/hello")
  						.addObject("data", "hello!");
	
  	return mav;
 } 

# responseViewV2


String을 반환하는 경우 - View or HTTP 메시지

  • @ResponseBody가 없으면 response/hello로 뷰 리졸버가 실행되어서 뷰를 찾고, 렌더링 한다
  • @ResponseBody 가 있으면 뷰 리졸버를 실행하지 않고, HTTP 메시지 바디에 직접 response/hello라는 문자 입력
    • 뷰의 논리 이름인 response/hello 를 반환하면 다음 경로의 뷰 템플릿이 렌더링 되는 것을 확인할수 있음
    • 실행: templates/response/hello.html
 @RequestMapping("/response-view-v2")
 public String responseViewV2(Model model) {
 	model.addAttribute("data", "hello!!");
 	return "response/hello";
 }

# responseViewV3

Void를 반환하는 경우

  • @Controller를 사용하고, HttpServletResponse , OutputStream(Writer)같은 HTTP 메시지 바디를 처리하는 파라미터가 없으면 요청 URL을 참고해서 논리 뷰 이름으로 사용
    • 요청 URL: /response/hello
    • 실행: templates/response/hello.html
    @RequestMapping("/response/hello")
    public void responseViewV3(Model model) {
    		 model.addAttribute("data", "hello!!");
    }
    📌 참고 이 방식은 명시성이 너무 떨어지고, 딱 맞는 경우도 많이 없어서, 권장하지 않음

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

# responseBodyStringV1

  • (서블릿을 직접 다룰 때 처럼) HttpServletResponse 객체를 통해서 HTTP 메시지 바디에 직접 ok 응답 메시지를 전달
  • response.getWriter().write("ok")
@GetMapping("/response-body-string-v1")
public void responseBodyV1(HttpServletResponse response) throws IOException {
	response.getWriter().write("ok");
 }  

# responseBodyStringV2

  • ResponseEntity엔티티는 HttpEntity를 상속 받음
  • HttpEntity는 HTTP 메시지의 헤더, 바디정보를 가지고 있음
    • ResponseEntity는 여기에 더해서 HTTP 응답 코드를 설정가능
@GetMapping("/response-body-string-v2")
public ResponseEntity<String> responseBodyV2() {
	return new ResponseEntity<>("ok", HttpStatus.OK);
 }  

# responseBodyStringV3

  • @ResponseBody 를 사용하면 view를 사용하지 않고, HTTP 메시지 컨버터를 통해서 HTTP 메시지를 직접 입력가능
    • ResponseEntity 도 동일한 방식으로 동작
 @ResponseBody
 @GetMapping("/response-body-string-v3")
 public String responseBodyV3() {
 	return "ok";
 } 

# responseBodyJsonV1

  • ResponseEntity를 반환
  • HTTP 메시지 컨버터를 통해서 JSON 형식으로 변환되어서 반환
@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);
 }  

# responseBodyJsonV2

  • ResponseEntity는 HTTP 응답 코드를 설정할 수 있는데, @ResponseBody를 사용하면 이런 것을 설정하기 까다로움
    • @ResponseStatus(HttpStatus.OK) 애노테이션을 사용하면 응답 코드도 설정 가능
    • 애노테이션이기 때문에 응답 코드를 동적으로 변경 불가능(동적으로 변경하려면 ResponseEntity 사용)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
@GetMapping("/response-body-json-v2")
 public HelloData responseBodyJsonV2() {
    HelloData helloData = new HelloData();
    helloData.setUsername("userA");
    helloData.setAge(20);
    return helloData;
 }  

# @RestController

  • @Controller대신 @RestController 애노테이션을 사용하면, 해당 컨트롤러에 모두 @ResponseBody가 적용되는 효과있음
  • 따라서 뷰 템플릿을 사용하는 것이 아니라, HTTP 메시지 바디에 직접 데이터 입력
  • Rest API(HTTP API)를 만들 때 사용하는 컨트롤러

📌 참고 @ResponseBody는 클래스 레벨에 두면 전체 메서드에 적용되는데, @RestController에노테이션 안에 @ResponseBody가 적용되어 있음

HTTP 메시지 컨버터

@ResponseBody 사용 원리

  • HTTP의 BODY에 문자 내용을 직접 반환
  • viewResolver 대신에 HttpMessageConverter 동작
  • 기본 문자처리: StringHttpMessageConverter
  • 기본 객체처리: MappingJackson2HttpMessageConverter

스프링 MVC는 다음의 경우에 HTTP 메시지 컨버터를 적용

  • HTTP 요청: @RequestBody , HttpEntity(RequestEntity)
  • HTTP 응답: @ResponseBody , HttpEntity(ResponseEntity)
  • canRead(), canWrite() : 메시지 컨버터가 해당 클래스, 미디어타입을 지원하는지 체크
  • read(), write() : 메시지 컨버터를 통해서 메시지를 읽고 쓰는 기능

스프링 부트 주요 메시지 컨버터

0 = ByteArrayHttpMessageConverter

데이터 처리 : byte[]
클래스 타입 : byte[]
미디어타입 : */*(아무거나)
요청 예) @RequestBody byte[] data
응답 예) @ResponseBody return byte[]
쓰기 미디어타입 : application/octet-stream

1 = StringHttpMessageConverter

데이터 처리 : String 문자
클래스 타입 : String
미디어타입 : */*(아무거나)
요청 예) @RequestBody String data
응답 예) @ResponseBody return "ok"
쓰기 미디어타입 : text/plain

2 = MappingJackson2HttpMessageConverter

데이터 처리 : application/json
클래스 타입 : 객체 또는 HashMap
미디어타입 : application/json 관련
요청 예) ) @RequestBody HelloData data
응답 예) @ResponseBody return helloData
쓰기 미디어타입 : application/json 관련

# HTTP 요청 데이터 읽기

  • HTTP 요청이 오고, 컨트롤러에서 @RequestBody, HttpEntity 파라미터 사용
  • 메시지 컨버터가 메시지를 읽을 수 있는지 확인하기 위해 canRead()를 호출
    • 대상 클래스 타입을 지원하는가
      • 예) @RequestBody의 대상 클래스 (byte[], String, HelloData)
    • HTTP 요청의 Content-Type 미디어 타입을 지원하는가
      • 예) text/plain, application/json, */*
    • canRead() 조건을 만족하면 read()를 호출해서 객체 생성하고, 반환

# HTTP 응답 데이터 생성

  • 컨트롤러에서 @ResponseBody, HttpEntity로 값이 반환
  • 메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해 canWrite()를 호출
    • 대상 클래스 타입을 지원하는가.
      • 예) return의 대상 클래스 ( byte[] , String , HelloData )
    • HTTP 요청의 Accept 미디어 타입을 지원하는가(더 정확히는 @RequestMapping의 produces)
      • 예) text/plain , application/json , */*
  • canWrite() 조건을 만족하면 write()를 호출해서 HTTP 응답 메시지 바디에 데이터 생성

요청 매핑 헨들러 어뎁터 구조

# RequestMappingHandlerAdapter(요청 매핑 핸들 어댑터)동작 방식

ArgumentResolver

  • 애노테이션 기반의 컨트롤러는 매우 다양한 파라미터를 사용할 수 있다
    이렇게 파라미터를 유연하게 처리할 수 있는 이유는 ArgumentResolver 덕분이다
    • ArgumentResolver 를 호출해서 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터의 값(객체)을 생성

HandlerMethodArgumentResolver(줄여서 ArgumentResolver)

  • ArgumentResolver의 supportsParameter()를 호출해서 해당 파라미터를 지원하는지 체크
  • 지원하면 resolveArgument()를 호출해서 실제 객체를 생성
  • 생성된 객체가 컨트롤러 호출시 넘어감

HandlerMethodReturnValueHandler(줄여서 ReturnValueHandler)

  • 응답 값을 변환하고 처리
  • 컨트롤러에서 String으로 뷰 이름을 반환해도, 동작하는 이유

📌참고 가능한 응답 값 목록은 다음 공식 메뉴얼에서 확인 가능
https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-annreturn-types

HTTP 메시지 컨버터 위치

HTTP 메시지 컨버터를 사용하는 @RequestBody 도 컨트롤러가 필요로 하는 파라미터의 값에 사용되고, @ResponseBody의 경우도 컨트롤러의 반환 값을 이용한다

요청의 경우 :
• @RequestBody를 처리하는 ArgumentResolver가 있고, HttpEntity를 처리하는 ArgumentResolver가 있음
• ArgumentResolver들이 HTTP 메시지 컨버터를 사용해서 필요한객체를 생성

응답의 경우:
• @ResponseBody와 HttpEntity를 처리하는 ReturnValueHandler가 있다
• 여기에서 HTTP 메시지 컨버터를 호출해서 응답 결과를 만듬

스프링 MVC
• @RequestBody @ResponseBody가 있으면 RequestResponseBodyMethodProcessor(ArgumentResolver) 사용
• HttpEntity가 있으면 HttpEntityMethodProcessor(ArgumentResolver) 사용

확장
스프링은 다음을 모두 인터페이스로 제공, 필요하면 언제든지 기능을 확장할 수 있다

  • HandlerMethodArgumentResolver
  • HandlerMethodReturnValueHandler
  • HttpMessageConverter

섹션7. 스프링 MVC - 웹 페이지 만들기

프로젝트 생성

Packaging는 Jar를 선택

# Welcome 페이지 추가

  • /resources/static/위치에 index.html 파일을 둠
  • 스프링 부트가 Welcome 페이지로 처리해줌

요구사항 분석

상품 도메인 모델
상품 ID
상품명
가격
수량

상품 관리 기능
상품 목록
상품 상세
상품 등록
상품 수정

상품 도메인 개발

# Item - 상품 객체

 private Long id;
 private String itemName;
 private Integer price; // 값이 null이 들어올수 있기때문에 Integer사용(Int는 null 허용X)
 private Integer quantity;

# ItemRepository - 상품 저장소

public Item save(Item item) {
     item.setId(++sequence);
     store.put(item.getId(), item);
     return item;
 }
  
 public Item findById(Long id) {
	 return store.get(id);
 }
  
 public List<Item> findAll() {
	 return new ArrayList<>(store.values());
 }
  
 public void update(Long itemId, Item updateParam) {
	 Item findItem = findById(itemId);
	 findItem.setItemName(updateParam.getItemName());
	 findItem.setPrice(updateParam.getPrice());
 	findItem.setQuantity(updateParam.getQuantity());
 }  

# ItemRepositoryTest - 상품 저장소 테스트

 @AfterEach // test 끝날때마다 초기화 실행
 void afterEach() {
 	itemRepository.clearStore();
 }   
 @Test
 void save() {
   //given
   Item item = new Item("itemA", 10000, 10);
  
   //when
   Item savedItem = itemRepository.save(item);
  
   //then
   Item findItem = itemRepository.findById(item.getId());
   assertThat(findItem).isEqualTo(savedItem);
 }
  
 @Test
 void findAll() {
   //given
   Item item1 = new Item("item1", 10000, 10);
   Item item2 = new Item("item2", 20000, 20);
   itemRepository.save(item1);
   itemRepository.save(item2);
  
  //when
   List<Item> result = itemRepository.findAll();
  
  //then
   assertThat(result.size()).isEqualTo(2);
   assertThat(result).contains(item1, item2);
 }
  
 @Test
 void updateItem() {
   //given
   Item item = new Item("item1", 10000, 10);
   Item savedItem = itemRepository.save(item);
   Long itemId = savedItem.getId();
  
  //when
   Item updateParam = new Item("item2", 20000, 30);
   itemRepository.update(itemId, updateParam);
   Item findItem = itemRepository.findById(itemId);
  
  //then
	assertThat(findItem.getItemName()).isEqualTo(updateParam.getItemName());
	assertThat(findItem.getPrice()).isEqualTo(updateParam.getPrice());
	assertThat(findItem.getQuantity()).isEqualTo(updateParam.getQuantity());
 }
}

상품 서비스 HTML

#부트스트랩

부트스트랩 공식 사이트: https://getbootstrap.com

  • resources/static/css/bootstrap.min.css 추가
  • /resources/static 에 넣어두었기 때문에 스프링 부트가 정적 리소스를 제공

📌참고
/resources/static 폴더에 HTML을 넣어두면, 실제 서비스에서도 공개됨
서비스를 운영한다면 지금처럼 공개할 필요없는 HTML을 두는 것은 주의 필요

  • 상품 목록 : /resources/static/html/items.html
  • 삼품 상세 : /resources/static/html/item.html
  • 상품 등록 폼 : /resources/static/html/addForm.html
  • 상품 수정 폼 : /resources/static/html/editForm.html

상품 목록 - 타임리프

# BasicItemController

  • 컨트롤러 로직은 itemRepository에서 모든 상품을 조회한 다음에 모델에 담고 뷰 템플릿 호출
  • @RequiredArgsConstructor
    • final이 붙은 멤버변수만 사용해서 생성자를 자동으로 만들어준다
 public BasicItemController(ItemRepository itemRepository) {
	 this.itemRepository = itemRepository;
} 
  • 이렇게 생성자가 딱 1개만 있으면 스프링이 해당 생성자에 @Autowired로 의존관계 주입
  • 따라서 final 키워드를 빼면 안됨 (의존관계 주입이 안됨)

# 테스트용 데이터 추가

@PostConstruct : 해당 빈의 의존관계가 모두 주입되고 나면 초기화 용도로 호출

 @PostConstruct
 public void init() {
   itemRepository.save(new Item("testA", 10000, 10));
   itemRepository.save(new Item("testB", 20000, 20));
 }

# 타임리프 사용 선언

  • th 사용 <html xmlns:th="http://www.thymeleaf.org">

# 속성 변경 - th:href

th:href="@{/css/bootstrap.min.css}"

  • href="value1" 을 th:href="value2" 의 값으로 변경
  • 타임리프 뷰 템플릿을 거치게 되면 원래 값을 th:xxx 값으로 변경한다(만약 값이 없다면 새로 생성)
  • HTML을 그대로 볼 때는 href 속성이 사용되고, 뷰 템플릿을 거치면 th:href 의 값이 href 로 대체되면서 동적으로 변경

# 타임리프 핵심

  • th:xxx 가 붙은 부분은 서버사이드에서 렌더링 되고, 기존 것을 대체
  • th:xxx 이 없으면 기존 html의 xxx 속성이 그대로 사용
  • HTML을 파일로 직접 열었을 때, th:xxx가 있어도 웹 브라우저는 th: 속성을 알지 못하므로 무시한다
  • HTML을 파일 보기를 유지하면서 템플릿 기능도 할 수 있음

# URL 링크 표현식 - @{...}

  • th:href="@{/css/bootstrap.min.css}"
  • @{...} : 타임리프의 URL 링크 표현식

# [타임리프] 속성 변경 - th:onclick

  • onclick="location.href='addForm.html'"
  • th:onclick="|location.href='@{/basic/items/add}'|"

# 리터럴 대체 - |...|

타임리프에서 문자와 표현식 등은 분리되어 있기 때문에 더해서 사용해야하지만, 리터럴 대체 문법을 사용하면 더하기 없이 편리하게 사용할 수 있다

  • 결과 : location.href='/basic/items/add'
  • 더하기 사용 : th:onclick="'location.href=' + '\'' + @{/basic/items/add} + '\''"
  • 리터럴 대체 문법사용 : th:onclick="|location.href='@{/basic/items/add}'|"

# [타임리프] 반복 출력 - th:each

  • <tr th:each="item : ${items}">
  • 반복은 th:each를 사용하여 모델에 포함된 items 컬렉션 데이터가 item 변수에 하나씩 포함되고, 반복문 안에서 item 변수를 사용할 수 있음
  • 컬렉션의 수 만큼 ..이 하위 테그를 포함해서 생성

# [타임리프] 변수 표현식 - ${...}

  • <td th:text="${item.price}">10000</td>
  • 모델에 포함된 값이나, 타임리프 변수로 선언한 값을 조회할 수 있음
  • 프로퍼티 접근법을 사용한다. (item.getPrice())

# 내용 변경 - th:text

-<td th:text="${item.price}">10000</td>

  • 내용의 값을 th:text 의 값으로 변경
    • 여기서는 10000을 ${item.price}의 값으로 변경

# URL 링크 표현식2 - @{...}

  • 상품 ID를 선택하는 링크
    • th:href="@{/basic/items/{itemId}(itemId=${item.id})}"
  • 경로 변수({itemId}) 뿐만 아니라 쿼리 파라미터도 생성가능

# URL 링크 간단히

  • 리터럴 대체 문법을 활용해서 간단히 사용할 수 있음
  • th:href="@{|/basic/items/${item.id}|}"

📌참고
타임리프는 순수 HTML 파일을 웹 브라우저에서 열어도 내용을 확인할 수 있고, 서버를 통해 뷰 템플릿을거치면 동적으로 변경된 결과를 확인할 수 있음
순수 HTML을 그대로 유지하면서 뷰 템플릿도 사용할 수 있는 타임리프의 특징을 네츄럴 템플릿(natural templates)이라 한다.

상품 상세

# 상품 상세 컨트롤

PathVariable 로 넘어온 상품ID로 상품을 조회하고, 모델에 담고 뷰 템플릿을 호출

@GetMapping("/{itemId}")
public String item(@PathVariable Long itemId, Model model) {
   Item item = itemRepository.findById(itemId);
   model.addAttribute("item", item);
   return "basic/item";
}  

상품 등록 폼

단순히 뷰 템플릿만 호출

@GetMapping("/add")
public String addForm() {
   return "basic/addForm";
}

# 속성 변경 - th:action

상품 등록 폼의 URL과 실제 상품 등록을 처리하는 URL을 똑같이 맞추고, HTTP 메서드로 두 기능을 구분

상품 등록 폼 : GET /basic/items/add
상품 등록 처리 : POST /basic/items/add

상품 등록 처리 - @ModelAttribute

# POST - HTML Form

  • content-type: application/x-www-form-urlencoded
  • 메시지 바디에 쿼리 파리미터 형식으로 전달 itemName=itemA&price=10000&quantity=10
    • 예) 회원 가입, 상품 주문, HTML Form 사용

# addItemV1 -@RequestParam

  • 요청 파라미터 형식을 처리해야 하므로 @RequestParam을 사용
  • itemName 요청 파라미터 데이터를 해당 변수에 받는다
  • Item 객체를 생성하고 itemRepository를 통해서 저장
  • 저장된 item을 모델에 담아서 뷰에 전달
@PostMapping("/add")
public String addItemV1(@RequestParam String itemName,
                        @RequestParam int price,
                        @RequestParam Integer quantity,
                        Model model) {
  
   Item item = new Item();
   item.setItemName(itemName);
   item.setPrice(price);
   item.setQuantity(quantity);
  
   itemRepository.save(item);
// model.addAttribute("item", item);
   
   return "basic/item";
} 

# addItemV2 -상품 등록 처리 -@ModelAttribute

  • 기존 @RequestParam으로 변수를 하나하나 받아서 Item을 생성하는 과정이 불편, @ModelAttribute를 사용해서 한번에 처리 가능
  1. @ModelAttribute - 요청 파라미터 처리
    • @ModelAttribute는 Item객체를 생성하고, 요청 파라미터의 값을 프로퍼티 접근법(setXxx)으로 입력
  2. @ModelAttribute - Model 추가
    • 모델(Model)에 @ModelAttribute로 지정한 객체를 자동으로 넣어 줌
    • model.addAttribute(...)를 주석처리 되어도 동작 가능
@PostMapping("/add")
public String addItemV2(@ModelAttribute("item") Item item, Model model) {
   
  itemRepository.save(item);
  model.addAttribute("item", item); //자동 추가, 생략 가능
  
  return "basic/item";
} 

# addItemV3 -상품 등록 처리 -ModelAttribute 이름 생략

  • @ModelAttribute 의 이름을 생략 가능
  • 생략시 모델에 저장될 때 클래스명을 사용하는데 클래스의 첫글자만 소문자로 변경해서 등록한다
    • 예)Item item
@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item) {
  itemRepository.save(item);

  return "basic/item";
}

# addItemV4 -상품 등록 처리 -ModelAttribute 전체 생략

  • @ModelAttribute 자체도 생략 가능
  • 대상 객체는 모델에 자동 등록
@PostMapping("/add")
public String addItemV4(Item item) {
  itemRepository.save(item);

  return "basic/item";
}  

상품 수정

  • 수정에 필요한 정보를 조회하고, 수정용 폼 뷰를 호출
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
  Item item = itemRepository.findById(itemId);
  model.addAttribute("item", item);
  
  return "basic/editForm";
}  
  

# 상품 수정 개발

상품 등록 폼의 URL과 실제 상품 등록을 처리하는 URL을 똑같이 맞추고, HTTP 메서드로 두 기능을 구분

  • GET /items/{itemId}/edit : 상품 수정 폼
  • POST /items/{itemId}/edit : 상품 수정 처리
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @ModelAttribute Item item) {
  itemRepository.update(itemId, item);
  
  return "redirect:/basic/items/{itemId}";
}  

#리다이렉트

  • 마지막에 뷰 템플릿을 호출하는 대신에 상품 상세 화면으로 이동하도록 리다이렉트 호출
  • 스프링은 redirect:/... 으로 편리하게 리다이렉트를 지원
  • redirect:/basic/items/{itemId}
    • 컨트롤러에 매핑된 @PathVariable의 값은 redirect 에도 사용 할 수 있음

📌참고
• HTML Form 전송은 PUT, PATCH를 지원하지 않음X
• GET, POST만 사용 가능
• PUT, PATCH는 HTTP API 전송시에 사용

PRG Post/Redirect/Get

# 상품 중복 등록

# POST 등록 후 새로 고침

  • 웹 브라우저의 새로 고침은 마지막에 서버에 전송한 데이터를 다시 전송

  • 상품 등록 폼에서 데이터를 입력하고 저장을 선택하면 POST /add + 상품 데이터를 서버로 전송, 이 상태에서 새로 고침을 또 선택하면 마지막에 전송한 POST /add + 상품 데이터 서버로 전송한다

    # POST, Redirect GET

  • 상품 저장 후에 뷰 템플릿으로 이동하는 것이 아니라, 상품 상세 화면으로 리다이렉트를 호출

  • 웹 브라우저는 리다이렉트의 영향으로 상품 저장 후에 실제 상품 상세 화면으로 다시 이동

  • 마지막에 호출한 내용이 상품 상세 화면인 GET /items/{id}가 호출 됨

  • 이런 문제 해결 방식을 PRG Post/Redirect/Get라 한다

  • 📌아래처럼 URL에 변수를 더해서 사용하는 것은 URL 인코딩이 안되기 때문에 위험하므로 RedirectAttributes를 사용해야한다

/**
 * PRG - Post/Redirect/Get
 */
@PostMapping("/add")
public String addItemV5(Item item) {
  itemRepository.save(item);
 
  return "redirect:/basic/items/" + item.getId();
}  

RedirectAttributes

  • 리다이렉트 할 때 간단히 status=true를 추가하여, 뷰 템플릿에서 이 값이 있으면 메시지를 출력
  • RedirectAttributes를 사용하면 URL 인코딩도 해주고, pathVarible, 쿼리 파라미터까지 처리한다
  • redirect:/basic/items/{itemId}
    • pathVariable 바인딩: {itemId}
    • 나머지는 쿼리 파라미터로 처리: ?status=true
@PostMapping("/add")
public String addItemV6(Item item, RedirectAttributes redirectAttributes) {
 
  Item savedItem = itemRepository.save(item);
  redirectAttributes.addAttribute("itemId", savedItem.getId());
  redirectAttributes.addAttribute("status", true);
  

  return "redirect:/basic/items/{itemId}";
}
   

# 뷰 템플릿 메시지 추가

<h2 th:if="${param.status}" th:text="'저장 완료!'"></h2>

  • th:if : 해당 조건이 참이면 실행(status=true)
  • ${param.status} : 타임리프에서 쿼리 파라미터를 편리하게 조회하는 기능
    • 원래는 컨트롤러에서 모델에 직접 담고 값을 꺼내하지만, 쿼리 파라미터는 자주 사용해서
      타임리프에서 직접 지원한다)

🤔강의를 듣고(잡담)

드디어 들어본 MVC패턴 1편 역시 생각했던것 처럼 흥미롭고 유익했다.
처음에 하나씩 알려주는데, 이 정보들이 조각조각 같아서 솔직히 이해하지 못했다.ㅎㅎ
엥... 이제 뭐지?😵‍💫 이 상태로 일단! 듣는데 의의를 두자고 생각하면서 그냥 들었다.
그런데 어느 순간부터 각 정보가 하나씩 맞춰지고 전체 그림이 완성되어가는데 정말 놀라웠다.🌊💫
아직은 완전히 이해한다고 못 하지만 그래도 전반적인 흐름을 파악하는 데에는 성공한 것 같아 기뻤다.
이제 스프링 MVC 2편 백엔드 웹 개발 활용 기술을 공부하면, 지금보다 더 익숙하게 이해하면서 활용할 수 있지 않을까 기대중이다.

profile
오늘의 기록

0개의 댓글