Spring Web MVC

DispatcherServlet

자바 설정 Since 3.0 - WebApplicationInitializer 정의 예

정상적으로 완료되면 실행 시 로그에 "INFO: 1 Spring WebApplicationInitializers detected on classpath"처럼 기록된다. 자동으로 스캔되지 않는 경우 /META-INF/services/javax.servlet.ServletContainerInitializer 파일에 해당 클래스 명시

web.xml 기술 예

  • 기본적으로 {서블릿_이름}-servlet.xml 설정을 읽어온다
  • {서블릿_이름}-servlet == default namespace

  • 다른 파일(들)을 로드하고 싶거나, 아예 로드하고 싶지 않은 경우 contextConfigLocation 조정
  • 각 경로들은 ','로 구분하면 된다. 클래스패스에 있는 파일은 classpath:conf.xml처럼 지정하면 된다

    ↓ 추가 로드 x

    <servlet> <servlet-name>app</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value></param-value> </init-param> </servlet>
  • xml에서 ConfigurableWebApplicationContext 구현 클래스를 지정하고 싶은 경우, contextClass 조정
  • 기본값은 XmlWebApplicationContext

    ↓ xml

    <servlet> <servlet-name>app</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextClass</param-name> <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value> </init-param> <init-param> <param-name>contextConfigLocation</param-name> <param-value>package..ClassName</param-value> </init-param> </servlet>

Context Hierarchy

  • 일반적으로 하나의 WebApplicationContext로 충분하지만, 루트 WebApplicationContext를 DispatcherServlet들이 공유하고, 각각은 자식 WebApplicationContext를 갖는 계층 구조로도 구성 가능하다
  • 루트 WebApplicationContext는 전형적으로 Repository, Service 등 여러 Servlet 사이에서 공유될 빈들을 갖는다
  • 이들은 자식 WebApplicationContext로 상속되며, 필요에 따라 재정의(override)할 수 있다

  • 자바 구성 예
  • web.xml 예

Special Bean Types

DispatcherServlet이 요청을 처리하도록 전달하는 빈들을 Special Bean이라고 하며, 정의하지 않은 경우 기본값이 사용된다

타입설명
HandlerMapping요청 URI -> 핸들러 매핑. @RequestMapping, @GetMapping, ...
HandlerAdapter핸들러의 구현에 상관없이 DispatcherServlet이 호출할 수 있도록 래퍼 제공
HandlerExceptionResolver예외 발생 시 처리 전략
ViewResolver핸들러가 렌더링할 View 이름을 반환하면 적절한 View를 선택
LocaleResolver, LocaleContextResolver클라이언트 Locale 선택
MultipartResolverMulti-part 요청 처리 api 제공
FlashMapManager요청 간 FlashMap 인스턴스 공유 기능 제공

Processing; 요청 처리 절차

  1. 요청에 맞는 WebApplicationContext가 검색되어 바운딩
  2. LocaleResolver 바운딩
  3. ThemeResolver 바운딩
  4. Multi-part 요청이라면 MultipartHttpServletRequest로 래핑
  5. 요청에 대한 핸들러 호출

Interception

핸들러 요청에 대한 전/후 처리를 추가할 수 있다 Goto - MVC config - Interceptors

↓ interface HandlerInterceptor

default boolean preHandle(request, response, handler); // DispatcherServlet이 핸들러를 결정하고, 아직 HandlerAdapter가 핸들러를 호출하지 않은 시점에 실행된다 // 다음 처리로 진행한다면 true. false를 반환한 경우, DispatcherServlet은 여기서 response로 모든 응답을 했다고 간주한다 default void postHandle(request, response, handler, @Nullable modelAndView); // HandlerAdapter가 핸들러를 호출하고, 아직 DispatcherServlet이 뷰를 렌더링하지 않은 시점에 실행된다 // 전달된 modelAndView에 다른 모델 객체들을 추가해도 된다 default void afterCompletion(request, response, handler, @Nullable ex) // 모든 처리가 완료되었으므로 자원 정리 등의 작업을 수행해도 된다 // preHandle()이 성공적으로 true를 반환한 경우에도 실행된다

Exceptions

HandlerExceptionResolver 종류

  • SimpleMappingExceptionResolver
  • 예외 클래스 -> 뷰 이름 매핑

    ↓ xml

    <bean id="exceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"> <property name="exceptionMappings"> <props> <prop key="MaxUploadSizeExceededException"> error </prop> </props> </property> </bean>
  • DefaultHandlerExceptionResolver
  • 예외 -> HTTP 상태 코드 매핑

  • ResponseStatusExceptionResolver
  • 예외 발생 메서드 또는 발생한 예외 클래스의 @ResponseStatus -> HTTP 상태 코드 매핑

  • ExceptionHandlerExceptionResolver
  • @Controller 또는 @ControllerAdvice 클래스의 @ExceptionHandler 메서드가 예외 처리 Goto - Annotated Controllers - Exceptions

Chain of Resolvers

여러 HandlerExceptionResolver 빈을 정의하면 order 및 정의된 순서에 따라 호출되는 체인을 구성할 수 있다

Container Error Page

예외가 처리되지 않았고, 응답 상태가 에러(4xx, 5xx)인 경우를 위해 기본 에러 페이지를 지정할 수 있다

↓ web.xml

<error-page> <exception-type>java.lang.Throwable</exception-type> // 옵션 <location>/path/url</location> </error-page>

View Resolution

ViewResolver 종류

    Goto - View Technologies
  • AbstractCachingViewResolver
  • 뷰 인스턴스를 캐시하여 이용한다. cache 속성을 이용해 전체 캐시를 끄거나, removeFromCache()로 일부 뷰만 캐시에서 제거할 수 있다

  • XmlViewResolver, BeanNameViewResolver
  • 현재 컨텍스트에서 뷰 이름과 일치하는 빈을 뷰로 이용한다. XmlViewResolver은 따로 지정하지 않으면 기본으로 /WEB-INF/views.xml를 로드한다

  • ResourceBundleViewResolver
  • ResourceBundle(기본 리소스 파일 : views.properties)에 저장된 빈 정보로 뷰를 찾는다. 정확히는 [viewname].(class) 속성을 뷰 클래스로, [viewname].url 속성을 뷰 url로 이용한다

  • UrlBasedViewResolver
    • Url 경로에 대응하는 파일을 뷰로 이용한다
    • setContentType()으로 기본 Content-Type 헤더 설정 가능 ─ JVM 기본 인코딩이 잘못된 경우 이걸로 수정 가능
    • ↓ java

      @ComponentScan(basePackages = "io.github.donggi.mvc.controller, io.github.donggi.mvc.service") @EnableWebMvc @Configuration public class AppConfig implements WebMvcConfigurer { @Override public void configureViewResolvers(ViewResolverRegistry registry) { var resolver = new FreeMarkerViewResolver("", ".ftl"); resolver.setContentType("text/html;charset=UTF-8"); registry.viewResolver(resolver); } }
  • InternalResourceViewResolver
  • UrlBasedViewResolver의 서브클래스로, InternalResourceView(Servlet, JSP) 파일을 뷰로 이용한다. 그 외 서브클래스 ─ TilesViewResolver, XsltViewResolver, FreeMarkerViewResolver ─ 들도 존재

    ↓ xml

    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/> // ← JSTL 이용하는 경우 <property name="prefix" value="/WEB-INF/view/" /> <property name="suffix" value=".jsp" /> </bean>
  • ContentNegotiatingViewResolver
  • 컨텐츠 협상이 가능한 ViewResolver. 다른 ViewResolver들이 고른 View 중, contentType이 요청과 일치하는 것을 선택한다

Handling

  • 여러 ViewResolver를 이용 가능
  • order 속성으로 순서를 제어할 수 있다

  • null을 반환하면 뷰를 찾지 못했음을 의미한다
  • InternalResourceViewResolver는 마지막에 동작해야 한다

Redirecting

  • UrlBasedViewResolver 및 서브클래스를 이용하는 경우, 뷰 이름 앞에 redirect:를 붙이면 리다이렉션을 수행한다
  • 예. redirect:/myapp/some/resource

  • redirect:https://myhost.com/some 처럼 외부 절대경로 이용 가능

Forwarding

  • UrlBasedViewResolver 및 서브클래스를 이용하는 경우, 뷰 이름 앞에 forward:를 붙이면 포워딩을 수행한다
  • RequestDispatcher.forward()를 호출하는 InternalResourceView 인스턴스를 생성하므로 JSP만 사용하는 경우는 유용하지 않음
  • 다른 종류의 뷰를 이용하지만 서블릿/JSP로 포워딩이 필요한 경우에 적합

Multipart Resolver

Multi-part 요청을 처리하려면 DispatcherServlet에 "multipartResolver" 이름의 MultipartResolver 빈을 정의해야 한다

  • commons-fileupload
    • JavaConfig 예
    • xml 예
  • Since Servlet 3.0
    1. Initializer에서 MultipartConfigElement 추가, 또는 web.xml에 multipart-config 추가
    2. StandardServletMultipartResolver 빈 정의
    • JavaConfig 예
    • xml 예
  • @RequestParam으로 Map<String, MultipartFile>, MultiValueMap<String, MultipartFile>도 가능
  • Since Servlet 3.0 : MultipartFile 대신 javax.servlet.http.Part 이용 가능

Logging - Sensitive Data

기본적으로 요청 인자와 헤더는 로깅하지 않는다. 로깅하려면 DispatcherServlet의 enableLoggingRequestDetails를 true로 설정하면 된다

↓ java

public class Initializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected void customizeRegistration(ServletRegistration.Dynamic registration) { registration.setInitParameter("enableLoggingRequestDetails", "true"); } }

↓ xml

<servlet> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>enableLoggingRequestDetails</param-name> <param-value>true</param-value> </init-param> </servlet>

Filters

OncePerRequestFilter 서브클래스

AbstractRequestLoggingFilter, CharacterEncodingFilter, CorsFilter, FormContentFilter, ForwardedHeaderFilter, HiddenHttpMethodFilter, HttpPutFormContentFilter, MultipartFilter, OpenEntityManagerInViewFilter, OpenSessionInViewFilter, RelativeRedirectFilter, RequestContextFilter, ShallowEtagHeaderFilter

Default Encoding

CharacterEncodingFilter는 HttpServletRequest 변수 request에 대하여 아래를 수행한다

↓ java

if (isForceRequestEncoding() || request.getCharacterEncoding() == null) { request.setCharacterEncoding(encoding); }

Form Data

FormContentFilter를 이용하면 HTTP PUT, PATCH, DELETE 요청으로 온 application/x-www-form-urlencoded 내용을 ServletRequest.getParameter*()로 접근할 수 있다

Forwarded Headers

  • 프록시 서버를 거치는 경우 실제 클라이언트의 주소는 Forwarded 헤더 ─ X-Forwarded-Host, X-Forwarded-Port, ... ─ 로 옮겨진다
  • ForwardedHeaderFilter를 이용하면 요청의 주소를 Forwarded 헤더의 것으로 변경하고 Forwarded 헤더를 지운다
  • 요청을 래핑해야 하므로, 가장 먼저 실행될 필요가 있다

  • Forwarded 헤더가 공격자로부터 삽입된 것이라면 이를 사용하지 않고 삭제만 해야한다
  • ForwardedHeaderFilter의 removeOnly 속성을 true로 설정하면 된다

  • AbstractAnnotationConfigDispatcherServletInitializer를 이용하는 경우, DispatcherType.REQUEST, ASYNC, ERROR 모두에 대해 실행된다
  • 그 외의 경우 DispatcherType.REQUEST에 대해서만 동작한다

CORS

Controller 클래스에 애너테이션을 붙여 CORS 설정을 할 수 있지만, Spring Security를 이용하는 경우 CorsFilter를 이용해 Security보다 먼저 실행되도록 설정할 것이 권장된다 Goto - CORS

Annotated Controllers

Declaration

  • @Controller : WebApplicationContext에서 웹 요청 처리자로 간주된다
  • @RestController : @Controller + 클래스 수준 @ResponseBody

Request Mapping

  • 컨트롤러 클래스 및 메서드에 @RequestMapping을 붙여 어떤 요청을 처리하는지 명시
  • ↓ java

    @RequestMapping("/file/*") public class FileController { @RequestMapping(value = "/upload", method = RequestMethod.GET) public String upload() { } }
  • HTTP 요청에 따라 @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping 존재

URI Patterns

PatternDescription
?임의 1글자
*1개 경로 조각 내의 임의 0개 이상 글자
**0개 이상의 연속적인 경로 조각
{name}1개 경로 조각을 name 변수로 획득
{name:regex}경로가 정규식에 일치하면 해당 부분을 name 변수로 획득

↓ java

@GetMapping("/owners/{ownerId}/pets/{petId}") public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId); // 컴파일된 클래스에 매개변수 이름이 그대로 남아있다면 @PathVariable에 이름 설정 불필요

Pattern Comparison

URL 하나가 여러 패턴에 매칭되는 경우, 가장 일반적이지 않은 패턴이 선택된다. 우선순위는 AntPathMatcher.AntPatternComparator.html 참고

Suffix Match

  • Before 5.3 : 기본적으로 .* 접미사 매칭이 수행된다. 예를 들어 패턴 "/person"은 "/person.pdf" 같은 URL도 매칭한다.
  • 기능을 끄려면 PathMatchConfigurer.useSuffixPatternMatching(false) Goto - MVC config - Path Matching, ContentNegotiationConfigurer.favorPathExtension(false) Goto - MVC config - Content Types를 호출해야 한다
  • 브라우저의 Accept 헤더를 일관적으로 해석하지 못하던 때에는 필수적이었지만, 요즘은 그렇지 않다. 5.3 버전부터는 기본적으로 false로 설정된다
  • RFD 공격을 예방하기 위해, 아래를 모두 만족하는 경우 응답 헤더에 Content-Disposition:inline;filename=f.txt를 설정한다
    1. URL에 파일 확장자가 존재
    2. 확장자가 안전하지 않고 컨텐츠 협상 대상도 아닌 경우

Media Types

  • Content-Type:application/json만 처리
  • ↓ java

    @PostMapping(path = "/pets", consumes = "application/json")
  • Content-Type:application/json만 처리 안 함
  • ↓ java

    @PostMapping(path = "/pets", consumes = "!application/json")
  • Accept:application/json만 처리
  • ↓ java

    @GetMapping(path = "/pets/{petId}", produces = "application/json")
  • Accept:application/json만 처리 안 함
  • ↓ java

    @GetMapping(path = "/pets/{petId}", produces = "!application/json")
  • 클래스 수준의 consumes, produces 가능
  • 클래스와 메서드 모두에 적용하는 경우, 메서드의 것만 적용

  • MediaType 클래스에 APPLICATION_JSON_UTF8_VALUE 등 상수 존재

Parameters, Headers

  • 요청 파라미터에 myParam이 존재하는 경우만 처리
  • ↓ java

    @GetMapping(path = "/pets/{petId}", params = "myParam")
  • 요청 파라미터에 myParam이 존재하지 않는 경우만 처리
  • ↓ java

    @GetMapping(path = "/pets/{petId}", params = "!myParam")
  • 요청 파라미터에 myParam 값이 "myValue"인 경우만 처리
  • ↓ java

    @GetMapping(path = "/pets/{petId}", params = "myParam=myValue")
  • 헤더의 경우 params 대신 headers 이용

Handler Methods

Method Arguments

    아래 조건을 모두 만족하는 경우, java.util.Optional을 결합하여 사용할 수 있다
  1. required 속성이 있는 @RequestParam, @RequestHeader 등의 애너테이션이 적용됨
  2. required=false임
ArgumentDescription
WebRequest, NativeWebRequest서블릿 API를 직접 사용하지 않고 요청에 대한 일반적인 접근 제공
ServletRequest, ServletResponseHttpServletRequest, MultipartRequest 등 임의 서브클래스 사용 가능
HttpSession세션이 반드시 존재하도록 한다. 동시성이 필요한 경우 RequestMappingHandlerAdapter 인스턴스의 synchronizeOnSession를 true로 설정
PushBuilderHTTP/2 리소스 푸시를 위한 Servlet 4.0 push builder API 제공. 클라이언트가 HTTP/2를 지원하지 않으면 null
Principal현재 인증 유저
HttpMethodHTTP 요청 타입
Locale, TimeZone, ZoneId현재 로캐일, 타임존
InputStream, ReaderRaw 요청 접근
OutputStream, Writer응답을 직접 쓰려는 경우 사용
@PathVariableURI 경로 획득 Goto - URI Patterns
@MatrixVariableURI 상의 이름-값 쌍 획득 Goto - Matrix Variables
@RequestParam서블릿 요청 파라미터(URL query, Form data 모두 해당) 획득. Multi-part 파일도 가능
@RequestHeader요청 헤더 획득
@CookieValue쿠키 획득
@RequestBodyHTTP 요청 body 획득. HttpMessageConverter를 통해 지정 타입으로 변환된다
HttpEntity<B>요청 헤더(HttpHeaders) + body(B) 획득. body는 HttpMessageConverter를 통해 지정 타입으로 변환된다
@RequestPartmultipart/form-data 요청 획득. 각 part는 HttpMessageConverter를 통해 지정 타입으로 변환된다
Map, Model, ModelMap렌더링에 사용할 모델
RedirectAttributes리다이렉션 쿼리에 덧붙일 속성Goto - Redirect Attributes, 리다이렉션 이후 요청까지 임시로 저장할 속성Goto - Flash Attributes
@ModelAttribute기저 모델에 존재하는 객체 획득(없으면 생성). 기저와 바인딩되며, 존재하는 검증도 수행된다. Goto - @ModelAttribute. 바인딩을 원하지 않는 경우 @ModelAttribute(binding=false) 설정
Errors, BindingResult@ModelAttribute 인자의 검증 및 바인딩 수행 시 발생한 예외 획득. @RequestBody 또는 @RequestPart 검증 시 발생한 예외 획득. 각 Errors, BindingResult 인자는 검증 대상 바로 다음에 위치해야 한다
SessionStatus + class-level @SessionAttributesForm 처리가 완료되면 @SessionAttributes로 저장 중인 값을 정리하기 위한 SessionStatus 획득 Goto - @SessionAttribute
UriComponentsBuilder요청 host, port, scheme, context path, servlet mapping으로 구성된 builder 획득
@SessionAttribute세션에 저장된 임의 인스턴스 접근 Goto - @SessionAttribute
@RequestAttribute임의 요청 속성 접근 Goto - @RequestAttribute
임의 타입위 타입 중 어느 것에도 일치하지 않는 단순 타입은 @RequestParam, 그 외에는 @ModelAttribute

Return Values

Return TypeDescription
@ResponseBodyHttpMessageConverter 인스턴스가 반환값을 적절히 응답에 쓴다 Goto - @ResponseBody
HttpEntity<B>, ResponseEntity<B>HTTP 헤더(HttpHeaders)와 바디(B)를 포함한 전체 응답을 반환하면 HttpMessageConverter가 적절히 응답에 쓴다Goto - ResponseEntity
HttpHeaders응답 헤더 반환. body는 empty
String뷰 이름을 반환하면 ViewResolver가 적절히 뷰를 선택한다
View렌더링할 뷰
Map, Model, @ModelAttribute AnyType묵시적 모델에 추가될 속성
ModelAndView뷰 + 모델
voidvoid 리턴, null 리턴이 아래 상황에서 이루어졌다면 응답이 완료됐다고 간주한다
  • ServletResponse, OutputStream 인자를 메서드로 받은 경우
  • @ResponseStatus를 갖는 경우
  • 컨트롤러가 ETag, lastModified를 설정한 경우
그 외의 경우 "no response body"(REST 컨트롤러), 기본 뷰(HTTP 컨트롤러)를 의미한다
DeferredResult<V>, ListenableFuture<V>, CompletionStage<V>, CompletableFuture<V>위 임의 타입을 임의 스레드에서 비동기적으로 반환 Goto - DeferredResult
Callable<V>위 임의 타입을 Spring MVC 관리 스레드에서 비동기적으로 반환 Goto - Callable
ResponseBodyEmitter, SseEmitter비동기적으로 객체를 전달하면 HttpMessageConverter가 변환하여 쓴다. ResponseEntity의 body로 이용 가능. Goto - HTTP Streaming
StreamingResponseBody비동기적으로 OutputStream에 쓴다. ResponseEntity의 body로 이용 가능. Goto - HTTP Streaming
Reactive types — Reactor, RxJava, or others through ReactiveAdapterRegistry한 번에 여러 값을 전송하는 경우 DeferredResult 대안

Type Conversion

문자열로 표현되는 요청 인자 ─ @RequestParam, @RequestHeader, @PathVariable, @MatrixVariable, @CookieValue ─ 들을 String 외의 타입으로 받으려면 변환이 필요하다
기본 타입과 Date 등 일부 클래스는 자동으로 변환이 가능하다. 전역적인 변환기를 등록하려면 Goto - MVC config - Type Conversion, @Controller, @ControllerAdvice에만 적용되는 변환기를 등록하려면 Goto - DataBinder 참고

Matrix Variables

  • Java Config
  • XML Config
  • Matrix Variable 예
  • 각 path part 모두 matrix variable을 가질 수 있다
  • 각 path part 별로 matrix variable을 모아 받을 수 있다

@RequestParam, @RequestBody

  • Get 요청 query 예
  • 배열 또는 리스트를 이용해 동일 이름의 파라미터들을 모을 수 있다
  • Map<String, String>, MultiValueMap<String, String>로 전체 파라미터를 모을 수 있다
  • Post 요청 Jackson JSON 예

@RequestHeader

특정 이름의 헤더, 또는 전체 헤더 ─ Map<String, String>, MultiValueMap<String, String>, HttpHeaders ─ 획득 가능

@ModelAttribute

    @ModelAttribute로 선언된 인자는 아래 순서에 따라 획득된다
  1. 이미 Model에 추가된 객체
  2. @SessionAttribute로 저장된 객체Goto - @SessionAttribute
  3. URI path variable로부터 변환(Converter)
  4. 기본생성자 호출
  5. Primary 생성자 호출. 생성 인자는 서블릿 요청으로부터 결정

@SessionAttributes, @SessionAttribute

  • @SessionAttributes는 자동으로 세션에 객체를 저장하고, 완료 시 제거하는 기능을 제공한다
  • @SessionAttribute로 세션 객체를 획득할 수 있다

@RequestAttribute

Filter, HandlerInterceptor 등에 의해 추가된 속성을 획득할 수 있다

Redirect Attributes

  • 기본적으로 Model의 속성들은 리다이렉트 URL에 포함된다
  • 속성의 자동 추가를 원하지 않는다면, @RequestMapping 메서드에서 RedirectAttributes 매개변수를 명시적으로 이용하면 된다
  • 전역적으로 설정을 변경하려면 RequestMappingHandlerAdapter의 ignoreDefaultModelOnRedirect 속성을 true로 변경하면 된다
  • 하위 호환을 위해 false가 기본값이다

Flash Attributes

  • Flash attribute는 다음 요청에서 소비하기 위해 일시적으로 세션에 저장하는 데이터다. 주로 리다이렉션 처리에 이용된다
  • 임시 속성을 담는 데 FlashMap이 이용되고, FlashMapManager이 FlashMap을 관리한다
  • FlashMap은 이전 요청으로부터 전달되는 input, 다음 요청으로 전달할 output으로 구분된다
  • @RequestMapping 메서드에서는 input-output FlashMap을 직접 이용하지 않고 RedirectAttributes를 획득하여 사용하면 된다 Goto - Redirect Attributes
  • RedirectView는 FlashMap 인스턴스에 대해 리다이렉트 URL과 시각을 기록함으로써, 실제 리다이렉트된 요청에 임시 속성을 전달하게 한다
  • 그럼에도 불구하고 비동기적으로 빈번하게 요청이 들어오는 경우, 실제로 전달되어야 할 곳이 아닌 다른 요청에 임시 속성을 전달될 가능성이 있다

@ResponseBody

  • 메서드에 @ResponseBody가 적용된 경우 반환 객체를 HTTP 응답으로(HttpMessageConverter) 전송한다. AJAX에 유용
  • Jackson JSON 예

ResponseEntity

Model

  • @Controller 또는 @ControllerAdvice 클래스의 메서드에 @ModelAttribute를 적용하면 @RequestMapping 전에 호출되어 모델을 구성한다
  • @ControllerAdvice 안에서 이용하는 경우 여러 컨트롤러에 공유하는 효과가 있다
  • @ModelAttribute 메서드는 @RequestMapping 메서드가 사용하는 매개변수 타입들을 사용할 수 있다 ─ @ModelAttribute, 요청 body 관련 부분 제외

DataBinder

  • @Controller, @ControllerAdvice 클래스는 @InitBinder 메서드에서 WebDataBinder 인스턴스를 초기화할 수 있다
  • 이를 통해 PropertyEditor, Converter, Formatter를 등록할 수 있다

  • @InitBinder 메서드는 @RequestMapping 메서드가 사용하는 매개변수 타입들을 사용할 수 있다 ─ @ModelAttribute 제외
  • 전역적인 변환기 등록은 Goto - MVC config - Type Conversion 참고

Exceptions

  • @Controller, @ControllerAdvice 클래스는 예외 처리를 위한 @ExceptionHandler 메서드를 가질 수 있다
  • 발생한 최상위 예외 또는 가장 가까운 원인 예외 타입을 기준으로 매칭된다
  • 둘 이상의 예외 타입을 처리하려는 경우, 공통 부모 클래스를 인자로 하면 된다
  • value 속성으로 특정 예외들을 명시 가능
  • ↓ java

    @ExceptionHandler({FileSystemException.class, RemoteException.class})
  • @ControllerAdvice 예

Method Arguments

가능한 메서드 인자
ArgumentDescription
Exception발생 예외. 임의 서브클래스 사용 가능
HandlerMethod예외 발생 메서드
WebRequest, NativeWebRequest서블릿 API를 직접 사용하지 않고 요청에 대한 일반적인 접근 제공
ServletRequest, ServletResponseHttpServletRequest, MultipartRequest 등 임의 서브클래스 사용 가능
HttpSession세션이 반드시 존재하도록 한다. 동시성이 필요한 경우 RequestMappingHandlerAdapter 인스턴스의 synchronizeOnSession를 true로 설정
Principal현재 인증 유저
HttpMethodHTTP 요청 타입
Locale, TimeZone, ZoneId현재 로캐일, 타임존
OutputStream, Writer응답을 직접 쓰려는 경우 사용
Map, Model, ModelMap예외 응답을 위한 모델. 항상 비어있음
RedirectAttributes리다이렉션 쿼리에 덧붙일 속성Goto - Redirect Attributes, 리다이렉션 이후 요청까지 임시로 저장할 속성Goto - Flash Attributes
@SessionAttribute세션에 저장된 임의 인스턴스 접근 Goto - @SessionAttribute
@RequestAttribute임의 요청 속성 접근 Goto - @RequestAttribute

Return Values

가능한 반환값
ReturnDescription
@ResponseBodyHttpMessageConverter 인스턴스가 반환값을 적절히 응답에 쓴다 Goto - @ResponseBody
HttpEntity<B>, ResponseEntity<B>HTTP 헤더와 바디를 포함한 전체 응답을 반환하면 HttpMessageConverter가 적절히 응답에 쓴다 Goto - ResponseEntity
String뷰 이름을 반환하면 ViewResolver가 적절히 뷰를 선택한다
View렌더링할 뷰
Map, Model, @ModelAttribute AnyType묵시적 모델에 추가될 속성
ModelAndView뷰 + 모델
voidvoid 리턴, null 리턴이 아래 상황에서 이루어졌다면 응답이 완료됐다고 간주한다
  • ServletResponse, OutputStream 인자를 메서드로 받은 경우
  • @ResponseStatus를 갖는 경우
  • 컨트롤러가 ETag, lastModified를 설정한 경우
그 외의 경우 "no response body"(REST 컨트롤러), 기본 뷰(HTTP 컨트롤러)를 의미한다

REST API 전역 예외 처리

@ControllerAdvice 클래스가 ResponseEntityExceptionHandler를 상속함으로써 스프링 내부 예외를 자동으로 처리하고 ResponseEntity로 반환할 수 있다

Controller Advice

  • @ExceptionHandler, @InitBinder, @ModelAttribute 메서드는 @Controller 뿐만 아니라 @ControllerAdvice, @RestControllerAdvice 클래스도 가질 수 있다
  • 적용 범위를 제한하지 않은 경우(default) 모든 요청에 적용된다

  • @RestControllerAdvice = @ControllerAdvice + @ResponseBody로, @ExceptionHandler 메서드가 응답을 반환함을 의미한다
  • @ExceptionHandler 메서드는 @Controller의 것이 먼저 실행되고, @InitBinder, @ModelAttribute 메서드는 @Controller의 것이 나중에 실행된다

Functional Endpoints

Overview

  • WebMvc.fn에서 HTTP 요청은 HandlerFunction을 통해 처리된다
  • ↓ @FunctionalInterface HandlerFunction

    // org.springframework.web.servlet.function Since 5.2 T handle(ServerRequest request) // org.springframework.web.reactive.function.server Since 5.0 Mono<T> handle(ServerRequest request)
  • HTTP 요청에 대한 HandlerFunction의 선택은 RouterFunction을 통해 이루어진다
  • ↓ @FunctionalInterface RouterFunction

    // org.springframework.web.servlet.function Since 5.2 Optional<HandlerFunction<T>> route(ServerRequest request) // org.springframework.web.reactive.function.server Since 5.0 Mono<HandlerFunction<T>> route(ServerRequest request)
  • 정의된 RouterFunction 빈들은 RouterFunction#andOther()를 통해 합쳐진다
  • RouterFunctions.route()가 RouterFunction 빌더를 제공한다

HandlerFunction

  • ServerRequest
  • HTTP 요청에 대한 불변 객체. HTTP 요청 방식, URI, 헤더, 쿼리 인자, 요청 본문(body)을 제공한다

  • ServerResponse
  • HTTP 응답에 대한 불변 객체.

RouterFunction

  • RequestPredicates 유틸리티 클래스가 유용한 RequestPredicate들을 제공한다
  • RequestPredicate#and, or을 이용해 여러 predicate을 조합할 수 있다

  • 각 라우터는 순서대로 평가되고, 가장 처음으로 매칭되는 핸들러가 요청을 처리한다
  • 중첩 경로에 대한 라우팅 예
  • ↓ java

    import static org.springframework.web.servlet.function.RouterFunctions.route; route() .path("/person", builder -> builder .GET("/{id}", accept(APPLICATION_JSON), handler::getPerson) .GET("", accept(APPLICATION_JSON), handler::listPeople) .POST("/person", handler::createPerson)) .build(); route() .path("/person", b1 -> b1 .nest(accept(APPLICATION_JSON), b2 -> b2 .GET("/{id}", handler::getPerson) .GET("", handler::listPeople)) .POST("/person", handler::createPerson)) .build();

Filtering Handler Functions

before, after, filter를 이용해 사전/사후 작업을 정의할 수 있다

↓ java

route() .path("/person", b1 -> b1 .nest(accept(APPLICATION_JSON), b2 -> b2 .GET("/{id}", handler::getPerson) .GET("", handler::listPeople) .before(request -> ServerRequest.from(request).header("X-RequestHeader", "Value").build())) .POST("/person", handler::createPerson)) .after((request, response) -> logResponse(response)) .build();

URI Links

UriComponents

↓ java

var uriComponents = UriComponentsBuilder .fromUriString("https://example.com/hotels/{hotel}") .queryParam("q", "{q}").encode().build(); URI uri = uriComponents.expand("Westin", "123").toUri();

↓ java

URI uri = UriComponentsBuilder .fromUriString("https://example.com/hotels/{hotel}?q={q}") .build("Westin", "123");

UriBuilder

↓ java

var baseUrl = "https://example.com"; var uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl); URI uri = uriBuilderFactory.uriString("/hotels/{hotel}") .queryParam("q", "{q}").build("Westin", "123");

URI Encoding

  • UriComponentsBuilder#encode()
  • URI 템플릿을 먼저 인코딩. URI 변수들은 할당되는대로 인코딩

  • UriComponents#encode()
  • URI 변수들이 할당된 후 전체 인코딩

  • DefaultUriBuilderFactory#setEncodingMode()
    • EncodingMode.TEMPLATE_AND_VALUES Since 5.0.8 : URI 템플릿을 먼저 인코딩. URI 변수들은 할당되는대로 인코딩
    • EncodingMode.URI_COMPONENT : URI 변수들이 할당된 후 전체 인코딩
    • EncodingMode.VALUES_ONLY : URI 변수만 UriUtils#encodeUriVariables()으로 인코딩
    • EncodingMode.NONE : 인코딩 안 함

Relative Servlet Requests

↓ java

HttpServletRequest request; // Re-uses host, scheme, port, path and query string... var builder = ServletUriComponentsBuilder.fromRequest(request) .replaceQueryParam("accountId", "{id}") .build().expand("123").encode();

Links in Controllers

↓ java

@Controller @GetMapping("/path1/{val1}") public class C { @GetMapping("/path2/{val2}") public ModelAndView methodName() { } } var uriComponents = MvcUriComponentsBuilder .fromMethodName(C.class, "methodName", "value2") .buildAndExpand("value1");

↓ java

UriComponentsBuilder base = ServletUriComponentsBuilder.fromCurrentContextPath().path("/en"); MvcUriComponentsBuilder builder = MvcUriComponentsBuilder.relativeTo(base);

Asynchronous Requests

Configuration

요청 비동기 처리를 위해 서블릿 컨테이너 수준의 옵션을 설정해야 한다

Servlet Container

  • Java Config
  • AbstractAnnotationConfigDispatcherServletInitializer를 이용하면 된다

  • web.xml
    • DispatcherServlet, Filter 정의에 <async-supported>true</async-supported>
    • 필터 매핑에 <dispatcher>ASYNC</dispatcher>

Spring MVC

  • Java Config
  • WebMvcConfigurer의 configureAsyncSupport 재정의

  • XML Config
  • <mvc:annotation-driven> 안에 <async-support> 정의

    ↓ 설정 가능한 옵션들
  • timeout : 설정하지 않으면 서블릿 컨테이너의 것이 적용된다
  • timeout은 DeferredResult, ResponseBodyEmitter, SseEmitter, WebAsyncTask 각각에서도 설정 가능하다

  • AsyncTaskExecutor : 설정하지 않으면 SimpleAsyncTaskExecutor가 이용된다
  • DeferredResultProcessingInterceptor, CallableProcessingInterceptor

DeferredResult

별개 스레드에서 값을 쓴다

Callable

Callable에서 값을 반환한다

Processing

Interception

  • WebMvcConfigurer 예
  • DeferredResult#onTimeout 예
  • WebAsyncTask 예

Compared to WebFlux

  • Servlet 3.0에 추가된 비동기 처리는 Filter-Servlet 체인에서 벗어나 ─ 컨테이너 스레드는 해제된다 ─ 결과를 대기한다
  • 응답을 쓰기 위해 별도 스레드가 이용되며, IO 자체는 blocking이다

  • 반면 WebFlux는 Servlet API를 따르지 않으며, 최초 설계부터 비동기적이다
  • 요청 처리에 대한 모든 단계가 비동기적으로 처리될 수 있다. IO는 non-blocking이다

HTTP Streaming

ResponseBodyEmitter

각 객체들은 HttpMessageConverter를 통해 변환된다.

SseEmitter

ResponseBodyEmitter의 서브클래스로, Server-Sent Events를 지원한다.

StreamingResponseBody

Converter를 거치지 않고 OutputStream에 직접 쓴다

Disconnects

Servlet API는 클라이언트 연결 종료를 통지하지 않음에 유의

CORS

@CrossOrigin

    ↓ 기본 설정
  • 모든 origin 허용
  • 모든 header 허용
  • 매핑에 일치하는 모든 HTTP method 허용
  • allowedCredentials 비허용
  • maxAge 30분

Global Configuration

  • Java Configuration
  • ↓ java

    @Configuration @EnableWebMvc public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api1/**")...; registry.addMapping("/api2/**")...; } }
  • XML Configuration
  • ↓ xml

    <mvc:cors> <mvc:mapping path="/api/**" allowed-origins="https://domain1.com, https://domain2.com" allowed-methods="GET, PUT" allowed-headers="header1, header2, header3" exposed-headers="header1, header2" allow-credentials="true" max-age="123" /> <mvc:mapping path="/resources/**" allowed-origins="https://domain1.com" /> </mvc:cors>

CorsFilter

↓ java

var config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("https://domain1.com"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); var source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); var filter = new CorsFilter(source);

HTTP Caching

CacheControl

Cache-Control 헤더 정보를 의미한다

↓ java

// Cache for an hour - "Cache-Control: max-age=3600" CacheControl.maxAge(1, TimeUnit.HOURS); // Prevent caching - "Cache-Control: no-store" CacheControl.noStore();

Controllers

↓ java

@RequestMapping(...) public ResponseEntity<Something> method(...) { return ResponseEntity .ok() .cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS)) ...; }

View Technologies

Thymeleaf

Spring MVC 연동은 Thymeleaf 프로젝트가 관리한다. 다음 빈들이 필요하다 ─ ServletContextTemplateResolver, SpringTemplateEngine, ThymeleafViewResolver

FreeMarker

JSP, JSTL

View Resolvers

ResourceBundleViewResolver 예
InternalResourceViewResolver 예

JSPs versus JSTL

JSTL(JSP Standard Tag Library)을 이용할 때에는 뷰 클래스로 JstlView를 이용해야 한다

Spring’s JSP Tag Library

spring-webmvc.jar 안의 spring.tld에 스프리이 제공하는 태그 정의가 담겨 있다. 또는 package.description 참고

MVC Config

MVC 구성 켜기

  1. Java Configuration
  2. ↓ java

    @Configuration @EnableWebMvc public class WebConfig { }
  3. XML Configuration
  4. ↓ xml

    <mvc:annotation-driven>

Type Conversion

  • @Controller, @ControllerAdvice에만 적용되는 변환기를 등록하려면 Goto - DataBinder 참고
  • 전역 Java Configuration 예
  • XML에서는 FormattingConversionServiceFactoryBean 빈을 정의하면 된다

Validation

  • Goto - Bean Validation(예. Hibernate Validator)가 클래스패스에 존재한다면 LocalValidatorFactoryBean이 전역적으로 등록되어 @Valid, Validated를 컨트롤러 메서드에서 사용할 수 있다
  • 직접 LocalValidatorFactoryBean를 정의하는 경우, @Primary를 붙여 충돌을 피해야 한다

  • 전역 Validator 설정
    • Java Configuration
    • ↓ java

      @Configuration @EnableWebMvc public class WebConfig implements WebMvcConfigurer { @Override public Validator getValidator() { } }
    • XML Configuration
    • ↓ xml

      <mvc:annotation-driven validator="globalValidator"/>
  • 컨트롤러 수준 Validator 설정
  • ↓ java

    @Controller public class MyController { @InitBinder protected void initBinder(WebDataBinder binder) { } }

Interceptors

  1. Java Configuration 예
  2. addInterceptors(InterceptorRegistry)를 재정의하여 추가

  3. XML Configuration 예
  4. mvc:interceptors 태그 안에 작성

Content Types

  • ContentNegotiationConfigurer를 이용해 확장자별 응답 Content-Type을 조정할 수 있다
  • XML Configuration
  • ContentNegotiationManagerFactoryBean 빈 정의

Message Converters

  • WebMvcConfigurer#configureMessageConverters() 재정의
  • 이 메서드에서 컨버터를 추가하는 경우, Spring MVC 기본 컨버터들은 등록되지 않는다

  • WebMvcConfigurer#extendMessageConverters() 재정의
  • Spring MVC 기본 컨버터 등록 후 호출되어 사용자 컨버터 등록, 컨버터 수정 등 작업 가능

  • Spring MVC 기본 컨버터
    • StringHttpMessageConverter, FormHttpMessageConverter 등 기본 등록
    • 클래스패스에 존재하는 라이브러리에 맞춰 MappingJacksonHttpMessageConverter 등 등록

Static Resources

↓ java

@Configuration @EnableWebMvc public class AppConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(final ResourceHandlerRegistry registry) { registry.addResourceHandler("/img/**").addResourceLocations("file:static/img/","classpath:/static/img/").setCachePeriod(31556926); registry.addResourceHandler("/css/**").addResourceLocations("classpath:/static/css/"); registry.addResourceHandler("/js/**").addResourceLocations("classpath:/static/js/"); } }

↓ xml

<mvc:resources mapping="/resources/**" location="/public, classpath:/static/" cache-period="31556926" />

Default Servlet

  • DispatcherServlet이 "/" ─ 즉, 모든 요청을 받으면서도 static 리소스 또한 처리할 수 있다
  • Java Configuration
  • ↓ java

    @Configuration @EnableWebMvc public class WebConfig implements WebMvcConfigurer { @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { configurer.enable("myCustomDefaultServlet"); } }
  • XML Configuration
  • ↓ xml

    <mvc:default-servlet-handler default-servlet-name="myCustomDefaultServlet"/>

Path Matching

  • PathMatchConfigurer.html를 이용해 여러 옵션을 설정할 수 있다
  • Java Configuration
  • ↓ java

    @Configuration @EnableWebMvc public class WebConfig implements WebMvcConfigurer { @Override public void configurePathMatch(PathMatchConfigurer configurer) { configurer .setUseTrailingSlashMatch(false) .setUseRegisteredSuffixPatternMatch(true); } }
  • XML Configuration
  • ↓ xml

    <mvc:annotation-driven> <mvc:path-matching trailing-slash="false" registered-suffixes-only="true"/> </mvc:annotation-driven>

Advanced Java Config

@EnableWebMvc는 DelegatingWebMvcConfiguration를 임포트하며, 이는 아래 작업을 수행한다

  • Spring MVC 기본 설정
  • WebMvcConfigurer 탐지 후 각 설정 메서드 호출

고급 설정을 원하는 경우 @EnableWebMvc를 제거하고 DelegatingWebMvcConfiguration를 직접 상속하면 된다

REST Clients

RestTemplate

동기적인 HTTP 요청을 수행한다. Spring 5.0부터는 WebClient를 대신 이용할 것이 권장된다

WebClient Since 5.0

  • Non-blocking, 리액티브 HTTP 클라이언트
  • 동기적인 요청도 가능

  • 함수형 스타일로, Java 8 람다 사용 가능
  • 테스트를 위한 WebTestClient도 존재

WebSockets

Introduction to WebSocket

  • WebSocket 프로토콜은 1개의 클라이언트-서버 TCP 연결 위에서 전이중, 양방향 통신 채널을 수립하는 표준을 제공한다
  • 저수준 전송 프로토콜로, 클라이언트-서버 상호 합의된 임의 형태의 데이터 전송이 가능하다
  • Sec-WebSocket-Protocol 헤더로 미리 전송 형태를 합의할 수 있다

  • HTTP/1.1 업그레이드 기능을 이용해 기존 HTTP/HTTPS를 웹 소켓 연결 WS/WSS로 그대로 전환
  • 사용하던 80/443포트 그대로 연결하므로 방화벽의 차단에도 안전. 프록시 서버가 HTTP 업그레이드를 처리하지 않는다면 WSS를 이용

  • WebSocket 서버 앞에 Nginx와 같은 웹서버가 존재하는 경우, 업그레이드 요청을 전달하도록 설정 필요
  • 마찬가지로 클라우드 환경에서도 별도 설정이 필요할 수 있다

  • 외부 프록시가 장기간 idle한 연결을 강제로 끊을 수도 있음에 유의

WebSocket API Since JavaEE 7

javax.websocket : 클라이언트/서버 공통 기능

  • WebSocketContainer
  • 웹소켓 클라이언트 기능. connectToServer()

  • ContainerProvider
  • WebSocketContainer 인스턴스 획득을 위한 getWebSocketContainer()

  • @ClientEndpoint
  • POJO가 클라이언트측 웹소켓임을 나타낸다. @OnOpen, @OnClose, @OnError, @OnMessage 메서드를 가질 수 있다

  • @OnOpen, @OnClose
  • 선택적 Session 매개변수, 선택적 EndpointConfig 매개변수, 0 ~ n개 @PathParam String 매개변수를 가질 수 있다

  • @OnError
  • 선택적 Session 매개변수, Throwable 매개변수, 0 ~ n개 @PathParam String 매개변수를 가질 수 있다

  • @interface OnMessage
  • 선택적 Session 매개변수, 0 ~ n개 @PathParam String 매개변수, 아래 중 하나의 메시지 매개변수를 가질 수 있다

    1. 텍스트 메시지 처리
      • 전체 메시지 String
      • 메시지로부터 변환되는 Java primitive or class
      • 메시지 청크 String and boolean pair : 마지막 메시지는 true
      • 전체 메시지 Reader
      • Decoder.Text or Decoder.TextStream
    2. 이진 메시지 처리
      • 전체 메시지 byte[] or ByteBuffer
      • 메시지 청크 (byte[] or ByteBuffer) and boolean pair : 마지막 메시지는 true
      • 전체 메시지 InputStream
      • Decoder.Binary or Decoder.BinaryStream
    3. 퐁 메시지 처리 : PongMessage

javax.websocket.server : 서버 전용 기능

  • ServerContainer extends WebSocketContainer
  • ServerEndpointConfig 인스턴스 또는 @ServerEndpoint 클래스 등록

Spring WebSocket API

WebSocketHandler

  • 서버측 WebSocket을 생성하려면 WebSocketHandler, TextWebSocketHandler, BinaryWebSocketHandler를 상속하면 된다
  • WebSocketHandler 등록 - Java Configuration
  • ↓ java

    @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myHandler, "/myHandler"); } }
  • WebSocketHandler 등록 - XML Configuration
  • ↓ xml

    <websocket:handlers> <websocket:mapping path="/myHandler" handler="myHandler"/> </websocket:handlers>
  • 동시성이 요구되는 경우, ConcurrentWebSocketSessionDecorator를 이용해 한 번에 한 스레드만 WebSocketSession를 이용해 전송하게 할 수 있다
  • WebSocketHandlerDecorator를 이용해 WebSocketHandler를 장식할 수 있다
  • ExceptionWebSocketHandlerDecorator는 임의 WebSocketHandler 메서드에서 처리되지 않은 예외 발생 시 1011 상태로 세션을 종료한다

WebSocket Handshake

  • HandshakeInterceptor를 이용해 WebSocket 연결 수립 전후 작업을 정의할 수 있다
  • HttpSessionHandshakeInterceptor 등록 - Java Configuration
  • ↓ java

    @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myHandler, "/myHandler").addInterceptors(new HttpSessionHandshakeInterceptor()); } }
  • HttpSessionHandshakeInterceptor 등록 - XML Configuration
  • ↓ xml

    <websocket:handlers> <websocket:mapping path="/myHandler" handler="myHandler"/> <websocket:handshake-interceptors> <bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/> </websocket:handshake-interceptors> </websocket:handlers>
  • 연결 수립 자체에 참여해야 한다면 DefaultHandshakeHandler를 상속하면 된다

Deployment

Spring MVC를 사용하지 않더라도, WebSocketHttpRequestHandler를 이용해 웹소켓 요청을 처리할 수 있다

Server Configuration

  • 버퍼 사이즈, idle timeout 등의 설정이 가능하다
  • Tomcat, WildFly, GlassFish 설정을 위해 ServletServerContainerFactoryBean를 정의할 수 있다
  • Jetty 설정을 위해 WebSocketServerFactory 인스턴스를 DefaultHandshakeHandler에 삽입할 수 있다
  • ↓ java

    @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(echoWebSocketHandler(), "/echo").setHandshakeHandler(handshakeHandler()); } @Bean public DefaultHandshakeHandler handshakeHandler() { WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER); policy.setInputBufferSize(8192); policy.setIdleTimeout(600000); return new DefaultHandshakeHandler(new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy))); } }
  • 클라이언트 설정을 위해 WebSocketContainerFactoryBean (XML) 또는 ContainerProvider.getWebSocketContainer() (Java configuration)를 사용할 수 있다

Allowed Origins

  • Since 4.1.5 : 기본적으로 같은 origin 요청만 허용
  • X-Frame-Options 헤더가 SAMEORIGIN으로 설정된다

  • 모든 origin 허용 : *로 설정
  • 일부 origin 허용 : http://, https://로 시작하는 origin들을 설정
  • ↓ Java Configuration

    @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myHandler(), "/myHandler").setAllowedOrigins("https://mydomain.com"); } }

    ↓ XML Configuration

    <websocket:handlers allowed-origins="https://mydomain.com"> <websocket:mapping path="/myHandler" handler="myHandler" /> </websocket:handlers>

SockJS Fallback

Overview

SockJS는 웹소켓 연결에 실패하더라도 HTTP 기반의 에뮬레이션을 통해 애플리케이션 API를 지원한다

    SockJS의 구성
  • SockJS Protocol
  • SockJS JavsScript client
  • SockJS server implementation : spring-websocket이 이에 해당
  • spring-websocket 4.1부터는 SockJS Java client도 포함

SockJS 클라이언트는 GET /info 요청으로 서버 정보를 획득한 후, 전송 수단을 선택한다 가능한 경우 WebSocket을 사용하고, 그 외엔 브라우저 별로 지원되는 HTTP 스트리밍, 최후에는 HTTP (long) polling을 이용한다
각 브라우저별 지원 사항은 #supported-transports-by-browser 참고. 각 전송 방법의 차이는 https://spring.io/blog/2012/05/08/spring-mvc-3-2-preview-techniques-for-real-time-updates/ 참고

모든 요청 URL은 아래의 꼴을 따른다

↓ text

https://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}
  • server-id : 요청을 클러스터로 묶을 경우에만 사용
  • session-id : SockJS 세션
  • transport : 전송 타입(websocket, xhr-streaming)

서버는 세션 수립 후 'o'(open frame), 25초(default) 동안 idle이면 'h'(heartbeat frame), 종결 시 'c'(close frame)를 전송한다

Enabling SockJS

↓ Java Configuration

@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myHandler(), "/myHandler").withSockJS(); } }

↓ XML Configuration

<websocket:handlers> <websocket:mapping path="/myHandler" handler="myHandler"/> <websocket:sockjs/> </websocket:handlers>

Spring MVC를 사용하지 않더라도, SockJsHttpRequestHandler를 이용해 SockJS를 이용할 수 있다

SockJS and CORS

CORS가 이미 허용된 경우, CORS를 허용하지 않는 경우(AbstractSockJsService#setSuppressCors(true)) 아래 설정을 하지 않는다

  • Access-Control-Allow-Origin : 요청 Origin 헤더로 설정
  • Access-Control-Allow-Credentials : true
  • Access-Control-Request-Headers : 요청 헤더 값으로 설정
  • Access-Control-Allow-Methods : 지원 타입(TransportType)으로 설정
  • Access-Control-Max-Age : 31536000(1년)

CORS 설정에서 WebSocket endpoint prefix를 제외하여 SockJsService가 처리하도록 하는 걸 고려해볼 것

SockJsClient

브라우저 없이 직접 WebSocket 서버에 연결하는 클라이언트를 제공한다. 지원 타입은 websocket, xhr-streaming, xhr-polling

  • websocket : Transport 이용
  • WebSocketTransport의 생성에는 StandardWebSocketClient(JSR-356), JettyWebSocketClient(Jetty 9+), 임의 WebSocketClient 구현체 사용 가능

  • xhr : XhrTransport 이용
    • RestTemplateXhrTransport : HTTP 요청에 Spring RestTemplate 사용
    • JettyXhrTransport : HTTP 요청에 Jetty HttpClient 사용

    ↓ SockJsClient 생성 및 연결

    var transports = new ArrayList<Transport>(2); transports.add(new WebSocketTransport(new StandardWebSocketClient())); transports.add(new RestTemplateXhrTransport()); var sockJsClient = new SockJsClient(transports); sockJsClient.doHandshake(new MyWebSocketHandler(), "ws://example.com:8080/sockjs");

STOMP

WebSocket 통신은 text 또는 binary 메시지 전송으로 이루어지며, 원활한 통신을 위해 sub-protocol을 이용할 수 있다

Overview

STOMP(Simple Text Oriented Messaging Protocol)는 양방향 네트워크 상에서 메시지 전송을 위한 최소 규격을 정의한다

↓ STOMP Frame

COMMAND header1:value1 header2:value2 Body^@
    Command 종류
  • SEND : 클라이언트 -> 서버 전송
  • SUBSCRIBE : 클라이언트 -> 서버 구독
  • MESSAGE : 서버 -> 클라이언트 브로드캐스트

이하 생략 - 사용하게 되면 정리

Spring WebSocket 예

  • javax.websocket-api, spring-websocket 필요
  • @EnableWebSocket + WebSocketHandler 예
  • @ServerEndpoint 예