반응형

# 스프링 MVC 기본 기능

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

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

1. 정적 리소스

  • 예) 웹 브라우저에 정적인 HTML, css, js을 제공할 때, 정적 리소스 사용.

2. 뷰 템플릿 사용

  • 예) 웹 브라우저에 동적인 HTML을 제공할 때, 뷰 템플릿 사용.

3. HTTP 메시지 사용

  • HTTP API를 제공하는 경우, HTML이 아니라 데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보낸다.

정적 리소스

  • 스프링 부트는 클래스패스의 다음 디렉토리에 있는 정적 리소스를 제공 : /static, /public, /resources, /META-INF/resources
  • 정적 리소스 : 해당 파일을 변경 없이 그대로 서비스하는 것.

뷰 템플릿

  • 뷰 템플릿을 거쳐 HTML이 생성되고, 뷰가 응답을 만들어서 전달.
  • 일반적으로 HTML을 동적으로 생성하는 용도로 사용 하지만, 다른 것들도 가능. (뷰 템플릿이 만들 수 있는 것이라면 뭐든지 가능.)
  • 뷰 템플릿 경로 
src/main/resources/templates
  • hello.html (뷰 템플릿)
<!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>
  • ResponseViewController. (뷰 템플릿 사용을 위한 컨트롤러)
package hello.springmvc.basic.response;

import org.springframework.stereotype.Controller;
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;
    }
}
  • String을 반환하는 경우 - View or HTTP 메시지
@RequestMapping("/response-view-v2")
public String responseViewV2(Model model) {
    model.addAttribute("data", "hello!");
    return "response/hello";
}
  • @ResponseBody 가 없으면 response/hello 로 뷰 리졸버가 실행되어 뷰를 찾고, 렌더링.
  • @ResponseBody 가 있으면 뷰 리졸버를 실행하지 않고, HTTP 메시지 바디에 직접 response/hello 라는 문자 입력.
  • Void를 반환하는 경우 - 해당 방식은 명시성이 떨어지고 이렇게 딱 맞는 경우도 없어서, 권장하지 않는다.
@RequestMapping("/response/hello")
public void responseViewV3(Model model) {
    model.addAttribute("data", "hello!");
}

Thymeleaf 스프링 부트 설정

  • 다음 라이브러리를 추가 (build.gradle에 추가)
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
  • 스프링 부트가 자동으로 ThymeleafViewResolver 와 필요한 스프링 빈들을 등록. 그리고 아래 설정도 사용. (기본 값)
  • application.properties
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html

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

  • HTTP API를 제공하는 경우, HTML이 아닌 데이터를 전달해야 하므로, HTTP 메시지 바디에 JSON 같은 형식으로 데이터를 실어 보낸다.

ResponseBodyController.

package hello.springmvc.basic.response;

import hello.springmvc.basic.HelloData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@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(25);
        return new ResponseEntity<>(helloData, HttpStatus.OK);
    }

    @ResponseStatus(HttpStatus.OK)
    @GetMapping("/response-body-json-v2")
    public HelloData responseBodyJsonV2() {
        HelloData helloData = new HelloData();
        helloData.setUsername("userA");
        helloData.setAge(25);
        return helloData;
    }
}

responseBodyJsonV1

  • ResponseEntity 를 반환. HTTP 메시지 컨버터를 통해 JSON 형식으로 변환되어서 반환.

responseBodyJsonV2

  • ResponseEntity 는 HTTP 응답 코드를 설정할 수 있는데, @ResponseBody 를 사용하면 설정하기 까다롭다.
  • @ResponseStatus(HttpStatus.OK) 애노테이션 : 응답 코드 설정할 수 있는 애노테이션. 물론 애노테이션이기 때문에 응답 코드를 동적으로 변경할 수는 없다. (프로그램 조건에 따라 동적으로 변경하려면 ResponseEntity 사용.)

@RestController (@Controller + @ResponseBody)

  • @Controller 대신 @RestController 애노테이션을 사용하면, 해당 컨트롤러에 모두 @ResponseBody 가 적용되는 효과.
  • 따라서 뷰 템플릿을 사용하는 것이 아닌, HTTP 메시지 바디에 직접 데이터를 입력한다.
  • 이름 그대로 Rest API(HTTP API)를 만들 때 사용하는 컨트롤러.
  • @ResponseBody 는 클래스 레벨에 두면 전체에 메서드에 적용, @RestController 에노테이션 안에 @ResponseBody 가 적용되어 있다.

## HTTP 메시지 컨버터

  • 뷰 템플릿으로 HTML을 생성해서 응답하는 것이 아닌, HTTP API처럼 JSON 데이터를 HTTP 메시지 바디에서 직접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 사용하면 편리하다.

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

  • HTTP 요청 : @RequestBody , HttpEntity(RequestEntity)
  • HTTP 응답 : @ResponseBody , HttpEntity(ResponseEntity)

HTTP 메시지 컨버터 인터페이스

org.springframework.http.converter.HttpMessageConverter
/*
 * Copyright 2002-2021 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.http.converter;

import java.io.IOException;
import java.util.Collections;
import java.util.List;

import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;

/**
 * Strategy interface for converting from and to HTTP requests and responses.
 *
 * @author Arjen Poutsma
 * @author Juergen Hoeller
 * @author Rossen Stoyanchev
 * @since 3.0
 * @param <T> the converted object type
 */
public interface HttpMessageConverter<T> {

   /**
    * Indicates whether the given class can be read by this converter.
    * @param clazz the class to test for readability
    * @param mediaType the media type to read (can be {@code null} if not specified);
    * typically the value of a {@code Content-Type} header.
    * @return {@code true} if readable; {@code false} otherwise
    */
   boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);

   /**
    * Indicates whether the given class can be written by this converter.
    * @param clazz the class to test for writability
    * @param mediaType the media type to write (can be {@code null} if not specified);
    * typically the value of an {@code Accept} header.
    * @return {@code true} if writable; {@code false} otherwise
    */
   boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);

   /**
    * Return the list of media types supported by this converter. The list may
    * not apply to every possible target element type and calls to this method
    * should typically be guarded via {@link #canWrite(Class, MediaType)
    * canWrite(clazz, null}. The list may also exclude MIME types supported
    * only for a specific class. Alternatively, use
    * {@link #getSupportedMediaTypes(Class)} for a more precise list.
    * @return the list of supported media types
    */
   List<MediaType> getSupportedMediaTypes();

   /**
    * Return the list of media types supported by this converter for the given
    * class. The list may differ from {@link #getSupportedMediaTypes()} if the
    * converter does not support the given Class or if it supports it only for
    * a subset of media types.
    * @param clazz the type of class to check
    * @return the list of media types supported for the given class
    * @since 5.3.4
    */
   default List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
      return (canRead(clazz, null) || canWrite(clazz, null) ?
            getSupportedMediaTypes() : Collections.emptyList());
   }

   /**
    * Read an object of the given type from the given input message, and returns it.
    * @param clazz the type of object to return. This type must have previously been passed to the
    * {@link #canRead canRead} method of this interface, which must have returned {@code true}.
    * @param inputMessage the HTTP input message to read from
    * @return the converted object
    * @throws IOException in case of I/O errors
    * @throws HttpMessageNotReadableException in case of conversion errors
    */
   T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
         throws IOException, HttpMessageNotReadableException;

   /**
    * Write an given object to the given output message.
    * @param t the object to write to the output message. The type of this object must have previously been
    * passed to the {@link #canWrite canWrite} method of this interface, which must have returned {@code true}.
    * @param contentType the content type to use when writing. May be {@code null} to indicate that the
    * default content type of the converter must be used. If not {@code null}, this media type must have
    * previously been passed to the {@link #canWrite canWrite} method of this interface, which must have
    * returned {@code true}.
    * @param outputMessage the message to write to
    * @throws IOException in case of I/O errors
    * @throws HttpMessageNotWritableException in case of conversion errors
    */
   void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
         throws IOException, HttpMessageNotWritableException;

}
  • HTTP 메시지 컨버터는 HTTP 요청, HTTP 응답 둘 다 사용.
canRead() , canWrite() : 메시지 컨버터가 해당 클래스, 미디어타입을 지원하는지 체크.

read() , write() : 메시지 컨버터를 통해서 메시지를 읽고 쓰는 기능.

스프링 부트 기본 메시지 컨버터 (일부 생략)

0 = ByteArrayHttpMessageConverter

1 = StringHttpMessageConverter

2 = MappingJackson2HttpMessageConverter

주요한 메시지 컨버터

  • 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 관련

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)

  • @RequestMapping 을 처리하는 핸들러 어댑터인 RequestMappingHandlerAdapter (요청 매핑 헨들러 어뎁터)

ArgumentResolver (파라미터 처리!)

  • 애노테이션 기반의 컨트롤러는 매우 다양한 파라미터를 사용. HttpServletRequest , Model 은 물론이고, @RequestParam , @ModelAttribute 같은 애노테이션 그리고 @RequestBody , HttpEntity 같은 HTTP 메시지를 처리하는 부분까지 큰 유연함을 보여주었다.
  • 이렇게 파라미터를 유연하게 처리할 수 있는 이유가 바로 ArgumentResolver 덕분.
  • 애노테이션 기반 컨트롤러를 처리하는 RequestMappingHandlerAdaptor 는 바로 이 ArgumentResolver 를 호출해 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터의 값(객체)을 생성. 그리고 파리미터의 값이 모두 준비되면 컨트롤러를 호출하면서 값을 넘겨준다.
  • 스프링은 30개가 넘는 ArgumentResolver 를 기본으로 제공. (HandlerMethodArgumentResolver 인데 줄여서 ArgumentResolver 라고 부른다)
/*
 * Copyright 2002-2014 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.web.method.support;

import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;

/**
 * Strategy interface for resolving method parameters into argument values in
 * the context of a given request.
 *
 * @author Arjen Poutsma
 * @since 3.1
 * @see HandlerMethodReturnValueHandler
 */
public interface HandlerMethodArgumentResolver {

   /**
    * Whether the given {@linkplain MethodParameter method parameter} is
    * supported by this resolver.
    * @param parameter the method parameter to check
    * @return {@code true} if this resolver supports the supplied parameter;
    * {@code false} otherwise
    */
   boolean supportsParameter(MethodParameter parameter);

   /**
    * Resolves a method parameter into an argument value from a given request.
    * A {@link ModelAndViewContainer} provides access to the model for the
    * request. A {@link WebDataBinderFactory} provides a way to create
    * a {@link WebDataBinder} instance when needed for data binding and
    * type conversion purposes.
    * @param parameter the method parameter to resolve. This parameter must
    * have previously been passed to {@link #supportsParameter} which must
    * have returned {@code true}.
    * @param mavContainer the ModelAndViewContainer for the current request
    * @param webRequest the current request
    * @param binderFactory a factory for creating {@link WebDataBinder} instances
    * @return the resolved argument value, or {@code null} if not resolvable
    * @throws Exception in case of errors with the preparation of argument values
    */
   @Nullable
   Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
         NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;

}

 

  • 동작방식 : ArgumentResolver 의 supportsParameter() 를 호출 해당 파라미터를 지원하는지 체크, 지원하면 resolveArgument() 를 호출해서 실제 객체를 생성. 그리고 이렇게 생성된 객체가 컨트롤러 호출 시 넘어간다.

ReturnValueHandler

  • HandlerMethodReturnValueHandler 를 줄여 ReturnValueHandle 라고 부른다.
  • ArgumentResolver 와 비슷한데, 이것은 응답 값을 변환하고 처리.
  • 컨트롤러에서 String으로 뷰 이름을 반환해도, 동작하는 이유가 바로 ReturnValueHandler 덕분이다.
  • 스프링은 10여개가 넘는 ReturnValueHandler 를 지원. (ModelAndView , @ResponseBody , HttpEntity , String 등)

HTTP 메시지 컨버터

  • HTTP 메시지 컨버터를 사용하는 @RequestBody 도 컨트롤러가 필요로 하는 파라미터의 값에 사용.
  • @ResponseBody 도 컨트롤러의 반환 값을 이용.
  • ArgumentResolver은 HTTP 메서지 컨버터 사용. 

요청

  • @RequestBody 를 처리하는 ArgumentResolver, HttpEntity 를 처리하는 ArgumentResolver 가 존재.
  • 이 ArgumentResolver 들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성하는 것.

응답

  • @ResponseBody 와 HttpEntity 를 처리하는 ReturnValueHandler 가 존재. 그리고 여기에서 HTTP 메시지 컨버터를 호출해서 응답 결과를 만든다.
  • 스프링 MVC는 @RequestBody @ResponseBody 가 있으면 RequestResponseBodyMethodProcessor (ArgumentResolver), HttpEntity 가 있으면 HttpEntityMethodProcessor (ArgumentResolver)를 사용.

확장

  • 스프링은 다음을 모두 인터페이스로 제공. (필요하면 언제든 기능 확장가능.)
  • HandlerMethodArgumentResolver
  • HandlerMethodReturnValueHandler
  • HttpMessageConverter
  • 기능 확장 : WebMvcConfigurer 를 상속 받아 스프링 빈으로 등록하면 된다.
반응형

+ Recent posts