- 웹
- 8.1. 서블릿 웹 애플리케이션(Servlet Web Applications)
- 8.1.1. 스프링 웹 MVC 프레임워크(The “Spring Web MVC Framework”) Spring MVC 자동 구성(Spring MVC Auto-configuration) Http메세지컨버터(HttpMessageConverters) 메세지코드리졸버(MessageCodesResolver) 정적 콘텐츠(Static Content) 웰컴 페이지(Welcome Page) 커스텀 파비콘(Custom Favicon) 패스 매칭 앤 컨텐츠 협상(Path Matching and Content Negotiation) 컨피규러블웹바인딩이니셜라이저(ConfigurableWebBindingInitializer) 템플릿 엔진(Template Engines) 에러 핸들링(Error Handling) CORS 지원(CORS Support)
- 8.1.2. JAX-RS 및 저지(JAX-RS and Jersey)
- 8.1.3. 임베디드 서블릿 컨테이너 지원(Embedded Servlet Container Support) 서블릿, 필터, 리스너(Servlets, Filters, and Listeners)Servlets, Filters, and Listeners 서블릿 컨텍스트 초기화(Servlet Context Initialization) 서블릿웹서버애플리케이션컨텍스트(The ServletWebServerApplicationContext) 임베디드 서블릿 컨테이너 커스텀(Customizing Embedded Servlet Containers) JSP 제한사항(JSP Limitations)
- 8.2. 리액티브 웹 애플리케이션(Reactive Web Applications)
- 8.2.1. 스프링 웹플럭스 프레임워크(The “Spring WebFlux Framework”) 스프링 웹 플럭스 자동구성(Spring WebFlux Auto-configuration) HttpMessageReaders 및 HttpMessageWriters가 포함된 HTTP 코덱(HTTP Codecs with HttpMessageReaders and HttpMessageWriters) 정적 콘텐츠(Static Content) 웰컴 페이지(Welcome Page) 템플릿 엔진(Template Engines) 에러 핸들링(Error Handling) 웹 필터(Web Filters)
- 8.2.2. 임베디드 리액티브 서버 지원(Embedded Reactive Server Support)
- 8.2.3. 리액티브 서버 리소스 컨피규레이션(Reactive Server Resources Configuration)
- 8.3. 정상 종료(Graceful Shutdown)
- 8.4. 스프링 시큐리티(Spring Security)
- 8.4.1. MVC 시큐리티(MVC Security)
- 8.4.2. 웹플럭스 시큐리티(WebFlux Security)
- 8.4.3. 오어스2(OAuth2) 클라이언트(Client) 리소스 서버(Resource Server) Authorization Server
- 8.4.4. 샘엘(SAML) 2.0 신뢰 당사자(Relying Party)
- 8.5. 스프링 세션(Spring Session)
- 8.6. 스프링을 위한 그래프QL(Spring for GraphQL)
- 8.6.1. 그래프QL 스키마(GraphQL Schema)
- 8.6.2. 그래프QL 런타임와이어링(GraphQL RuntimeWiring)
- 8.6.3. 쿼리dsl과 쿼리예제 리포지터리 지원(Querydsl and QueryByExample Repositories Support)
- 8.6.4. 전송(Transports) HTTP와 웹소켓(HTTP and WebSocket) R소켓(RSocket)
- 8.6.5. 예외 핸들링(Exception Handling)
- 8.6.6. 그래피QL과 스키마 프린터(GraphiQL and Schema printer)
- 8.7. 스프링 헤이티오스(Spring HATEOAS)
- 8.8. 다음에 읽을 내용(What to Read Next) ***
- 8.1. 서블릿 웹 애플리케이션(Servlet Web Applications)
8. 웹(Web)
스프링 부트는 웹 애플리케이션 개발에 매우 적합하다. 임베디드 톰캣, 제티(Jetty), 언더토우(Undertow) 또는 네티(Netty)를 사용하여 독립형 HTTP 서버를 생성할 수 있다. 대부분의 웹 애플리케이션은 spring-boot-starter-web
모듈을 사용하여 빠르게 시작하고 실행한다. spring-boot-starter-webflux
모듈을 사용하여 리액티브 웹 애플리케이션을 구축하도록 선택할 수도 있다.
아직 스프링 부트 웹 애플리케이션을 개발하지 않았다면 시작하기(Getting Started) 장의 예제인 “Hello World!”를 따라할 수 있다.
8.1. 서블릿 웹 애플리케이션(Servlet Web Applications)
서블릿 기반 웹 애플리케이션을 구축하려는 경우 스프링MVC 또는 저지(Jersey)에 대한 스프링 부트 자동 구성(auto-configuration)을 활용할 수 있다.
8.1.1. 스프링 웹 MVC 프레임워크(The “Spring Web MVC Framework”)
스프링 웹 MVC 프레임워크(종종 “스프링 MVC”라고도 함)는 풍부한 “모델 뷰 컨트롤러” 웹 프레임워크이다. 스프링 MVC를 사용하면 들어오는 HTTP 요청(request)을 처리하기 위해 특별한 @Controller
또는 @RestController
빈을 생성할 수 있다. 컨트롤러의 메서드는 @RequestMapping
어노테이션을 사용하여 HTTP에 매핑된다.
다음 코드는 제이슨(JSON) 데이터를 제공하는 일반적인 @RestController
를 보여준다.
자바
import java.util.List;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/users")
public class MyRestController {
private final UserRepository userRepository;
private final CustomerRepository customerRepository;
public MyRestController(UserRepository userRepository, CustomerRepository customerRepository) {
this.userRepository = userRepository;
this.customerRepository = customerRepository;
}
@GetMapping("/{userId}")
public User getUser(@PathVariable Long userId) {
return this.userRepository.findById(userId).get();
}
@GetMapping("/{userId}/customers")
public List<Customer> getUserCustomers(@PathVariable Long userId) {
return this.userRepository.findById(userId).map(this.customerRepository::findByUser).get();
}
@DeleteMapping("/{userId}")
public void deleteUser(@PathVariable Long userId) {
this.userRepository.deleteById(userId);
}
}
코틀린
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/users")
class MyRestController(private val userRepository: UserRepository, private val customerRepository: CustomerRepository) {
@GetMapping("/{userId}")
fun getUser(@PathVariable userId: Long): User {
return userRepository.findById(userId).get()
}
@GetMapping("/{userId}/customers")
fun getUserCustomers(@PathVariable userId: Long): List<Customer> {
return userRepository.findById(userId).map(customerRepository::findByUser).get()
}
@DeleteMapping("/{userId}")
fun deleteUser(@PathVariable userId: Long) {
userRepository.deleteById(userId)
}
}
기능적 변형인 “WebMvc.fn”은 다음 예제와 같이 라우팅 구성을 요청의 실제 처리와 분리한다.
자바
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.function.RequestPredicate;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;
import static org.springframework.web.servlet.function.RequestPredicates.accept;
import static org.springframework.web.servlet.function.RouterFunctions.route;
@Configuration(proxyBeanMethods = false)
public class MyRoutingConfiguration {
private static final RequestPredicate ACCEPT_JSON = accept(MediaType.APPLICATION_JSON);
@Bean
public RouterFunction<ServerResponse> routerFunction(MyUserHandler userHandler) {
return route()
.GET("/{user}", ACCEPT_JSON, userHandler::getUser)
.GET("/{user}/customers", ACCEPT_JSON, userHandler::getUserCustomers)
.DELETE("/{user}", ACCEPT_JSON, userHandler::deleteUser)
.build();
}
}
코틀린
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.MediaType
import org.springframework.web.servlet.function.RequestPredicates.accept
import org.springframework.web.servlet.function.RouterFunction
import org.springframework.web.servlet.function.RouterFunctions
import org.springframework.web.servlet.function.ServerResponse
@Configuration(proxyBeanMethods = false)
class MyRoutingConfiguration {
@Bean
fun routerFunction(userHandler: MyUserHandler): RouterFunction<ServerResponse> {
return RouterFunctions.route()
.GET("/{user}", ACCEPT_JSON, userHandler::getUser)
.GET("/{user}/customers", ACCEPT_JSON, userHandler::getUserCustomers)
.DELETE("/{user}", ACCEPT_JSON, userHandler::deleteUser)
.build()
}
companion object {
private val ACCEPT_JSON = accept(MediaType.APPLICATION_JSON)
}
}
자바
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
@Component
public class MyUserHandler {
public ServerResponse getUser(ServerRequest request) {
...
return ServerResponse.ok().build();
}
public ServerResponse getUserCustomers(ServerRequest request) {
...
return ServerResponse.ok().build();
}
public ServerResponse deleteUser(ServerRequest request) {
...
return ServerResponse.ok().build();
}
}
코틀린
import org.springframework.stereotype.Component
import org.springframework.web.servlet.function.ServerRequest
import org.springframework.web.servlet.function.ServerResponse
@Component
class MyUserHandler {
fun getUser(request: ServerRequest?): ServerResponse {
return ServerResponse.ok().build()
}
fun getUserCustomers(request: ServerRequest?): ServerResponse {
return ServerResponse.ok().build()
}
fun deleteUser(request: ServerRequest?): ServerResponse {
return ServerResponse.ok().build()
}
}
스프링 MVC는 코어 스프링 프레임워크의 일부이며 자세한 정보는 레퍼런스 문서에서 확인할 수 있다. spring.io/guides
에는 스프링 MVC를 다루는 여러 가이드도 있다.
라우터 정의를 모듈화하기 위해 원하는 만큼 라우터펑션(RouterFunction)
빈을 정의할 수 있다. 우선순위를 적용해야 하는 경우 빈의 순위를 정할 수 있다.
스프링 MVC 자동 구성(Spring MVC Auto-configuration)
스프링 부트는 대부분의 애플리케이션에서 잘 작동하는 스프링 MVC에 대한 자동 구성(auto-configuration)을 제공한다.
자동 구성은 스프링의 기본값 위에 다음 기능을 추가한다.
콘텐트네고시에이팅뷰리졸버(ContentNegotiatingViewResolver)
및빈네임뷰리졸버(BeanNameViewResolver)
빈이 포함된다.WebJars
지원을 포함하여, 스태틱(Static) 리소스 제공을 지원한다(문서의 뒷부분에서 설명).컨버터(Converter)
,제네릭컨버터(GenericConverter)
및포매터(Formatter)
빈의 자동 등록.Http메세지컨버터(HttpMessageConverters)
지원(문서의 뒷부분에서 설명)메세지코드리졸버(MessageCodesResolver)
자동 등록(문서의 뒷부분에서 설명)- 스태틱(Static) index.html 지원.
컨피규러블웹바인딩이셜라이저(ConfigurableWebBindingInitializer)
빈의 자동 사용(이 문서의 뒷부분에서 설명)
이러한 스프링 부트 MVC 커스텀(customization)을 유지하고 더 많은 MVC 커스텀(인터셉터(interceptor), 포맷터(formatter), 뷰 컨트롤러(view controller) 및 기타 기능)를 수행하려는 경우 @EnableWebMvc
없이 웹Mvc컨피규어러(WebMvcConfigurer) 타입의 고유한 @Configuration
클래스를 추가할 수 있다.
리퀘스트매핑핸들러매핑(RequestMappingHandlerMapping)
, 리퀘스트매핑핸들러어댑터(RequestMappingHandlerAdapter)
또는 익셉션핸들러익셉션리졸버(ExceptionHandlerExceptionResolver)
의 커스텀 인스턴스를 제공하고 여전히 스프링 부트 MVC 커스텀를 유지하려는 경우 웹Mvc레지스트레이션(WebMvcRegistrations) 타입의 빈을 선언하고 이를 사용하여 해당 컴포넌트의 커스텀 인스턴스를 제공할 수 있다.
스프링 MVC를 완전히 제어하려면, @EnableWebMvc
어노테이션이 달린 고유한 @Configuration
을 추가하거나 @EnableWebMvc
의 자바독(Javadoc)에 설명된 대로 고유한 @Configuration
어노테이션이 달린 델리게이팅웹Mvc컨피규레이션(DelegatingWebMvcConfiguration)
을 추가할 수 있다.
노트
스프링 MVC는 application.properties
또는 application.yaml
파일의 값을 변환하는 데 사용되는 것과 다른 컨버전서비스(ConversionService)
를 사용한다. 이는 피리어드(Period), 듀레이션(Duration) 및 데이터사이즈(DataSize) 컨버터를 사용할 수 없으며 @DurationUnit
및 @DataSizeUnit
어노테이션이 무시된다는 의미다.
스프링 MVC에서 사용되는 컨버전서비스(ConversionService)를 커스텀하려면 addFormatters
메소드와 함께 웹Mvc컨피규어러(WebMvcConfigurer)
빈을 제공할 수 있다. 이 메서드에서 원하는 컨버터를 등록하거나 애플리케이션컨버전서비스(ApplicationConversionService)
에서 사용 가능한 스태틱 메서드에 위임할 수 있다. ***
Http메시지컨버터(HttpMessageConverters)
스프링 MVC는 Http메세지컨버터(HttpMessageConverter) 인터페이스를 사용하여 HTTP 요청(request)과 응답(responses)을 변환(convert)한다. 기본적으로 합리적인 기본값이 포함되어 있다. 예를 들어 객체는 자동으로 JSON(잭슨(Jackson) 라이브러리 사용) 또는 XML(사용 가능한 경우 잭슨(Jackson) XML 확장 사용 또는 잭슨(Jackson) XML 확장을 사용할 수 없는 경우 JAXB 사용)로 변환할 수 있다. 기본적으로 문자열은 UTF-8로 인코딩된다.
컨버터를 추가하거나 커스텀해야 하는 경우 다음 목록에 표시된 것처럼 스프링 부트의 Http메세지컨버터(HttpMessageConverters) 클래스를 사용할 수 있다.
자바
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
@Configuration(proxyBeanMethods = false)
public class MyHttpMessageConvertersConfiguration {
@Bean
public HttpMessageConverters customConverters() {
HttpMessageConverter<?> additional = new AdditionalHttpMessageConverter();
HttpMessageConverter<?> another = new AnotherHttpMessageConverter();
return new HttpMessageConverters(additional, another);
}
}
코틀린
import org.springframework.boot.autoconfigure.http.HttpMessageConverters
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.converter.HttpMessageConverter
@Configuration(proxyBeanMethods = false)
class MyHttpMessageConvertersConfiguration {
@Bean
fun customConverters(): HttpMessageConverters {
val additional: HttpMessageConverter<*> = AdditionalHttpMessageConverter()
val another: HttpMessageConverter<*> = AnotherHttpMessageConverter()
return HttpMessageConverters(additional, another)
}
}
컨텍스트에 존재하는 모든 Http메세지컨버터(HttpMessageConverter) 빈은 컨버터 목록에 추가된다. 동일한 방식으로 기본 컨버터를 오버라이드할 수도 있다.
메세지코드리졸버(MessageCodesResolver)
스프링 MVC에는 바인딩 오류로부터 오류 메시지를 렌더링하기 위한 오류 코드(메세지코드리졸버(MessageCodesResolver))를 생성하는 전략이 있다. spring.mvc.message-codes-resolver-format
프로퍼티 PREFIX_ERROR_CODE
또는 POSTFIX_ERROR_CODE
를 설정하면 스프링 부트가 자동으로 하나를 생성한다(DefaultMessageCodesResolver.Format
의 이넘 참조).
Spring MVC has a strategy for generating error codes for rendering error messages from binding errors: MessageCodesResolver. If you set the spring.mvc.message-codes-resolver-format property PREFIX_ERROR_CODE or POSTFIX_ERROR_CODE, Spring Boot creates one for you (see the enumeration in DefaultMessageCodesResolver.Format).
정적 컨텐츠(Static Content)
기본적으로 스프링 부트는 클래스패스의 /static
(또는 /public
, /resources
또는 /META-INF/resources
)이라는 디렉토리나 서블릿컨텍스트(ServletContext)
루트에서 스태틱 콘텐츠를 제공한다. 이는 스프링 MVC의 리소스Http리퀘스트핸들러(ResourceHttpRequestHandler)
를 사용하므로 사용자는 자체 웹Mvc컨피규어러(WebMvcConfigurer)
를 추가하고 addResourceHandlers
메서드를 오버라이드하여 해당 동작을 수정할 수 있다.
독립형(stand-alone) 웹 애플리케이션에서는 컨테이너의 기본 서블릿이 활성화되지 않는다. server.servlet.register-default-servlet
프로퍼티를 사용하여 활성화할 수 있다.
기본 서블릿은 스프링이 처리하지 않기로 결정한 경우 서블릿컨텍스트(ServletContext)의 루트에서 콘텐츠를 제공하는 폴백(fallback) 역할을 한다. 기본 MVC 구성을 수정하지 않는 한 대부분은 발생하지 않는다. 왜냐하면 스프링은 항상 디스패처서블릿(DispatcherServlet)
을 통해 요청(request)을 처리할 수 있기 때문이다.
기본적으로 리소스는 /**
에 매핑되지만 spring.mvc.static-path-pattern
프로퍼티을 사용하여 이를 조정할 수 있다. 예를 들어 모든 리소스를 /resources/**
에 재배치하는 방법은 다음과 같다.
프로퍼티스
spring.mvc.static-path-pattern=/resources/**
Yaml
spring:
mvc:
static-path-pattern: "/resources/**"
spring.web.resources.static-location
프로퍼티스를 사용하여 스태틱 리소스 위치를 커스텀할 수도 있다(기본값을 디렉토리 위치 리스트로 대체). 루트 서블릿 컨텍스트 패스인 “/”도 해당위치로 추가된다.
앞서 언급한 “표준” 스태틱 리소스 위치 외에도 [Webjars
컨텐트]에 대한 특별한 경우가 있다. 기본적으로 /webjars/**
에 패스가 있는 모든 리소스는 Webjars
포맷으로 패키지된 경우 jar 파일에서 제공된다. 패스는 spring.mvc.webjars-path-pattern
프로퍼티를 사용하여 커스텀할 수 있다.
애플리케이션이 jar로 패키지된 경우 src/main/webapp
디렉토리를 사용하지 말자. 이 디렉토리는 일반적인 표준이지만 war
패키징에서만 작동하며 jar을 생성하는 경우 대부분의 빌드 도구에서 자동으로 무시된다.
스프링 부트는 또한 스프링 MVC에서 제공하는 고급 리소스 처리 기능을 지원하여 캐시 버스팅 스태틱 리소스(cache-busting static resources) 또는 Webjar
버전에 구애받지 않는 URL 사용과 같은 사례를 허용한다.
Webjars
에 대해 버전에 구애받지 않는 URL을 사용하려면 webjars-locator-core
의존성을 추가하자. 그런 다음 Webjar
을 선언하자. 예를 들어 jQuery
를 사용하면 “/webjars/jquery/jquery.min.js”를 추가했을 때 “/webjars/jquery/x.y.z/jquery.min.js”가 된다. 여기서 x.y.z는 Webjar 버전이다.
제이보스(JBoss)를 사용하는 경우 webjars-locator-core
대신 webjars-locator-jboss-vfs
의존성성을 선언해야 한다. 그렇지 않으면 모든 Webjar가 404로 처리된다.
캐시 무효화를 사용하기 위해 다음 구성은 모든 스태틱 리소스에 대한 캐시 무효화 솔루션을 구성하여와 같은 콘텐츠 해시를 URL에 효과적으로 추가한다.
프로퍼티스(Properties)
spring.web.resources.chain.strategy.content.enabled=true
spring.web.resources.chain.strategy.content.paths=/**
Yaml
spring:
web:
resources:
chain:
strategy:
content:
enabled: true
paths: "/**"
Thymeleaf 및 FreeMarker에 대해 자동으로 구성된 리소스Url인코딩필터(ResourceUrlEncodingFilter)
덕분에 리소스에 대한 링크는 런타임 시 템플릿에 재작성된다. JSP를 사용할 때 이 필터를 수동으로 선언해야 한다. 다른 템플릿 엔진은 현재 자동으로 지원되지 않지만 커스텀 템플릿 macros/helpers 및 리소스Url프로바이더(ResourceUrlProvider)
를 사용할 수 있다.
예를 들어 자바스크립트 모듈 로더를 사용하여 리소스를 동적으로 로드할 때 파일명을 바꾸는 것은 옵션이 아니다. 그렇기 때문에 다른 전략도 지원되고 결합될 수 있다. “고정(fixed)” 전략은 다음 예와 같이 파일명을 변경하지 않고 URL에 스태틱 버전 문자열을 추가한다.
프로퍼티스(Properties)
spring.web.resources.chain.strategy.content.enabled=true
spring.web.resources.chain.strategy.content.paths=/**
spring.web.resources.chain.strategy.fixed.enabled=true
spring.web.resources.chain.strategy.fixed.paths=/js/lib/
spring.web.resources.chain.strategy.fixed.version=v12
Yaml
spring:
web:
resources:
chain:
strategy:
content:
enabled: true
paths: "/**"
fixed:
enabled: true
paths: "/js/lib/"
version: "v12"
이 구성을 사용하면 “/js/lib/” 아래에 있는 자바스크립트 모듈은 고정 버전 관리 전략(“/v12/js/lib/mymodule.js”)을 사용하는 반면, 다른 리소스는 여전히 컨텐트 전략()을 사용한다.
지원되는 추가 옵션은 WebProperties.Resources
를 참고하자.
이 기능은 블로그 게시물과 스프링 프레임워크의 레퍼런스 문서에 자세히 설명되어 있다.
웰컴 페이지(Welcome Page)
스프링 부트는 스태틱 및 템플릿 웰컴 페이지를 모두 지원한다. 먼저 구성된 스태틱 콘텐츠 위치에서 index.html 파일을 찾는다. 찾을 수 없으면 인덱스 템플릿을 찾는다. 둘 중 하나가 발견되면 자동으로 애플리케이션의 웰컴 페이지로 사용된다.
커스텀 파비콘(Custom Favicon)
다른 정적 리소스와 마찬가지로, 스프링 부트는 구성된 스태틱 콘텐츠 위치에서 favicon.ico를 확인한다. 해당 파일이 있으면 자동으로 애플리케이션의 파비콘으로 사용된다.
패스 매칭 앤 컨텐트 협상(Path Matching and Content Negotiation)
스프링 MVC는 요청(request) 패스를 보고 이를 애플리케이션에 정의된 매핑(예: 컨트롤러 메서드의 @GetMapping 어노테이션)과 일치시켜 들어오는 HTTP 요청(request)을 핸들러에 매핑할 수 있다.
스프링 부트는 기본적으로 접미사 패턴 일치를 비활성화하도록 한다. 이는 “GET /projects/spring-boot.json”과 같은 요청이 @GetMapping(“/projects/spring-boot”) 매핑과 일치하지 않음을 의미한다. 이는 스프링 MVC 애플리케이션의 모범 사례로 여겨진다. 이 기능은 과거에 적절한 “Accept” 요청 헤더를 보내지 않은 HTTP 클라이언트에 주로 유용했다. 우리는 올바른 콘텐츠 타입을 클라이언트에 보내야 했다. 요즘에는 컨텐트 협상이 훨씬 더 안정적이다.
적절한 “Accept” 요청 헤더를 일관되게 보내지 않는 HTTP 클라이언트를 처리하는 다른 방법이 있다. 접미사 일치를 사용하는 대신 쿼리 파라미터를 사용하여 “GET /projects/spring-boot?format=json”과 같은 요청이 @GetMapping(“/projects/spring-boot”)에 매핑되도록 할 수 있다.
프로퍼티스(Properties)
spring.mvc.contentnegotiation.favor-parameter=true
Yaml
spring:
mvc:
contentnegotiation:
favor-parameter: true
또는 다른 파라미터명을 사용하려는 경우.
프로퍼티스(Properties)
spring.mvc.contentnegotiation.favor-parameter=true
spring.mvc.contentnegotiation.parameter-name=myparam
Yaml
spring:
mvc:
contentnegotiation:
favor-parameter: true
parameter-name: "myparam"
대부분의 표준 미디어 타입(media types)은 기본적으로 지원되지만, 새로운 타입을 정의할 수도 있다.
프로퍼티스(Properties)
spring.mvc.contentnegotiation.media-types.markdown=text/markdown
Yaml
spring:
mvc:
contentnegotiation:
media-types:
markdown: "text/markdown"
스프링 프레임워크 5.3부터 스프링 MVC는 요청 패스를 컨트롤러 핸들러와 일치시키기 위한 여러 구현 전략을 지원한다. 이전에는 앤트패스매처(AntPathMatcher)
전략만 지원했지만 이제는 패스패턴파서(PathPatternParser)
도 제공한다. 이제 스프링 부트는 새로운 전략을 선택하고 선택할 수 있는 구성 프로퍼티를 제공한다.
프로퍼티스(Properties)
spring.mvc.pathmatch.matching-strategy=path-pattern-parser
Yaml
spring:
mvc:
pathmatch:
matching-strategy: "path-pattern-parser"
이 새로운 구현을 고려해야 하는 이유에 대한 자세한 내용은 전용 블로그 게시물을 참고하자.
패스패턴파서(PathPatternParser)
는 최적화된 구현이지만 일부 패스 패턴 변형(path patterns variants)의 사용을 제한한다. 서블릿 접두사(spring.mvc.servlet.path)를 사용하여 디스패처서블릿(DispatcherServlet)
을 매핑하거나 접미사 패턴을 매핑하는 것과 호환되지 않는다.
기본적으로, 스프링 MVC는 요청에 대한 핸들러를 찾을 수 없으면 404 Not Found
오류 응답을 보낸다. 대신 노핸들러파운드익셉션(NoHandlerFoundException)
을 발생시키려면 configprop:spring.mvc.throw-Exception-if-no-handler-found
를 true로 설정하자. 기본적으로 스태틱 콘텐츠 제공은 /**
에 매핑되므로 모든 요청에 대한 핸들러를 제공한다. 노핸들러파운드익셉션(NoHandlerFoundException)
이 발생하려면 spring.mvc.static-path-pattern
을 /resources/**
와 같은 보다 구체적인 값으로 설정하거나 spring.web.resources.add-mappings
를 false로 설정하여 스태틱 서비스 제공을 비활성화해야 한다.
컨피규러블웹바인딩이니셜라이저(ConfigurableWebBindingInitializer)
스프링 MVC는 웹바인딩이니셜라이저(WebBindingInitializer)
를 사용하여 특정 요청에 대한 웹데이터바인더(WebDataBinder)
를 초기화한다. 자신만의 컨피규러블웹바인딩이니셜라이저(ConfigurableWebBindingInitializer)
@Bean을 생성하면 스프링 부트는 이를 사용하도록 스프링 MVC를 자동으로 구성한다.
템플릿 엔진(Template Engines)
REST 웹 서비스뿐만 아니라, 스프링 MVC를 사용하여 동적(dynamic) HTML 콘텐츠를 제공할 수도 있다. 스프링 MVC는 Thymeleaf, FreeMarker 및 JSP를 포함한 다양한 템플릿 기술을 지원한다. 또한 다른 많은 템플릿 엔진에는 자체 스프링 MVC 통합이 포함되어 있다.
스프링 부트에는 다음 템플릿 엔진에 대한 자동 구성 지원이 포함되어 있다.
가능하다면 JSP는 피해야 한다. 임베디드 서블릿 컨테이너와 함께 사용할 때 몇 가지 알려진 제한 사항이 있다.
기본 구성으로 이러한 템플릿 엔진 중 하나를 사용하면 템플릿이 src/main/resources/templates
에서 자동 선택된다.
애플리케이션을 실행하는 방법에 따라 IDE에서 클래스패스 순서를 다르게 지정할 수 있다. IDE의 메인 메서드에서 애플리케이션을 실행하면 메이븐이나 그레이들을 사용하거나 패키지된 jar에서 애플리케이션을 실행할 때와 순서가 달라진다. 이로 인해 스프링 부트가 예상 템플릿을 찾지 못할 수 있다. 이 문제가 발생하면 IDE에서 클래스패스의 순서를 변경하여 모듈의 클래스와 리소스를 먼저 배치할 수 있다.
에러 핸들링(Error Handling)
기본적으로, 스프링 부트는 모든 오류를 합리적인 방식으로 처리하는 /error
매핑을 제공하며 이는 서블릿 컨테이너에 “전역(“global”)” 오류 페이지로 등록된다. 머신 클라이언트의 경우 오류 세부정보, HTTP 상태 및 예외 메시지가 포함된 JSON 응답을 생성한다. 브라우저 클라이언트의 경우 동일한 데이터를 HTML 형식으로 렌더링하는 “화이트레이블(whitelabel)” 오류 뷰가 있다(커스텀하려면 에러
를 해결하는 뷰
를 추가하자).
기본 오류 처리 동작을 커스텀하려는 경우 설정할 수 있는 다양한 server.error
프로퍼티가 있다. 부록의 “서버 프로퍼티스(Server Properties)” 절을 참고하자.
기본 동작을 완전히 대체하려면 에러컨트롤러(ErrorController)
를 구현하고 해당 타입의 빈을 등록하거나 에러애트리뷰트(ErrorAttributes) 타입의 빈을 추가하여 기존 메커니즘을 사용하되 내용을 대체할 수 있다.
스프링 프레임워크 6.0부터 RFC 7807 상세 문제가 지원된다. 스프링 MVC는 다음과 같이 application/problem+json
미디어 타입을 사용하여 커스텀 오류 메시지를 생성할 수 있다.
{
"type": "https://example.org/problems/unknown-project",
"title": "Unknown project",
"status": 404,
"detail": "No project found for id 'spring-unknown'",
"instance": "/projects/spring-unknown"
}
이 지원은 spring.mvc.problemdetails.enabled
를 true
로 설정하여 활성화할 수 있다.
다음 예제와 같이 @ControllerAdvice
어노테이션이 달린 클래스를 정의하여 특정 컨트롤러 및/또는 예외 타입을 반환하도록 JSON 문서를 커스텀할 수도 있다.
자바
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice(basePackageClasses = SomeController.class)
public class MyControllerAdvice extends ResponseEntityExceptionHandler {
@ResponseBody
@ExceptionHandler(MyException.class)
public ResponseEntity<?> handleControllerException(HttpServletRequest request, Throwable ex) {
HttpStatus status = getStatus(request);
return new ResponseEntity<>(new MyErrorBody(status.value(), ex.getMessage()), status);
}
private HttpStatus getStatus(HttpServletRequest request) {
Integer code = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
HttpStatus status = HttpStatus.resolve(code);
return (status != null) ? status : HttpStatus.INTERNAL_SERVER_ERROR;
}
}
코틀린
import jakarta.servlet.RequestDispatcher
import jakarta.servlet.http.HttpServletRequest
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.ResponseBody
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
@ControllerAdvice(basePackageClasses = [SomeController::class])
class MyControllerAdvice : ResponseEntityExceptionHandler() {
@ResponseBody
@ExceptionHandler(MyException::class)
fun handleControllerException(request: HttpServletRequest, ex: Throwable): ResponseEntity<*> {
val status = getStatus(request)
return ResponseEntity(MyErrorBody(status.value(), ex.message), status)
}
private fun getStatus(request: HttpServletRequest): HttpStatus {
val code = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE) as Int
val status = HttpStatus.resolve(code)
return status ?: HttpStatus.INTERNAL_SERVER_ERROR
}
}
이전 예제에서, SomeController
와 동일한 패키지에 정의된 컨트롤러에 의해 MyException
이 발생하는 경우 에러애트리뷰트(ErrorAttributes)
표현 대신 MyErrorBody
POJO의 JSON 표현이 사용된다.
컨트롤러 레벨에서 처리된 오류가 메트릭 인프라에 기록되지 않는 경우도 있다. 애플리케이션은 처리된 예외를 요청 애트리뷰트로 설정하여 이러한 예외가 요청 메트릭과 함께 기록되도록 할 수 있다.
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
@Controller
public class MyController {
@ExceptionHandler(CustomException.class)
String handleCustomException(HttpServletRequest request, CustomException ex) {
request.setAttribute(ErrorAttributes.ERROR_ATTRIBUTE, ex);
return "errorView";
}
}
import jakarta.servlet.http.HttpServletRequest
import org.springframework.boot.web.servlet.error.ErrorAttributes
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.ExceptionHandler
@Controller
class MyController {
@ExceptionHandler(CustomException::class)
fun handleCustomException(request: HttpServletRequest, ex: CustomException?): String {
request.setAttribute(ErrorAttributes.ERROR_ATTRIBUTE, ex)
return "errorView"
}
}
커스텀 에러 페이지(Custom Error Pages)
특정 상태 코드에 대한 커스텀 HTML 오류 페이지를 표시하려면 /error 디렉토리에 파일을 추가하면 된다. 오류 페이지는 스태틱 HTML(즉, 스태틱 리소스 디렉토리에 추가됨)이거나 템플릿을 사용하여 작성될 수 있다. 파일명은 정확한 상태 코드이거나 시리즈 마스크(series mask)여야 한다.
예를 들어, 404를 스태틱 HTML 파일에 매핑하려면 디렉토리 구조는 다음과 같다.
src/
+- main/
+- java/
| + <source code>
+- resources/
+- public/
+- error/
| +- 404.html
+- <other public assets>
프리매이커(FreeMarker)
템플릿을 사용하여 모든 5xx 오류를 매핑하려면 디렉토리 구조는 다음과 같다.
src/
+- main/
+- java/
| + <source code>
+- resources/
+- templates/
+- error/
| +- 5xx.ftlh
+- <other templates>
보다 복잡한 매핑의 경우 다음 예제와 같이 에러뷰리졸버(ErrorViewResolver)
인터페이스를 구현하는 빈을 추가할 수도 있다.
자바
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.ModelAndView;
public class MyErrorViewResolver implements ErrorViewResolver {
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
// 요청 또는 상태를 사용하여 선택적으로 ModelAndView를 반환한다.
if (status == HttpStatus.INSUFFICIENT_STORAGE) {
// 여기에 커스텀 모델 값을 추가할 수 있다.
new ModelAndView("myview");
}
return null;
}
}
코틀린
import jakarta.servlet.http.HttpServletRequest
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver
import org.springframework.http.HttpStatus
import org.springframework.web.servlet.ModelAndView
class MyErrorViewResolver : ErrorViewResolver {
override fun resolveErrorView(request: HttpServletRequest, status: HttpStatus, model: Map<String, Any>): ModelAndView? {
// 요청 또는 상태를 사용하여 선택적으로 ModelAndView를 반환한다.
if (status == HttpStatus.INSUFFICIENT_STORAGE) {
// 여기에 커스텀 모델 값을 추가할 수 있다.
return ModelAndView("myview")
}
return null
}
}
@ExceptionHandler
메서드 및 @ControllerAdvice
와 같은 일반 스프링 MVC 기능을 사용할 수도 있다. 그런 다음 에러컨트롤러(ErrorController)
는 처리되지 않은 예외를 선택한다.
스프링 MVC 외부의 오류 페이지 매핑(Mapping Error Pages Outside of Spring MVC)
스프링 MVC를 사용하지 않는 애플리케이션의 경우 에러페이지레지스트라(ErrorPageRegistrar)
인터페이스를 사용하여 에러페이지(ErrorPages)를 직접 등록할 수 있다. 이 추상화는 기본 임베디드 서블릿 컨테이너와 직접 작동하며 스프링 MVC 디스패처서블릿(DispatcherServlet)이 없어도 작동한다.
자바
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.ErrorPageRegistrar;
import org.springframework.boot.web.server.ErrorPageRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
@Configuration(proxyBeanMethods = false)
public class MyErrorPagesConfiguration {
@Bean
public ErrorPageRegistrar errorPageRegistrar() {
return this::registerErrorPages;
}
private void registerErrorPages(ErrorPageRegistry registry) {
registry.addErrorPages(new ErrorPage(HttpStatus.BAD_REQUEST, "/400"));
}
}
코틀린
import org.springframework.boot.web.server.ErrorPage
import org.springframework.boot.web.server.ErrorPageRegistrar
import org.springframework.boot.web.server.ErrorPageRegistry
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpStatus
@Configuration(proxyBeanMethods = false)
class MyErrorPagesConfiguration {
@Bean
fun errorPageRegistrar(): ErrorPageRegistrar {
return ErrorPageRegistrar { registry: ErrorPageRegistry -> registerErrorPages(registry) }
}
private fun registerErrorPages(registry: ErrorPageRegistry) {
registry.addErrorPages(ErrorPage(HttpStatus.BAD_REQUEST, "/400"))
}
}
필터에 의해 처리되는 패스로 에러페이지(ErrorPage)를 등록하는 경우(저지(Jersey) 및 위켓(Wicket)과 같은 일부 스프링이 아닌 웹 프레임워크에서 일반적으로 발생함) 필터는 다음 예제와 같이 ERROR 디스패처로 명시적으로 등록되어야 한다.
자바
import java.util.EnumSet;
import jakarta.servlet.DispatcherType;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class MyFilterConfiguration {
@Bean
public FilterRegistrationBean<MyFilter> myFilter() {
FilterRegistrationBean<MyFilter> registration = new FilterRegistrationBean<>(new MyFilter());
// ...
registration.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));
return registration;
}
}
코틀린
import jakarta.servlet.DispatcherType
import org.springframework.boot.web.servlet.FilterRegistrationBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.util.EnumSet
@Configuration(proxyBeanMethods = false)
class MyFilterConfiguration {
@Bean
fun myFilter(): FilterRegistrationBean<MyFilter> {
val registration = FilterRegistrationBean(MyFilter())
// ...
registration.setDispatcherTypes(EnumSet.allOf(DispatcherType::class.java))
return registration
}
}
기본 필터레지스트레이션빈(FilterRegistrationBean)
에는 ERROR 디스패처 타입이 포함되어 있지 않다.
War 배포 시 오류 처리(Error Handling in a WAR Deployment)
서블릿 컨테이너에 배포되면 스프링 부트는 오류 페이지 필터를 사용하여 오류 상태가 있는 요청을 적절한 오류 페이지로 전달한다. 이는 서블릿 사양이 오류 페이지 등록을 위한 API를 제공하지 않기 때문에 필요하다. war 파일을 배포하는 컨테이너와 애플리케이션에서 사용하는 기술에 따라 몇 가지 추가 구성이 필요할 수 있다.
오류 페이지 필터는 응답이 아직 커밋되지 않은 경우에만 요청을 올바른 오류 페이지로 전달할 수 있다. 기본적으로 웹스피어 애플리케이션 서버(WebSphere Application Server) 8.0 이상에서는 서블릿의 서비스 메소드가 성공적으로 완료되면 응답을 커밋한다. com.ibm.ws.webcontainer.invokeFlushAfterService
를 false
로 설정하여 이 동작을 비활성화해야 한다.
CORS 지원(CORS Support)
CORS(Cross-origin resource sharing)는 IFRAME 또는 JSONP와 같이 덜 안전하고 덜 강력한 접근 방식을 사용하는 대신 어떤 종류의 도메인 간 요청이 승인되는지 유연한 방식으로 지정할 수 있도록 대부분의 브라우저에서 구현되는 W3C 사양이다.
버전 4.2부터 스프링 MVC는 CORS를 지원합니다. 스프링 부트 애플리케이션에서 @CrossOrigin
어노테이션과 함께 컨트롤러 메서드 CORS 구성을 사용하면 특정 구성이 필요하지 않다. 다음 예제와 같이 커스텀 addCorsMappings(CorsRegistry)
메소드를 사용하여 웹Mvc컨피규어러(WebMvcConfigurer)
빈을 등록하여 스태틱 CORS 구성을 정의할 수 있다.
자바
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration(proxyBeanMethods = false)
public class MyCorsConfiguration {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**");
}
};
}
}
코틀린
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.CorsRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
@Configuration(proxyBeanMethods = false)
class MyCorsConfiguration {
@Bean
fun corsConfigurer(): WebMvcConfigurer {
return object : WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/api/**")
}
}
}
}
8.1.2. JAX-RS 및 저지(JAX-RS and Jersey)
REST 엔드포인트에 JAX-RS 프로그래밍 모델을 선호하는 경우 스프링 MVC 대신 사용 가능한 구현 중 하나를 사용할 수 있다. 저지(Jersey) 및 아파치 CXF는 기본적으로 매우 잘 작동한다. CXF를 사용하려면 애플리케이션 컨텍스트에서 서블릿이나 필터를 @Bean
으로 등록해야 한다. 저지(Jersey)에는 일부 스프링 지원이 있으므로 스타터와 함께 스프링 부트에서 자동 구성 지원도 제공한다.
저지(Jersey)를 시작하려면 spring-boot-starter-jersey
를 의존성으로 포함시킨 다음 다음 예제와 같이 모든 엔드포인트를 등록하는 리소스컨피그(ResourceConfig) 타입의 @Bean 하나가 필요하다.
import org.glassfish.jersey.server.ResourceConfig;
import org.springframework.stereotype.Component;
@Component
public class MyJerseyConfig extends ResourceConfig {
public MyJerseyConfig() {
register(MyEndpoint.class);
}
}
실행 가능한 압축파일 검색에 대한 저지(Jersey)의 지원은 다소 제한적이다. 예를 들어, 실행 가능한 war 파일을 실행할 때 완전히 실행 가능한 jar 파일이나
WEB-INF/classes
에 있는 패키지의 엔드포인트를 검색할 수 없다. 이러한 제한을 피하려면packages
메소드를 사용하면 안 되며 앞의 예제와 같이 레지스터(Register) 메소드를 사용하여 엔드포인트를 개별적으로 등록해야 한다.
고급 커스텀를 위해, 리소스컨피그커스터마이저(ResourceConfigCustomizer)
를 구현하는 임의 개수의 빈을 등록할 수도 있다.
등록된 모든 엔드포인트는 다음 예제와 같이 HTTP 리소스 어노테이션(@GET 및 기타)이 있는 @Components
여야 한다.
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.springframework.stereotype.Component;
@Component
@Path("/hello")
public class MyEndpoint {
@GET
public String message() {
return "Hello";
}
}
엔드포인트는 스프링 @Component
이므로 생명주기은 스프링에 의해 관리되며 @Autowired
어노테이션을 사용하여 의존성을 주입하고 @Value
어노테이션을 사용하여 외부 구성을 주입할 수 있다. 기본적으로 저지(Jersey) 서블릿은 /*
에 등록되고 매핑된다. 리소스컨피그(ResourceConfig)에 @ApplicationPath
를 추가하여 매핑을 변경할 수 있다.
기본적으로, 저지(Jersey)는 저지서블릿레지스트레이션(JerseyServletRegistration)
이라는 서블릿레지스트레이션빈(ServletRegistrationBean)
타입의 @Bean
에서 서블릿으로 설정된다. 기본적으로 서블릿은 느리게 초기화되지만 spring.jersey.servlet.load-on-startup
을 설정하여 해당 동작을 커스텀할 수 있다. 동일한 이름을 가진 자신만의 빈을 생성하여 해당 빈을 비활성화하거나 오버라이드할 수 있다. spring.jersey.type=filter
를 설정하여 서블릿 대신 필터를 사용할 수도 있다(이 경우 대체하거나 오버라이드할 @Bean
은 저지필터레지스트레이션(JerseyFilterRegistration)이다). 필터에는 spring.jersey.filter.order
로 설정할 수 있는 @Order
가 있다. 저지(Jersey)를 필터로 사용하는 경우 저지(Jersey)가 가로채지 않는 모든 요청을 처리하는 서블릿이 있어야 한다. 애플리케이션에 이러한 서블릿이 포함되어 있지 않은 경우 server.servlet.register-default-servlet
을 true로 설정하여 기본 서블릿을 활성화할 수 있다. 서블릿과 필터 등록 모두 프로퍼티스 맵을 지정하기 위해 spring.jersey.init.*
를 사용하여 초기화 파라미터를 제공할 수 있다.
8.1.3. 임베디드 서블릿 컨테이너 지원(Embedded Servlet Container Support)
서블릿 애플리케이션의 경우 스프링 부트에는 임베디드 톰캣, 제티(Jetty) 및 언더토우(Undertow) 서버에 대한 지원이 포함되어 있다. 대부분의 개발자는 구성된 인스턴스를 얻기 위해 적절한 “스타터”를 사용한다. 기본적으로 임포트 서버는 포트 8080에서 HTTP 요청을 수신한다.
서블릿, 필터, 리스너(Servlets, Filters, and Listeners)
임베디드 서블릿 컨테이너를 사용할 때 스프링 빈을 사용하거나 서블릿 컴포넌트를 검색하여 서블릿 사양에서 서블릿, 필터 및 모든 리스너(예: HttpSessionListener)를 등록할 수 있다.
서블릿, 필터, 리스너를 스프링 빈으로 등록(Registering Servlets, Filters, and Listeners as Spring Beans)
스프링 빈인 모든 서블릿, 필터 또는 서블릿 *Listener 인스턴스는 임베디드 컨테이너에 등록된다. 이는 구성 중 application.properties
의 값을 참조하려는 경우 특히 편리할 수 있다.
기본적으로, 컨텍스트에 싱글 서블릿만 포함된 경우 /
에 매핑된다. 멀티 서블릿 빈의 경우 빈명이 패스 접두어로 사용된다. 필터는 /*
에 매핑된다.
컨벤션 기반 매핑이 충분히 유연하지 않은 경우 완전한 제어를 위해 서블릿레지스트레이션빈(ServletRegistrationBean)
, 필터레지스트레이션빈(FilterRegistrationBean)
및 서블릿리스너레지스트레이션빈(ServletListenerRegistrationBean)
클래스를 사용할 수 있다.
일반적으로 필터 빈은 순서가 없는 상태로 두는 것이 안전하다. 특정 순서가 필요한 경우 필터에 @Order
어노테이션을 추가하거나 Ordered
를 구현해야 한다. @Order
로 빈(bean) 메소드에 어노테이션을 달아 필터의 순서를 구성할 수 없다. @Order
를 추가하거나 Ordered
를 구현하기 위해 Filter
클래스를 변경할 수 없는 경우 필터에 대한 필터레지스트레이션빈(FilterRegistrationBean)
을 정의하고 setOrder(int)
메서드를 사용하여 등록된 빈의 순서를 설정해야 한다. Ordered.HIGHEST_PRECEDENCE
에서 요청의 바디(request body)를 읽는 필터를 구성하지 말자. 애플리케이션의 문자 인코딩 구성에 어긋날 수 있기 때문이다. 서블릿 필터가 요청을 래핑하는 경우 OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER
보다 작거나 같은 순서로 구성되어야 한다.
애플리케이션에서 모든 필터의 순서를 보려면 웹 로깅 그룹(logging.level.web=debug)에 대해 디버그 레벨 로깅을 활성화하자. 다음을 포함한 등록된 필터의 순서 및 URL 패턴이 시작 시 기록된다.
필터 빈은 애플리케이션 생명주기 초기에 초기화되므로 등록 시 주의하자. 상호작용하는 필터를 등록해야 하는 다른 빈인 경우 대신
델리게이팅필터프록시레지스트레이션빈(DelegatingFilterProxyRegistrationBean)
사용을 고려하자.
서블릿 컨텍스트 초기화(Servlet Context Initialization)
임베디드 서블릿 컨테이너는 jakarta.servlet.ServletContainerInitializer
인터페이스나 스프링의 org.springframework.web.WebApplicationInitializer
인터페이스를 직접 실행하지 않는다. 이는 war 실행중 서드 파티 라이브러리가 스프링 부트 애플리케이션을 손상시킬 수 있는 위험을 줄이기 위한 의도적인 설계된 결정이다.
스프링 부트 애플리케이션에서 서블릿 컨텍스트 초기화를 수행해야 하는 경우 org.springframework.boot.web.servlet.ServletContextInitializer
인터페이스를 구현하는 빈을 등록해야 한다. onStartup
메소드는 서블릿컨텍스트(ServletContext)
에 대한 접근을 제공하며 필요한 경우 기존 웹애플리케이션이니셜라이저(WebApplicationInitializer)
에 대한 어댑터로 쉽게 사용할 수 있다.
서블릿, 필터 및 리스너 스캐닝(Scanning for Servlets, Filters, and listeners)
임베디드 컨테이너를 사용할 때 @ServletComponentScan
을 사용하면 @WebServlet
, @WebFilter
및 @WebListener
어노테이션이 달린 클래스의 자동 등록을 활성화할 수 있다.
@ServletComponentScan
은 컨테이너의 임베디드 검색 메커니즘(built-in discovery mechanisms)이 대신 사용되는 독립형 컨테이너에서는 효과가 없다.
서블릿웹서버애플리케이션컨텍스트(The ServletWebServerApplicationContext)
내부적으로 스프링 부트는 임베디드 서블릿 컨테이너 지원을 위해 다른 타입의 애플리케이션컨텍스트(ApplicationContext)
를 사용한다. 서블릿웹서버애플리케이션컨텍스트(ServletWebServerApplicationContext)
는 싱글 서블릿웹서버팩토리(ServletWebServerFactory)
빈을 검색하여 자체적으로 부트스트랩하는 특별한 타입의 웹애플리케이션컨텍스트(WebApplicationContext)
이다. 일반적으로 톰캣서블릿웹서버팩토리(TomcatServletWebServerFactory)
, 제티서블릿웹서버팩토리(JettyServletWebServerFactory)
또는 언더토우서블릿웹서버팩토리(UndertowServletWebServerFactory)
가 자동 구성된다.
일반적으로 이러한 구현 클래스를 알 필요는 없다. 대부분의 애플리케이션은 자동으로 구성되며 적절한 애플리케이션컨텍스트(ApplicationContext)
및 서블릿웹서버팩토리(ServletWebServerFactory)
가 생성된다.
임베디드 컨테이너에서 서블릿컨텍스트(ServletContext)
는 애플리케이션 컨텍스트 초기화 중에 발생하는 서버 시작의 일부로 설정된다. 이 때문에 애플리케이션컨텍스트(ApplicationContext)
의 빈은 서블릿컨텍스트(ServletContext)
로 안정적으로 초기화될 수 없다. 이 문제를 해결하는 한 가지 방법은 애플리케이션컨텍스트(ApplicationContext)
를 빈의 의존성으로 주입하고 필요할 때만 서블릿컨텍스트(ServletContext)
에 접근하는 것이다. 또 다른 방법은 서버가 시작된 후 콜백을 사용하는 것이다. 이는 다음과 같이 애플리케이션스타디드이벤트(ApplicationStartedEvent)
를 수신하는 애플리케이션리스너(ApplicationListener)
를 사용하여 수행할 수 있다.
import jakarta.servlet.ServletContext;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.web.context.WebApplicationContext;
public class MyDemoBean implements ApplicationListener<ApplicationStartedEvent> {
private ServletContext servletContext;
@Override
public void onApplicationEvent(ApplicationStartedEvent event) {
ApplicationContext applicationContext = event.getApplicationContext();
this.servletContext = ((WebApplicationContext) applicationContext).getServletContext();
}
}
임베디드 서블릿 컨테이너 커스텀(Customizing Embedded Servlet Containers)
공통 서블릿 컨테이너 설정은 스프링 환경 프로퍼티를 사용하여 구성할 수 있다. 일반적으로 application.properties
또는 application.yaml
파일에서 프로퍼티스를 정의한다.
일반적인 서버 설정에는 다음 내용이 포함된다.
- 네트워크 설정: 들어오는 HTTP 요청(
server.port
)에 대한 수신 포트,server.address
에 바인드할 인터페이스 주소 등. - 세션 설정: 세션 지속 여부(
server.servlet.session.pertant
), 세션 시간 초과(server.servlet.session.timeout
), 세션 데이터 위치(server.servlet.session.store-dir
) 및 세션 쿠키 구성(server.servlet.session.cookie.*
). - 에러 관리: 오류 페이지(server.error.path)의 위치 등.
- SSL
- HTTP compression
스프링 부트는 가능한 한 공통 설정 노출하기를 시도하지만 이것이 항상 가능한 것은 아니다. 이러한 경우 전용 네임스페이스(dedicated namespaces)는 서버별 커스텀을 제공한다(server.tomcat
및 server.undertow
참고). 예를 들어, 임베디드 서블릿 컨테이너의 특정 기능을 사용하여 접근 로그를 구성할 수 있다.
전체 목록은 서버프로퍼티스(ServerProperties) 클래스를 참고하자.
SameSite 쿠키(SameSite Cookies)
SameSite 쿠키 애트리뷰트는 웹 브라우저에서 교차 사이트 요청(cross-site requests)에 쿠키 제출 여부 제어하는 데 사용될 수 있다. 이 애트리뷰트는 애트리뷰트가 누락되었을 때 사용되는 기본값을 변경하기 시작한 최신 웹 브라우저와 특히 관련이 있다.
세션 쿠키의 SameSite 애트리뷰트를 변경하려면, server.servlet.session.cookie.same-site
프로퍼티을 사용할 수 있다. 이 프로퍼티는 자동 구성된 톰캣, 제티 및 언더토우 서버에서 지원된다. 또한 스프링 세션 서블릿 기반 세션리포지터리(SessionRepository)
빈을 구성하는 데에도 사용된다.
예를 들어 세션 쿠키에 SameSite 애트리뷰트가 None
이 되도록 하려면 application.properties
또는 application.yaml
파일에 다음을 추가할 수 있다.
프로퍼티스(Properties)
server.servlet.session.cookie.same-site=none
Yaml
server:
servlet:
session:
cookie:
same-site: "none"
Http서블릿리스폰스(HttpServletResponse)
에 추가된 다른 쿠키의 SameSite 애트리뷰트를 변경하려면 쿠키세임사이트서플라이어(CookieSameSiteSupplier)
를 사용할 수 있다. 쿠키세임사이트서플라이어(CookieSameSiteSupplier)
에는 쿠키가 전달되며 SameSite 값 또는 null을 반환할 수 있다.
특정 쿠키를 신속하게 일치시키는 데 사용할 수 있는 다양한 편의 팩토리 및 필터 메소드가 있다. 예를 들어, 다음 빈을 추가하면 정규식 myapp.*
와 일치하는 이름을 가진 모든 쿠키에 대해 Lax의 SameSite가 자동으로 적용된다.
자바
import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class MySameSiteConfiguration {
@Bean
public CookieSameSiteSupplier applicationCookieSameSiteSupplier() {
return CookieSameSiteSupplier.ofLax().whenHasNameMatching("myapp.*");
}
}
코틀린
import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration(proxyBeanMethods = false)
class MySameSiteConfiguration {
@Bean
fun applicationCookieSameSiteSupplier(): CookieSameSiteSupplier {
return CookieSameSiteSupplier.ofLax().whenHasNameMatching("myapp.*")
}
}
프로그래밍 방식의 커스텀(Programmatic Customization)
임베디드 서블릿 컨테이너를 프로그래밍 방식으로 구성해야 하는 경우 웹서버팩토리커스터마이저(WebServerFactoryCustomizer)
인터페이스를 구현하는 스프링 빈을 등록할 수 있다. 웹서버팩토리커스터마이저(WebServerFactoryCustomizer)
는 다양한 커스텀 설정 방법을 포함하는 컨피규러블서블릿웹서버팩토리(ConfigurableServletWebServerFactory)
에 대한 접근를 제공한다. 다음 예에서는 프로그래밍 방식으로 포트를 설정하는 방법을 보여준다.
자바
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.stereotype.Component;
@Component
public class MyWebServerFactoryCustomizer implements
WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
@Override
public void customize(ConfigurableServletWebServerFactory server) {
server.setPort(9000);
}
}
코틀린
import org.springframework.boot.web.server.WebServerFactoryCustomizer
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory
import org.springframework.stereotype.Component
@Component
class MyWebServerFactoryCustomizer :
WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
override fun customize(server: ConfigurableServletWebServerFactory) {
server.setPort(9000)
}
}
톰캣서블릿웹서버팩토리(TomcatServletWebServerFactory)
, 제티서블릿웹서버팩토리(JettyServletWebServerFactory)
및 언더토우서블릿웹서버팩토리(UndertowServletWebServerFactory)
는 각각 톰캣, 제티 및 언더토우에 대한 추가 커스텀 설정 방법이 있는 컨피규러블서블릿웹서버팩토리(ConfigurableServletWebServerFactory)
전용 변형이다. 다음 예에서는 톰캣 관련 구성 옵션에 대한 접근을 제공하는 톰캣서블릿웹서서팩토리(TomcatServletWebServerFactory)
를 커스텀하는 방법을 보여준다.
자바
import java.time.Duration;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;
@Component
public class MyTomcatWebServerFactoryCustomizer implements
WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
@Override
public void customize(TomcatServletWebServerFactory server) {
server.addConnectorCustomizers((connector) -> connector.setAsyncTimeout(Duration.ofSeconds(20).toMillis()));
}
}
코틀린
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory
import org.springframework.boot.web.server.WebServerFactoryCustomizer
import org.springframework.stereotype.Component
import java.time.Duration
@Component
class MyTomcatWebServerFactoryCustomizer : WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
override fun customize(server: TomcatServletWebServerFactory) {
server.addConnectorCustomizers({ connector -> connector.asyncTimeout = Duration.ofSeconds(20).toMillis() })
}
}
ConfigurableServletWebServerFactory를 직접 커스텀하기(Customizing ConfigurableServletWebServerFactory Directly)
서블릿웹서버팩토리(ServletWebServerFactory)
에서 확장해야 하는 고급 사례의 경우 이러한 타입의 빈을 직접 노출할 수 있다.
다양한 구성 옵션에 대한 Setter가 제공된다. 좀 더 색다른 작업을 수행해야 하는 경우를 대비해 여러 보호(protected) 메서드 “후크(hook)”도 제공된다. 자세한 내용은 소스 코드 문서를 참고하자.
자동 구성된 커스텀 프로그램은 커스텀 팩토리에 계속 적용되므로, 해당 옵션은 신중하게 사용하자.
JSP 제한 사항(JSP Limitations)
임베디드 서블릿 컨테이너를 사용하고 실행 가능한 압축파일로 패키지된 스프링 부트 애플리케이션을 실행할 때 JSP 지원에 몇 가지 제한 사항이 있다.
- 제티와 톰캣을 사용할 경우 war 패키징을 사용하면 작동한다. 실행 가능한 war는 java -jar로 실행될 때 작동하며 모든 표준 컨테이너에 배포할 수도 있다. 실행 가능한 jar를 사용할 때는 JSP가 지원되지 않다.
- 언더토우(Undertow)는 JSP를 지원하지 않는다.
- 커스텀 error.jsp 페이지를 생성해도 오류 처리를 위한 기본 뷰가 오버라이드되지 않는다. 대신 커스텀 오류 페이지를 사용해야 한다.
8.2. 리액티브 웹 애플리케이션(Reactive Web Applications)
스프링 부트는 스프링 웹플럭스에 대한 자동 구성(auto-configuration)을 제공하여 리액티브 웹 애플리케이션 개발을 단순화한다.
8.2.1. 스프링 웹플럭스 프레임워크(The “Spring WebFlux Framework”)
스프링 웹플럭스(Spring WebFlux)는 스프링 프임워 5.0에 도입된 새로운 리액티브 웹 프레임워크이다. 스프링 MVC와 달리, 서블릿 API가 필요하지 않고, 완전히 비동기적이고, 논블락킹이며 리액터(Reactor) 프로젝트를 통해 리액티브 스림(Reactive Streams) 사양을 구현한다.
스프링 웹플럭스(Spring WebFlux)는 함수형(functional) 기반과 어노테이션 기반이라는 두 가지 형태로 제공된다. 다음 예제와 같이 어노테이션 기반 모델은 스프링 MVC 모델과 매우 유사하다.
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/users")
public class MyRestController {
private final UserRepository userRepository;
private final CustomerRepository customerRepository;
public MyRestController(UserRepository userRepository, CustomerRepository customerRepository) {
this.userRepository = userRepository;
this.customerRepository = customerRepository;
}
@GetMapping("/{userId}")
public Mono<User> getUser(@PathVariable Long userId) {
return this.userRepository.findById(userId);
}
@GetMapping("/{userId}/customers")
public Flux<Customer> getUserCustomers(@PathVariable Long userId) {
return this.userRepository.findById(userId).flatMapMany(this.customerRepository::findByUser);
}
@DeleteMapping("/{userId}")
public Mono<Void> deleteUser(@PathVariable Long userId) {
return this.userRepository.deleteById(userId);
}
}
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
@RestController
@RequestMapping("/users")
class MyRestController(private val userRepository: UserRepository, private val customerRepository: CustomerRepository) {
@GetMapping("/{userId}")
fun getUser(@PathVariable userId: Long): Mono<User?> {
return userRepository.findById(userId)
}
@GetMapping("/{userId}/customers")
fun getUserCustomers(@PathVariable userId: Long): Flux<Customer> {
return userRepository.findById(userId).flatMapMany { user: User? ->
customerRepository.findByUser(user)
}
}
@DeleteMapping("/{userId}")
fun deleteUser(@PathVariable userId: Long): Mono<Void> {
return userRepository.deleteById(userId)
}
}
함수형(functional)의 변형인 “WebFlux.fn”은 다음 예와 같이 라우팅 구성을 요청의 실제 처리와 분리한다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicate;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
@Configuration(proxyBeanMethods = false)
public class MyRoutingConfiguration {
private static final RequestPredicate ACCEPT_JSON = accept(MediaType.APPLICATION_JSON);
@Bean
public RouterFunction<ServerResponse> monoRouterFunction(MyUserHandler userHandler) {
return route()
.GET("/{user}", ACCEPT_JSON, userHandler::getUser)
.GET("/{user}/customers", ACCEPT_JSON, userHandler::getUserCustomers)
.DELETE("/{user}", ACCEPT_JSON, userHandler::deleteUser)
.build();
}
}
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.MediaType
import org.springframework.web.reactive.function.server.RequestPredicates.DELETE
import org.springframework.web.reactive.function.server.RequestPredicates.GET
import org.springframework.web.reactive.function.server.RequestPredicates.accept
import org.springframework.web.reactive.function.server.RouterFunction
import org.springframework.web.reactive.function.server.RouterFunctions
import org.springframework.web.reactive.function.server.ServerResponse
@Configuration(proxyBeanMethods = false)
class MyRoutingConfiguration {
@Bean
fun monoRouterFunction(userHandler: MyUserHandler): RouterFunction<ServerResponse> {
return RouterFunctions.route(
GET("/{user}").and(ACCEPT_JSON), userHandler::getUser).andRoute(
GET("/{user}/customers").and(ACCEPT_JSON), userHandler::getUserCustomers).andRoute(
DELETE("/{user}").and(ACCEPT_JSON), userHandler::deleteUser)
}
companion object {
private val ACCEPT_JSON = accept(MediaType.APPLICATION_JSON)
}
}
import reactor.core.publisher.Mono;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
@Component
public class MyUserHandler {
public Mono<ServerResponse> getUser(ServerRequest request) {
...
}
public Mono<ServerResponse> getUserCustomers(ServerRequest request) {
...
}
public Mono<ServerResponse> deleteUser(ServerRequest request) {
...
}
}
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse
import reactor.core.publisher.Mono
@Component
class MyUserHandler {
fun getUser(request: ServerRequest?): Mono<ServerResponse> {
return ServerResponse.ok().build()
}
fun getUserCustomers(request: ServerRequest?): Mono<ServerResponse> {
return ServerResponse.ok().build()
}
fun deleteUser(request: ServerRequest?): Mono<ServerResponse> {
return ServerResponse.ok().build()
}
}
웹플럭스는 스프링 프레임워크의 일부이며 자세한 내용은 해당 레퍼런스 문서에서 확인할 수 있다.
라우터 정의를 모듈화하기 위해 원하는 만큼 라우터펑션(RouterFunction) 빈을 정의할 수 있다. 우선순위를 적용해야 하는 경우 빈의 순위를 정할 수 있다.
시작하려면, 애플리케이션에 spring-boot-starter-webflux
모듈을 추가하자.
애플리케이션에 spring-boot-starter-web
및 spring-boot-starter-webflux
모듈을 모두 추가하면 웹플럭스가 아닌 스프링 부트가 스프링 MVC를 자동 구성하게 된다. 많은 스프링 개발자가 리액티브 웹클라이언트(WebClient)를 사용하기 위해 스프링 MVC 애플리케이션에 spring-boot-starter-webflux
를 추가하기 때문에 이렇게 동작을 선택했다. 선택한 애플리케이션 타입을 SpringApplication.setWebApplicationType(WebApplicationType.REACTIVE)
으로 설정하여 선택을 강제할 수 있다.
스프링 웹플럭스 자동 구성(Spring WebFlux Auto-configuration)
스프링 부트는 대부분의 애플리케이션에서 잘 작동하는 스프링 웹플럭스에 대한 자동 구성을 제공한다.
자동 구성은 스프링의 기본값 위에 다음 기능을 추가한다.
Http메세지리더(HttpMessageReader)
및Http메세지라이터(HttpMessageWriter)
인스턴스에 대한 코덱(codec) 구성(이 문서의 뒷부분에서 설명)- WebJars 지원을 포함하여 스태틱 리소스 제공 지원(이 문서 뒷부분에서 설명)
스프링 부트 웹플럭스 기능을 유지하고 추가 웹플럭스 구성을 추가하려는 경우 @EnableWebFlux
없이 웹플럭스컨피규어러(WebFluxConfigurer) 타입의 @Configuration
클래스를 추가할 수 있다.
스프링 웹플럭스를 완전히 제어하려면 @EnableWebFlux
어노테이션이 달린 고유한 @Configuration
을 추가할 수 있다.
HttpMessageReaders 및 HttpMessageWriters가 포함된 HTTP 코덱(HTTP Codecs with HttpMessageReaders and HttpMessageWriters)
스프링 웹플럭스는 Http메세지리더(HttpMessageReader)
및 Http메세지라이터(HttpMessageWriter)
인터페이스를 사용하여 HTTP 요청과 응답을 변환한다. 클래스패스에서 사용 가능한 라이브러리를 확인하여 적절한 기본값을 갖도록 코덱컨피규어러(CodecConfigurer)로 구성된다.
스프링 부트는 코덱인 spring.codec.*
에 대한 전용 구성 프로퍼티스를 제공한다. 또한 코덱커스터마이저(CodecCustomizer)
인스턴스를 사용하여 커스텀을 적용한다. 예를 들어 spring.jackson.*
구성 키는 잭슨(Jackson) 코덱에 적용된다.
코덱을 추가하거나 커스텀해야 하는 경우, 다음 예와 같이 커스텀 코덱커스터마이저(CodecCustomizer) 컴포넌트를 생성할 수 있다.
import org.springframework.boot.web.codec.CodecCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.codec.ServerSentEventHttpMessageReader;
@Configuration(proxyBeanMethods = false)
public class MyCodecsConfiguration {
@Bean
public CodecCustomizer myCodecCustomizer() {
return (configurer) -> {
configurer.registerDefaults(false);
configurer.customCodecs().register(new ServerSentEventHttpMessageReader());
// ...
};
}
}
import org.springframework.boot.web.codec.CodecCustomizer
import org.springframework.context.annotation.Bean
import org.springframework.http.codec.CodecConfigurer
import org.springframework.http.codec.ServerSentEventHttpMessageReader
class MyCodecsConfiguration {
@Bean
fun myCodecCustomizer(): CodecCustomizer {
return CodecCustomizer { configurer: CodecConfigurer ->
configurer.registerDefaults(false)
configurer.customCodecs().register(ServerSentEventHttpMessageReader())
}
}
}
부트의 커스텀 JSON 시리얼라이저(serializer) 및 디시리얼라이저(deserializer)를 활용할 수도 있다
정적 콘텐츠(Static Content)
기본적으로 스프링 부트는 클래스패스에 있는 /static
(또는 /public
, /resources
또는 /META-INF/resources
)이라는 디렉토리의 정적 콘텐츠를 제공한다. 이는 스프링 웹플럭스의 리소스웹핸들러(ResourceWebHandler)
를 사용하므로 사용자는 자체 웹플럭스컨피규어러(WebFluxConfigurer)
를 추가하고 addResourceHandlers
메서드를 오버라이드하여 해당 동작을 수정할 수 있다.
기본적으로 리소스는 /**
에 매핑되지만 spring.webflux.static-path-pattern
프로퍼티을 설정하여 조정할 수 있다. 예를 들어 모든 리소스를 /resources/**
에 재배치하는 방법은 다음과 같다.
프로퍼티스(Properties)
spring.webflux.static-path-pattern=/resources/**
Yaml
spring:
webflux:
static-path-pattern: "/resources/**"
spring.web.resources.static-location
을 사용하여 정적 리소스 위치를 커스텀할 수도 있다. 이렇게 하면 기본값이 디렉토리 위치로 대체된다. 그렇게 하면 기본 시작 페이지 감지가 커스텀 위치로 전환된다. 따라서 시작할 때 위치에 index.html
이 있으면 해당 위치가 애플리케이션의 홈 페이지다.
앞서 나열된 “표준(“standard”)” 정적 리소스 위치 외에도 Webjars 콘텐츠에 대한 특별한 경우가 만들어졌다. 기본적으로 /webjars/**
에 패스가 있는 모든 리소스는 Webjars 형식으로 패키지된 경우 jar 파일에서 제공된다. 패스는 spring.webflux.webjars-path-pattern
프로퍼티로 커스텀할 수 있다.
스트링 웹플럭스 애플리케이션은 서블릿 API에 엄격하게 의존하지 않으므로 war 파일로 배포할 수 없으며 src/main/webapp
디렉토리를 사용하지 않는다.
웰컴 페이지(Welcome Page)
스프링 부트는 정적 및 템플릿 시작 페이지를 모두 지원한다. 먼저 구성된 정적 콘텐츠 위치에서 index.html
파일을 찾는다. 찾을 수 없으면 인덱스 템플릿을 찾는다. 둘 중 하나가 발견되면 자동으로 애플리케이션의 시작 페이지로 사용된다.
템플릿 엔진(Template Engines)
REST 웹 서비스뿐만 아니라 스프링 웹플럭스를 사용하여 동적 HTML 콘텐츠를 제공할 수도 있다. 스프링 웹플럭스는 Thymeleaf, FreeMarker 및 Mustache를 포함한 다양한 템플릿 기술을 지원한다.
스프링 부트에는 다음 템플릿 엔진에 대한 자동 구성 지원이 포함되어 있다.
- FreeMarker
- Thymeleaf
- Mustache
기본 구성으로 이러한 템플릿 엔진 중 하나를 사용하면 템플릿이 src/main/resources/templates
에서 자동으로 선택된다.
에러 핸들링(Error Handling)
스프링 부트는 모든 오류를 합리적인 방식으로 처리하는 웹익셉션핸들러(WebExceptionHandler)를 제공한다. 처리 순서에서 위치는 마지막으로 웹플럭스에서 제공하는 핸들러 바로 앞이다. 머신 클라이언트의 경우 오류 세부정보, HTTP 상태 및 예외 메시지가 포함된 JSON 응답을 생성한다. 브라우저 클라이언트의 경우 동일한 데이터를 HTML 타입으로 렌더링하는 “whitelabel” 오류 처리기가 있다. 오류를 표시하기 위해 자체 HTML 템플릿을 제공할 수도 있다(다음 섹션 참조).
스프링 부트에서 오류 처리를 직접 커스텀하기 전에 스프링 웹플럭스에서 RFC 7807 문제 세부 정보 지원을 활용할 수 있다. 스프링 웹플럭스는 다음과 같이 application/problem+json
미디어 타입을 사용하여 커스텀 오류 메시지를 생성할 수 있다.
{
"type": "https://example.org/problems/unknown-project",
"title": "Unknown project",
"status": 404,
"detail": "No project found for id 'spring-unknown'",
"instance": "/projects/spring-unknown"
}
이 지원은 spring.webflux.problemdetails.enabled
를 true로 설정하여 활성화할 수 있다.
이 기능을 커스텀하는 첫 번째 단계에는 기존 메커니즘을 사용하되 오류 내용을 대체하거나 늘리는 작업이 포함되는 경우가 많다. 이를 위해 에러애트리뷰트(ErrorAttributes) 타입의 빈을 추가할 수 있다.
오류 처리 동작을 변경하려면 에러웹익셉션핸들러(ErrorWebExceptionHandler)를 구현하고 해당 타입의 빈 정의를 등록할 수 있다. 에러웹익셉션핸들라(ErrorWebExceptionHandler)는 매우 낮은 레벨이기 때문에 스프링 부트는 다음 예제와 같이 웹플럭스 기능적 방식으로 오류를 처리할 수 있도록 편리한 앱스트랙트에러웹익셥션핸들러(AbstractErrorWebExceptionHandler)도 제공한니다.
import reactor.core.publisher.Mono;
import org.springframework.boot.autoconfigure.web.WebProperties.Resources;
import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.function.server.ServerResponse.BodyBuilder;
@Component
public class MyErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {
public MyErrorWebExceptionHandler(ErrorAttributes errorAttributes, Resources resources, ApplicationContext applicationContext) {
super(errorAttributes, resources, applicationContext);
}
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(this::acceptsXml, this::handleErrorAsXml);
}
private boolean acceptsXml(ServerRequest request) {
return request.headers().accept().contains(MediaType.APPLICATION_XML);
}
public Mono<ServerResponse> handleErrorAsXml(ServerRequest request) {
BodyBuilder builder = ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR);
// ... additional builder calls
return builder.build();
}
}
import org.springframework.boot.autoconfigure.web.WebProperties
import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler
import org.springframework.boot.web.reactive.error.ErrorAttributes
import org.springframework.context.ApplicationContext
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.server.RouterFunction
import org.springframework.web.reactive.function.server.RouterFunctions
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse
import reactor.core.publisher.Mono
@Component
class MyErrorWebExceptionHandler(
errorAttributes: ErrorAttributes?,
resources: WebProperties.Resources?,
applicationContext: ApplicationContext?
) : AbstractErrorWebExceptionHandler(errorAttributes, resources, applicationContext) {
override fun getRoutingFunction(errorAttributes: ErrorAttributes): RouterFunction<ServerResponse> {
return RouterFunctions.route(this::acceptsXml, this::handleErrorAsXml)
}
private fun acceptsXml(request: ServerRequest): Boolean {
return request.headers().accept().contains(MediaType.APPLICATION_XML)
}
fun handleErrorAsXml(request: ServerRequest?): Mono<ServerResponse> {
val builder = ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
// ... additional builder calls
return builder.build()
}
}
보다 완전한 그림을 위해 디폴트에러웹익셥션핸들러(DefaultErrorWebExceptionHandler)
를 직접 하위 클래스로 분류하고 특정 메서드를 오버라이드할 수도 있다.
컨트롤러 또는 핸들러 기능 레벨에서 처리된 오류가 메트릭 인프라에 기록되지 않는 경우도 있다. 애플리케이션은 처리된 예외를 요청 애트리뷰트로 설정하여 이러한 예외가 요청 메트릭과 함께 기록되도록 할 수 있다.
자바
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.reactive.result.view.Rendering;
import org.springframework.web.server.ServerWebExchange;
@Controller
public class MyExceptionHandlingController {
@GetMapping("/profile")
public Rendering userProfile() {
// ...
throw new IllegalStateException();
}
@ExceptionHandler(IllegalStateException.class)
public Rendering handleIllegalState(ServerWebExchange exchange, IllegalStateException exc) {
exchange.getAttributes().putIfAbsent(ErrorAttributes.ERROR_ATTRIBUTE, exc);
return Rendering.view("errorView").modelAttribute("message", exc.getMessage()).build();
}
}
코틀린
import org.springframework.boot.web.reactive.error.ErrorAttributes
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.reactive.result.view.Rendering
import org.springframework.web.server.ServerWebExchange
@Controller
class MyExceptionHandlingController {
@GetMapping("/profile")
fun userProfile(): Rendering {
// ...
throw IllegalStateException()
}
@ExceptionHandler(IllegalStateException::class)
fun handleIllegalState(exchange: ServerWebExchange, exc: IllegalStateException): Rendering {
exchange.attributes.putIfAbsent(ErrorAttributes.ERROR_ATTRIBUTE, exc)
return Rendering.view("errorView").modelAttribute("message", exc.message ?: "").build()
}
}
커스텀 에러 페이지(Custom Error Pages)
주어진 상태 코드에 대한 커스텀 HTML 오류 페이지를 표시하려는 경우 예를 들어 /error
디렉토리에 파일을 추가하여 error/*
에서 해결되는 보기를 추가할 수 있다. 오류 페이지는 정적 HTML(즉, 정적 리소스 디렉토리에 추가됨)이거나 템플릿을 사용하여 구축될 수 있다. 파일명은 정확한 상태 코드, 상태 코드 시리즈 마스크 또는 일치하는 항목이 없는 경우 기본값에 대한 오류여야 한다. 기본 오류 보기에 대한 경로는 error/error
인 반면 스프링 MVC에서는 기본 오류 보기는 error이다.
예를 들어 404를 정적 HTML 파일에 매핑하려면 디렉토리 구조는 다음과 같다.
src/
+- main/
+- java/
| + <source code>
+- resources/
+- public/
+- error/
| +- 404.html
+- <other public assets>
머스태치(Mustache) 템플릿을 사용하여 모든 5xx 오류를 매핑하려면 디렉토리 구조는 다음과 같다.
src/
+- main/
+- java/
| + <source code>
+- resources/
+- templates/
+- error/
| +- 5xx.mustache
+- <other templates>
웹 필터(Web Filters)
웹 플러스는 HTTP 요청-응답 교환을 필터링하기 위해 구현할 수 있는 웹필터(WebFilter) 인터페이스를 제공한다. 애플리케이션 컨텍스트에서 발견된 웹필터(WebFilter) 빈은 자동으로 각 교환을 필터링하는 데 사용된다.
필터의 순서가 중요한 경우 Ordered
를 구현하거나 @Order
로 어노테이션을 달 수 있습니다. 스프링 부트 자동 구성을 통해 웹 필터를 구성할 수 있다. 그렇게 하는 경우 다음 표에 표시된 순서가 사용된다.
웹필터 | 순서 |
---|---|
ServerHttpObservationFilter (마이크로미터 옵저버블리티(Observability)) | Ordered.HIGHEST_PRECEDENCE + 1 |
WebFilterChainProxy (스프링 시큐리티) | -100 |
HttpExchangesWebFilter | Ordered.LOWEST_PRECEDENCE - 10 |
8.2.2 임베디드 리액티브 서버 지원(Embedded Reactive Server Support)
스프링 부트에는 리액터 네티(Reactor Netty), 톰캣(Tomcat), 제티(Jetty) 및 언더토우(Undertow와) 같은 임베디드 리액티브 웹 서버에 대한 지원이 포함되어 있다. 대부분의 개발자는 완전히 구성된 인스턴스를 얻기 위해 적절한 “스타터”를 사용한다. 기본적으로 포함된 서버는 포트 8080에서 HTTP 요청을 수신한다.
8.2.3. 리액티브 서버 리소스 컨피규레이션(Reactive Server Resources Configuration)
리액터 네티 또는 제티 서버를 자동 구성할 때 스프링 부트는 서버 인스턴스에 HTTP 리소스를 제공하는 특정 빈(리액터리소스팩토리(ReactorResourceFactory) 또는 제티리소스팩토리(JettyResourceFactory))을 생성한다.
기본적으로, 이러한 리소스는 다음과 같은 경우 최적의 성능을 위해 리액터 네티(Reactor Netty) 및 제티(Jetty) 클라이언트와도 공유된다.
- 서버와 클라이언트에 동일한 기술이 사용된다.
- 클라이언트 인스턴스는 스프링 부트에 의해 자동 구성된
WebClient.Builder
빈을 사용하여 구축한다.
개발자는 커스텀 리액터리소스팩토리(ReactorResourceFactory) 또는 제티리소스팩토리(JettyResourceFactory) 빈을 제공하여 제티 및 리액터 네티에 대한 리소스 구성을 오버라이드할 수 있다. 이는 클라이언트와 서버 모두에 적용된다.
웹클라이언트(WebClient) 런타임 절에서 클라이언트측 리소스 구성에 대해 자세히 알아볼 수 있다.
8.3. 정상 종료(Graceful Shutdown)
4개의 내장 웹 서버(제티, 리액터 네티, 톰캣 및 언더토우)와 리액티브 및 서블릿 기반 웹 애플리케이션 모두에서 정상 종료(Graceful Shutdown)가 지원된다. 이는 애플리케이션 컨텍스트를 닫는 과정의 일부로 발생하며 스마트생명주기(SmartLifecycle) 빈을 중지하는 초기 단계에서 수행된다. 이 처리 중지는 기존 요청의 완료는 허용되지만 새 요청을 허용하지 않으며 유예 기간에 제한시간을 둔다. 새 요청이 허용되지 않는 정확한 방법은 사용 중인 웹 서버에 따라 다르다. 제티, 리액터 네티 및 톰캣은 네트워크 계층에서 요청 수락을 중지한다. 언더토우는 요청을 수락하지만 서비스 이용 불가(503) 응답으로 즉시 응답한다.
톰캣을 사용한 정상 종료에는 톰캣 9.0.33 이상이 필요하다.
단계적 종료 활성화는 다음 예와 같이 server.shutdown
프로퍼티를 구성한다.
프로퍼티스(Properties)
server.shutdown=graceful
Yaml
server:
shutdown: "graceful"
제한시간을 구성하려면 다음 예와 같이 spring.lifecycle.timeout-per-shutdown-phase
프로퍼티를 구성한다.
프로퍼티스(Properties)
spring.lifecycle.timeout-per-shutdown-phase=20s
Yaml
spring:
lifecycle:
timeout-per-shutdown-phase: "20s"
적절한 SIGTERM 신호를 보내지 않으면 IDE에서 정상 종료를 사용하는 것이 제대로 작동하지 않을 수 있다. 자세한 내용은 IDE 문서를 참고하자.
스프링 시큐리티(Spring Security)
스프링 시큐리티가 클래스패스에 있으면 기본적으로 웹 애플리케이션은 보호된다. 스프링 부트는 스프링 시큐리티의 콘텐츠 협상 전략(content-negotiation strategy)을 사용하여 httpBasic
또는 formLogin
을 사용할지 여부를 결정한다. 웹 애플리케이션에 메서드 레벨 시큐리티(method-level security)를 추가하려면 원하는 설정으로 @EnableGlobalMethodSecurity
를 추가할 수도 있다. 추가 정보는 스프링 시큐리티 레퍼런스 가이드에서 찾을 수 있다.
기본 유저디테일서비스(UserDetailsService)
에는 단일 사용자가 있다. 사용자명은 user
이고 비밀번호는 랜덤이며 다음 예와 같이 애플리케이션이 시작될 때 WARN
레벨에서 인쇄된다.
Using generated security password: 78fa095d-3f4c-48b1-ad50-e24c31d5cf35
This generated password is for development use only. Your security configuration must be updated before running your application in production.
로깅 구성을 세부적으로 조정하는 경우
org.springframework.boot.autoconfigure.security
카테고리가WARN
레벨 메시지를 기록하도록 설정되어 있는지 확인하자. 그렇지 않으면 기본 비밀번호가 인쇄되지 않는다.
spring.security.user.name
및 spring.security.user.password
를 제공하여 사용자명과 비밀번호를 변경할 수 있다.
웹 애플리케이션에서 기본적으로 제공되는 기본 기능은 다음과 같다.
- 인 메모리 저장소가 있는
유저디테일서비스(UserDetailsService)
(또는웹플럭스(WebFlux)
애플리케이션의 경우리액티브유저디테일서비스(ReactiveUserDetailsService)
) 빈과 생성된 비밀번호가 있는 단일 사용자(사용자 프로퍼티스는SecurityProperties.User
참고). - 전체 애플리케이션(액추에이터(actuator)가 클래스패스에 있는 경우 액추에이터 엔드포인트 포함)에 대한 양식 기반 로그인 또는 HTTP 기본 보안(요청의
Accept
헤더에 따라 다름). - 인증 이벤트 게시(publishing)를 위한
디폴트어센티케이션이벤트퍼블리셔(DefaultAuthenticationEventPublisher)
이다.
빈을 추가하여 다른 어센티케이션이벤트퍼블리셔(AuthenticationEventPublisher)
를 제공할 수 있다.
8.4.1. MVC 시큐리티(MVC Security)
기본 보안 구성은 시큐리티오토컨피규레이션(SecurityAutoConfiguration)
및 유저디테일서비스오토컨피규레이션(UserDetailsServiceAutoConfiguration)
에서 구현된다. 시큐리티오토컨피규레이션(SecurityAutoConfiguration)
은 웹 보안을 위해 스프링부트웹시큐리티컨피규레이션(SpringBootWebSecurityConfiguration)
을 가져오고 유저디테일서비스오토컨피규레이션(UserDetailsServiceAutoConfiguration)
은 웹이 아닌 애플리케이션에도 관련된 인증을 구성한다. 기본 웹 애플리케이션 보안 구성을 완전히 끄거나 OAuth2 클라이언트 및 리소스 서버와 같은 여러 스프링 보안 컴포넌트를 결합하려면 시큐리티필터체인(SecurityFilterChain)
타입의 빈을 추가하자 이렇게 해도 유저디테일서비스(UserDetailsService)
구성 또는 액추에이터(Actuator)의 보안이 비활성화되지는 않는다.
유저디테일서비스(UserDetailsService)
구성도 끄려면 유저디테일서비스(UserDetailsService)
, 어센티케이션프로바이더(AuthenticationProvider)
또는 어센티케이션매니저(AuthenticationManager)
타입의 빈을 추가할 수 있다.
커스텀 시큐리티필터체인(SecurityFilterChain)
빈을 추가하여 접근 규칙을 오버라이드할 수 있다. 스프링 부트는 액추에이터(Actuator) 엔드포인트 및 정적 리소스에 대한 접근 규칙을 오버라이드하는 데 사용할 수 있는 편리한 방법을 제공한다. 엔드포인트리퀘스트(EndpointRequest)
를 사용하여 Management.endpoints.web.base-path
프로퍼티를 기반으로 하는 리퀘스트매처(RequestMatcher)
를 생성할 수 있다. 패스리퀘스트(PathRequest)
를 사용하면 일반적으로 사용되는 위치의 리소스에 대한 리퀘스트매처(RequestMatcher)
를 생성할 수 있다.
8.4.2. 웹플럭스 시큐리티(WebFlux Security)
스프링 MVC 애플리케이션과 마찬가지로 spring-boot-starter-security
의존성을 추가하여 웹플럭스 애플리케이션을 보호할 수 있다. 기본 보안 구성은 리액티브시큐리티오토컨피규레이션(ReactiveSecurityAutoConfiguration)
및 유저디테일즈오토컨피규레이션(UserDetailsServiceAutoConfiguration)
에서 구현된다. 리액티브시큐리티오토컨피규레이션(ReactiveSecurityAutoConfiguration)
은 웹 보안을 위해 웹플럭스시큐리티컨피규레이션(WebFluxSecurityConfiguration)
을 가져오고 유저디테일즈서비스오토컨피규레이션(UserDetailsServiceAutoConfiguration)
은 웹이 아닌 애플리케이션에도 관련된 인증을 구성한다. 기본 웹 애플리케이션 보안 구성을 완전히 끄려면 웹필터체인프록시(WebFilterChainProxy)
타입의 빈을 추가할 수 있다. 이렇게 해도 유저디테일즈서비스(UserDetailsService)
구성 또는 액추에이터(Actuator)
의 보안이 비활성화되지 않는다.
유저디테일즈서비스(UserDetailsService)
구성도 끄려면 리액티브유저디테일즈서비스(ReactiveUserDetailsService)
또는 리액티브어센티케이션매니저(ReactiveAuthenticationManager)
유형의 빈을 추가하면 된다.
접근 규칙과 OAuth 2 클라이언트 및 리소스 서버와 같은 여러 스프링 시큐리티 컴포넌트의 사용은 커스텀 시큐리티웹필터체인(SecurityWebFilterChain)
빈을 추가하여 구성할 수 있다. 스프링 부트는 액추에이터(Actuator) 엔드포인트 및 정적 리소스에 대한 접근 규칙을 오버라이드하는 데 사용할 수 있는 편리한 방법을 제공한다. 엔드포인트리퀘스트(EndpointRequest)를 사용하여 Management.endpoints.web.base-path
프로퍼티를 기반으로 하는 서버웹익스체인지매처(ServerWebExchangeMatcher)
를 생성할 수 있다.
패스리퀘스트(PathRequest)
를 사용하면 일반적으로 사용되는 위치의 리소스에 대한 서버웹익스체인지매처(ServerWebExchangeMatcher)
를 만들 수 있다.
예를 들어, 다음과 같은 항목을 추가하여 보안 구성을 커스텀할 수 있다.
자바
import org.springframework.boot.autoconfigure.security.reactive.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration(proxyBeanMethods = false)
public class MyWebFluxSecurityConfiguration {
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange((exchange) -> { exchange.matchers(PathRequest.toStaticResources().atCommonLocations()).permitAll();
exchange.pathMatchers("/foo", "/bar").authenticated();
});
http.formLogin(withDefaults());
return http.build();
}
}
코틀린
import org.springframework.boot.autoconfigure.security.reactive.PathRequest
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.Customizer
import org.springframework.security.config.Customizer.withDefaults
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.web.server.SecurityWebFilterChain
@Configuration(proxyBeanMethods = false)
class MyWebFluxSecurityConfiguration {
@Bean
fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http.authorizeExchange { spec -> spec.matchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
spec.pathMatchers("/foo", "/bar").authenticated()
}
http.formLogin(withDefaults())
return http.build()
}
}
8.4.3. 오어스2(OAuth2)
OAuth2는 스프링에서 지원하는 널리 사용되는 인가(authorization) 프레임워크이다.
클라이언트(Client)
클래스패스에 spring-security-oauth2-client
가 있는 경우 자동 구성을 활용하여 OAuth2/Open ID Connect 클라이언트를 설정할 수 있다. 이 구성은 오어스2클라이언트프로퍼티스(OAuth2ClientProperties)
아래의 프로퍼티스를 활용한다. 서블릿과 리액티브 애플리케이션 모두에 동일 프로퍼티스를 적용할 수 있다.
다음 예제와 같이 spring.security.oauth2.client
접두사 아래에 여러 오어스2(OAuth2) 클라이언트 및 공급자를 등록할 수 있다.
프로퍼티스(Properties)
spring.security.oauth2.client.registration.my-login-client.client-id=abcd
spring.security.oauth2.client.registration.my-login-client.client-secret=password
spring.security.oauth2.client.registration.my-login-client.client-name=Client for OpenID Connect
spring.security.oauth2.client.registration.my-login-client.provider=my-oauth-provider
spring.security.oauth2.client.registration.my-login-client.scope=openid,profile,email,phone,address
spring.security.oauth2.client.registration.my-login-client.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
spring.security.oauth2.client.registration.my-login-client.client-authentication-method=client_secret_basic
spring.security.oauth2.client.registration.my-login-client.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.my-client-1.client-id=abcd
spring.security.oauth2.client.registration.my-client-1.client-secret=password
spring.security.oauth2.client.registration.my-client-1.client-name=Client for user scope
spring.security.oauth2.client.registration.my-client-1.provider=my-oauth-provider
spring.security.oauth2.client.registration.my-client-1.scope=user
spring.security.oauth2.client.registration.my-client-1.redirect-uri={baseUrl}/authorized/user
spring.security.oauth2.client.registration.my-client-1.client-authentication-method=client_secret_basic
spring.security.oauth2.client.registration.my-client-1.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.my-client-2.client-id=abcd
spring.security.oauth2.client.registration.my-client-2.client-secret=password
spring.security.oauth2.client.registration.my-client-2.client-name=Client for email scope
spring.security.oauth2.client.registration.my-client-2.provider=my-oauth-provider
spring.security.oauth2.client.registration.my-client-2.scope=email
spring.security.oauth2.client.registration.my-client-2.redirect-uri={baseUrl}/authorized/email
spring.security.oauth2.client.registration.my-client-2.client-authentication-method=client_secret_basic
spring.security.oauth2.client.registration.my-client-2.authorization-grant-type=authorization_code
spring.security.oauth2.client.provider.my-oauth-provider.authorization-uri=https://my-auth-server.com/oauth2/authorize
spring.security.oauth2.client.provider.my-oauth-provider.token-uri=https://my-auth-server.com/oauth2/token
spring.security.oauth2.client.provider.my-oauth-provider.user-info-uri=https://my-auth-server.com/userinfo
spring.security.oauth2.client.provider.my-oauth-provider.user-info-authentication-method=header
spring.security.oauth2.client.provider.my-oauth-provider.jwk-set-uri=https://my-auth-server.com/oauth2/jwks
spring.security.oauth2.client.provider.my-oauth-provider.user-name-attribute=name
Yaml
spring:
security:
oauth2:
client:
registration:
my-login-client:
client-id: "abcd"
client-secret: "password"
client-name: "Client for OpenID Connect"
provider: "my-oauth-provider"
scope: "openid,profile,email,phone,address"
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
client-authentication-method: "client_secret_basic"
authorization-grant-type: "authorization_code"
my-client-1:
client-id: "abcd"
client-secret: "password"
client-name: "Client for user scope"
provider: "my-oauth-provider"
scope: "user"
redirect-uri: "{baseUrl}/authorized/user"
client-authentication-method: "client_secret_basic"
authorization-grant-type: "authorization_code"
my-client-2:
client-id: "abcd"
client-secret: "password"
client-name: "Client for email scope"
provider: "my-oauth-provider"
scope: "email"
redirect-uri: "{baseUrl}/authorized/email"
client-authentication-method: "client_secret_basic"
authorization-grant-type: "authorization_code"
provider:
my-oauth-provider:
authorization-uri: "https://my-auth-server.com/oauth2/authorize"
token-uri: "https://my-auth-server.com/oauth2/token"
user-info-uri: "https://my-auth-server.com/userinfo"
user-info-authentication-method: "header"
jwk-set-uri: "https://my-auth-server.com/oauth2/jwks"
user-name-attribute: "name"
오픈ID 커넥션(OpenID Connect) 검색을 지원하는 오픈ID 커넥션(OpenID Connect) 공급자(provider)의 경우 구성을 더욱 단순화할 수 있다. 공급자는 발급자 ID(Issuer Identifier)로 주장하는 URI인 issuer-uri
로 구성되어야 한다. 예를 들어, 제공된 issuer-uri가 “https://example.com”인 경우 “OpenID 공급자 구성 요청”은 “https://example.com/.well-known/openid-configuration”으로 이루어진다. 결과는 “OpenID 공급자 구성 응답”이 될 것으로 예상된다. 다음 예에서는 issuer-uri를 사용하여 OpenID Connect 공급자를 구성하는 방법을 보여준다.
프로퍼티스(Properties)
spring.security.oauth2.client.provider.oidc-provider.issuer-uri=https://dev-123456.oktapreview.com/oauth2/default/
Yaml
spring:
security:
oauth2:
client:
provider:
oidc-provider:
issuer-uri: "https://dev-123456.oktapreview.com/oauth2/default/"
기본적으로, 스프링 시큐리티의 오어스2로그인어센티케이션필터(OAuth2LoginAuthenticationFilter)
는 /login/oauth2/code/*
와 일치하는 URL만 처리한다. 다른 패턴을 사용하도록 리디렉션 URI를 커스텀하려면 해당 커스텀 패턴을 처리하기 위한 구성을 제공해야 한다. 예를 들어 서블릿 애플리케이션의 경우 다음과 유사한 자체 시큐리티필터체인(SecurityFilterChain)
을 추가할 수 있다.
자바
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
public class MyOAuthClientConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((requests) -> requests
.anyRequest().authenticated()
)
.oauth2Login((login) -> login
.redirectionEndpoint((endpoint) -> endpoint
.baseUri("/login/oauth2/callback/*")
)
);
return http.build();
}
}
코틀린
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.web.SecurityFilterChain
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
open class MyOAuthClientConfiguration {
@Bean
open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
oauth2Login {
redirectionEndpoint {
baseUri = "/login/oauth2/callback/*"
}
}
}
return http.build()
}
}
스프링 부트는 클라이언트 등록 관리를 위해 스프링 시큐리티에서 사용되는 인메모리오어스2어솔라이즈드클라이언트서비스(InMemoryOAuth2AuthorizedClientService)
를 자동 구성한다. 인메모리오어스2어솔라이즈드클라이언트서비스(InMemoryOAuth2AuthorizedClientService)
는 기능이 제한되어 있으므로 개발 환경에만 사용하는 것이 좋다. 프로덕션 환경의 경우 Jdbc오어스2어솔라이즈드클라이언트서비스(JdbcOAuth2AuthorizedClientService)
를 사용하거나 오어스2어솔라이즈드클라이언트서비스(OAuth2AuthorizedClientService)
의 자체 구현을 만드는 것을 고려해보자.
일반 공급자를 위한 OAuth2 클라이언트 등록(OAuth2 Client Registration for Common Providers)
구글, 깃헙, 페이스북 및 옥타를 포함한 일반적인 OAuth2 및 OpenID 공급자의 경우 공급자 기본값 집합(각각 google, github, facebook 및 okta)을 제공한다.
이러한 공급자를 커스텀할 필요가 없는 경우 공급자 특성을 기본값을 유추해야 하는 provider
애트리뷰트로 설정할 수 있다. 또한 클라이언트 등록 키가 기본 지원 공급자와 일치하면 스프링 부트는 이를 추론한다.
즉, 다음 예의 두 구성에서는 구글 공급자를 사용한다.
프로퍼티스(Properties)
spring.security.oauth2.client.registration.my-client.client-id=abcd
spring.security.oauth2.client.registration.my-client.client-secret=password
spring.security.oauth2.client.registration.my-client.provider=google
spring.security.oauth2.client.registration.google.client-id=abcd
spring.security.oauth2.client.registration.google.client-secret=password
Yaml
spring:
security:
oauth2:
client:
registration:
my-client:
client-id: "abcd"
client-secret: "password"
provider: "google"
google:
client-id: "abcd"
client-secret: "password"
리소스 서버(Resource Server)
클래스패스에 spring-security-oauth2-resource-server
가 있으면 스프링 부트는 OAuth2 리소스 서버를 설정할 수 있다. JWT 구성의 경우 다음 예와 같이 JWK 설정 URI 또는 OIDC 발급자 URI를 지정해야 한다.
프로퍼티스(Properties)
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://example.com/oauth2/default/v1/keys
Yaml
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: "https://example.com/oauth2/default/v1/keys"
프로퍼티스(Properties)
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://dev-123456.oktapreview.com/oauth2/default/
Yaml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: "https://dev-123456.oktapreview.com/oauth2/default/"
인증 서버가 JWK 세트 URI를 지원하지 않는 경우 JWT 서명을 확인하는 데 사용되는 공개 키로 리소스 서버를 구성할 수 있다. 이는 spring.security.oauth2.resourceserver.jwt.public-key-location
프로퍼티를 사용하여 수행할 수 있다. 여기서 값은 PEM 인코딩 x509 형식의 공개 키가 포함된 파일을 가리켜야 한다.
프로퍼티스(Properties)
spring.security.oauth2.resourceserver.jwt.audiences[0]=my-audience
Yaml
spring:
security:
oauth2:
resourceserver:
jwt:
audiences:
- "my-audience"
서블릿과 리액티브 애플리케이션 모두에 동일한 프로퍼티스를 적용할 수 있다. 또는 서블릿 애플리케이션을 위한 자체 Jwt디코더(JwtDecoder) 빈을 정의하거나 리액티브 애플리케이션을 위한 리액티브Jwt디코더(ReactiveJwtDecoder)를 정의할 수 있다.
JWT 대신 불투명(opaque) 토큰이 사용되는 경우 자체 검사를 통해 토큰의 유효성을 검사하도록 다음내용 처럼 프로퍼티스를 구성할 수 있다.
프로퍼티스(Properties)
spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://example.com/check-token
spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id
spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret
Yaml
spring:
security:
oauth2:
resourceserver:
opaquetoken:
introspection-uri: "https://example.com/check-token"
client-id: "my-client-id"
client-secret: "my-client-secret"
다시 말하지만, 서블릿과 리액티브 애플리케이션 모두에 동일한 프로퍼티스를 적용할 수 있다. 또는 서블릿 애플리케이션에 대해 자체 오페이크토큰인트로스펙터(OpaqueTokenIntrospector) 빈을 정의하거나 반응 애플리케이션에 대해 리액티브오페이크토큰인트로스펙터(ReactiveOpaqueTokenIntrospector)를 정의할 수 있다.
Authorization 서버(Authorization Server)
클래스패스에 spring-security-oauth2-authorization-server
가 있는 경우 일부 자동 구성을 활용하여 서블릿 기반 OAuth2 인증 서버를 설정할 수 있다.
다음 예제와 같이 spring.security.oauth2.authorizationserver.client
접두사 아래에 여러 OAuth2 클라이언트를 등록할 수 있다.
프로퍼티스(Properties)
spring.security.oauth2.authorizationserver.client.my-client-1.registration.client-id=abcd
spring.security.oauth2.authorizationserver.client.my-client-1.registration.client-secret={noop}secret1
spring.security.oauth2.authorizationserver.client.my-client-1.registration.client-authentication-methods[0]=client_secret_basic
spring.security.oauth2.authorizationserver.client.my-client-1.registration.authorization-grant-types[0]=authorization_code
spring.security.oauth2.authorizationserver.client.my-client-1.registration.authorization-grant-types[1]=refresh_token
spring.security.oauth2.authorizationserver.client.my-client-1.registration.redirect-uris[0]=https://my-client-1.com/login/oauth2/code/abcd
spring.security.oauth2.authorizationserver.client.my-client-1.registration.redirect-uris[1]=https://my-client-1.com/authorized
spring.security.oauth2.authorizationserver.client.my-client-1.registration.scopes[0]=openid
spring.security.oauth2.authorizationserver.client.my-client-1.registration.scopes[1]=profile
spring.security.oauth2.authorizationserver.client.my-client-1.registration.scopes[2]=email
spring.security.oauth2.authorizationserver.client.my-client-1.registration.scopes[3]=phone
spring.security.oauth2.authorizationserver.client.my-client-1.registration.scopes[4]=address
spring.security.oauth2.authorizationserver.client.my-client-1.require-authorization-consent=true
spring.security.oauth2.authorizationserver.client.my-client-2.registration.client-id=efgh
spring.security.oauth2.authorizationserver.client.my-client-2.registration.client-secret={noop}secret2
spring.security.oauth2.authorizationserver.client.my-client-2.registration.client-authentication-methods[0]=client_secret_jwt
spring.security.oauth2.authorizationserver.client.my-client-2.registration.authorization-grant-types[0]=client_credentials
spring.security.oauth2.authorizationserver.client.my-client-2.registration.scopes[0]=user.read
spring.security.oauth2.authorizationserver.client.my-client-2.registration.scopes[1]=user.write
spring.security.oauth2.authorizationserver.client.my-client-2.jwk-set-uri=https://my-client-2.com/jwks
spring.security.oauth2.authorizationserver.client.my-client-2.token-endpoint-authentication-signing-algorithm=RS256
Yaml
spring:
security:
oauth2:
authorizationserver:
client:
my-client-1:
registration:
client-id: "abcd"
client-secret: "{noop}secret1"
client-authentication-methods:
- "client_secret_basic"
authorization-grant-types:
- "authorization_code"
- "refresh_token"
redirect-uris:
- "https://my-client-1.com/login/oauth2/code/abcd"
- "https://my-client-1.com/authorized"
scopes:
- "openid"
- "profile"
- "email"
- "phone"
- "address"
require-authorization-consent: true
my-client-2:
registration:
client-id: "efgh"
client-secret: "{noop}secret2"
client-authentication-methods:
- "client_secret_jwt"
authorization-grant-types:
- "client_credentials"
scopes:
- "user.read"
- "user.write"
jwk-set-uri: "https://my-client-2.com/jwks"
token-endpoint-authentication-signing-algorithm: "RS256"
client-secret
프로퍼티는 구성된 패스워드인코더(PasswordEncoder)
와 일치할 수 있는 형식이어야 한다. 패스워드인코더(PasswordEncoder)
의 기본 인스턴스는 PasswordEncoderFactories.createDelegatingPasswordEncoder()
을 통해 생성된다.
스프링 어소리제이션 서버(Spring Authorization Server)
에 제공되는 자동 구성 스프링 부트는 빠르게 시작하도록 설계됐다. 대부분의 애플리케이션은 커스텀이 필요하며 자동 구성을 오버라이드하기 위해 여러 빈을 정의하려고 한다.
다음 컴포넌트는 스프링 어소리제이션 서버(Spring Authorization Server)
와 관련된 자동 구성을 오버라이드하기 위해 빈으로 정의될 수 있다.
레지스터드클라이언트리포지터리(RegisteredClientRepository)
어소리제이션서버세팅즈(AuthorizationServerSettings)
시큐리티필터체인(SecurityFilterChain)
com.nimbusds.jose.jwk.source.JWKSource<com.nimbusds.jose.proc.SecurityContext>
Jwt디코더(JwtDecoder)
스프링 부트는 등록된 클라이언트 관리를 위해 스프링 어소리제이션 서버(Spring Authorization Server)
에서 사용되는 인메모리레지스터드클라이언트리포지터리(InMemoryRegisteredClientRepository)
를 자동 구성한다. 인메모리레지스터드클라이언트리포지터리(InMemoryRegisteredClientRepository)
는 기능이 제한되어 있으므로 개발 환경에만 사용하는 것이 좋다. 프로덕션 환경의 경우 Jdbc레지스터드클라이언드리포지터리(JdbcRegisteredClientRepository)
를 사용하거나 레지스터드클라이언트리포지터리(RegisteredClientRepository)
의 자체 구현을 만드는 것을 고려하자.
추가 정보는 스프링 어소리제이션 서버 레퍼런스 가이드의 시작하기(Getting Started) 장에서 찾을 수 있다.
8.4.4. 샘엘(SAML) 2.0
신뢰 당사자(Relying Party)
클래스패스에 spring-security-saml2-service-provider
가 있는 경우 일부 자동 구성을 활용하여 SAML 2.0 신뢰 당사자를 설정할 수 있다. 이 구성은 Saml2RelyingPartyProperties
아래의 프로퍼티스를 활용한다.
신뢰 당사자 등록은 아이덴티티 공급자(IDP: Identity Provider)와 서비스 공급자(SP: Service Provider) 간의 쌍 구성을 나타낸다. 다음 예제와 같이 spring.security.saml2.relyingparty
접두사 아래에 여러 신뢰 당사자를 등록할 수 있다.
프로퍼티스(Properties)
spring.security.saml2.relyingparty.registration.my-relying-party1.signing.credentials[0].private-key-location=path-to-private-key
spring.security.saml2.relyingparty.registration.my-relying-party1.signing.credentials[0].certificate-location=path-to-certificate
spring.security.saml2.relyingparty.registration.my-relying-party1.decryption.credentials[0].private-key-location=path-to-private-key
spring.security.saml2.relyingparty.registration.my-relying-party1.decryption.credentials[0].certificate-location=path-to-certificate
spring.security.saml2.relyingparty.registration.my-relying-party1.singlelogout.url=https://myapp/logout/saml2/slo
spring.security.saml2.relyingparty.registration.my-relying-party1.singlelogout.response-url=https://remoteidp2.slo.url
spring.security.saml2.relyingparty.registration.my-relying-party1.singlelogout.binding=POST
spring.security.saml2.relyingparty.registration.my-relying-party1.assertingparty.verification.credentials[0].certificate-location=path-to-verification-cert
spring.security.saml2.relyingparty.registration.my-relying-party1.assertingparty.entity-id=remote-idp-entity-id1
spring.security.saml2.relyingparty.registration.my-relying-party1.assertingparty.sso-url=https://remoteidp1.sso.url
spring.security.saml2.relyingparty.registration.my-relying-party2.signing.credentials[0].private-key-location=path-to-private-key
spring.security.saml2.relyingparty.registration.my-relying-party2.signing.credentials[0].certificate-location=path-to-certificate
spring.security.saml2.relyingparty.registration.my-relying-party2.decryption.credentials[0].private-key-location=path-to-private-key
spring.security.saml2.relyingparty.registration.my-relying-party2.decryption.credentials[0].certificate-location=path-to-certificate
spring.security.saml2.relyingparty.registration.my-relying-party2.assertingparty.verification.credentials[0].certificate-location=path-to-other-verification-cert
spring.security.saml2.relyingparty.registration.my-relying-party2.assertingparty.entity-id=remote-idp-entity-id2
spring.security.saml2.relyingparty.registration.my-relying-party2.assertingparty.sso-url=https://remoteidp2.sso.url
spring.security.saml2.relyingparty.registration.my-relying-party2.assertingparty.singlelogout.url=https://remoteidp2.slo.url
spring.security.saml2.relyingparty.registration.my-relying-party2.assertingparty.singlelogout.response-url=https://myapp/logout/saml2/slo
spring.security.saml2.relyingparty.registration.my-relying-party2.assertingparty.singlelogout.binding=POST
Yaml
spring:
security:
saml2:
relyingparty:
registration:
my-relying-party1:
signing:
credentials:
- private-key-location: "path-to-private-key"
certificate-location: "path-to-certificate"
decryption:
credentials:
- private-key-location: "path-to-private-key"
certificate-location: "path-to-certificate"
singlelogout:
url: "https://myapp/logout/saml2/slo"
response-url: "https://remoteidp2.slo.url"
binding: "POST"
assertingparty:
verification:
credentials:
- certificate-location: "path-to-verification-cert"
entity-id: "remote-idp-entity-id1"
sso-url: "https://remoteidp1.sso.url"
my-relying-party2:
signing:
credentials:
- private-key-location: "path-to-private-key"
certificate-location: "path-to-certificate"
decryption:
credentials:
- private-key-location: "path-to-private-key"
certificate-location: "path-to-certificate"
assertingparty:
verification:
credentials:
- certificate-location: "path-to-other-verification-cert"
entity-id: "remote-idp-entity-id2"
sso-url: "https://remoteidp2.sso.url"
singlelogout:
url: "https://remoteidp2.slo.url"
response-url: "https://myapp/logout/saml2/slo"
binding: "POST"
SAML2 로그아웃의 경우 기본적으로 스프링 시큐리티의 Saml2로그아웃리퀘스트필터(Saml2LogoutRequestFilter)
및 Saml2로그아웃리스폰스필터(Saml2LogoutResponseFilter)
는 /logout/saml2/slo
와 일치하는 URL만 처리한다. AP 시작 로그아웃 요청이 전송되는 URL 또는 AP가 로그아웃 응답을 전송하는 응답 URL을 커스텀하고 다른 패턴을 사용하려면 해당 커스텀 패턴을 처리하기 위한 구성을 제공해야 한다. 예를 들어 서블릿 애플리케이션의 경우 다음과 유사한 자체 시큐리티필터체인(SecurityFilterChain)
을 추가할 수 있다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration(proxyBeanMethods = false)
public class MySamlRelyingPartyConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
http.saml2Login(withDefaults());
http.saml2Logout((saml2) -> saml2.logoutRequest((request) -> request.logoutUrl("/SLOService.saml2"))
.logoutResponse((response) -> response.logoutUrl("/SLOService.saml2")));
return http.build();
}
}
8.5. 스프링 세션(Spring Session)
스프링 부트는 광범위한 데이터 저장소에 대한 스프링 세션 자동 구성을 제공한다. 서블릿 웹 애플리케이션을 구축할 때 다음 저장소가 자동으로 구성될 수 있다.
- 레디스(Redis)
- JDBC
- 헤이즐캐스트(Hazelcast)
- 몽고DB(MongoDB)
또한, 아파치 지오드(Geode)용 스프링 부트는 아파치 지오드를 세션 저장소로 사용하기 위한 자동 구성을 제공한다.
서블릿 자동 구성은 @Enable*HttpSession
를 대체한다.
단일 스프링 세션 모듈이 클래스패스에 존재하는 경우 스프링 부트는 해당 저장소 구현을 자동으로 사용한다. 둘 이상의 구현이 있는 경우 스프링 부트는 특정 구현을 선택하기 위해 다음 순서를 사용한다.
- 레디스(Redis)
- JDBC
- 헤이즐캐스트(Hazelcast)
- 몽고DB(MongoDB)
- 레디스, JDBC, 헤이즐캐스트 및 몽고DB 중 어느 것도 사용할 수 없는 경우 세션레지스트리(SessionRepository)를 구성하지 않는다.
리액티브 웹 애플리케이션을 구축할 때 다음 저장소를 자동으로 구성할 수 있다.
- 레디스
- 몽고DB
리액티브 자동 구성은 @Enable*WebSession
를 대체한다.
서블릿 구성과 유사하게 구현이 두 개 이상인 경우 스프링 부트는 특정 구현을 선택하기 위해 다음 순서를 사용한다.
- 레디스
- 몽고DB
- 레디스와 몽고DB를 모두 사용할 수 없으면
리액티브세션리포지터리(ReactiveSessionRepository)
를 구성하지 않는다.
각 저장소에는 특정 추가 설정이 있다. 예를 들어, 다음 예와 같이 JDBC 저장소의 테이블명을 커스텀할 수 있다.
프로퍼티스(Properties)
spring.session.jdbc.table-name=SESSIONS
Yaml
spring:
session:
jdbc:
table-name: "SESSIONS"
세션 시간 초과를 설정하려면 spring.session.timeout
프로퍼티를 사용할 수 있다. 해당 프로퍼티가 서블릿 웹 애플리케이션으로 설정되지 않은 경우 자동 구성은 server.servlet.session.timeout
값으로 대체한다.
@Enable*HttpSession
(서블릿) 또는 @Enable*WebSession
(리액티브)을 사용하여 스프링 세션 구성을 제어할 수 있다. 이로 인해 자동 구성이 취소된다. 그런 다음 이전에 설명한 구성 프로퍼티스 대신 어노테이션 애드리뷰트를 사용하여 스프링 세션을 구성할 수 있다.
8.6. 스프링을 위한 그래프QL(Spring for GraphQL)
그래프QL(GraphQL) 애플리케이션을 구축하려는 경우 그래프QL용 스프링부트 자동 구성을 활용할 수 있다. 그래프QL용 스프링 프로젝트는 그래프QL 자바를 기반으로 한다. 최소한 spring-boot-starter-graphql 스타터가 필요하다. 그래프QL은 전송에 구애받지 않기 때문에 웹을 통해 그래프QL API를 노출하려면 애플리케이션에 하나 이상의 추가 스타터가 있어야 한다.
스타터 | 전송방식 | 구현 |
---|---|---|
spring-boot-starter-web | HTTP | 스프링 MVC |
spring-boot-starter-websocket | 웹소켓 | 서블릿 앱에 대한 웹소켓 |
spring-boot-starter-webflux | HTTP, 웹소켓 | 스프링 웹플럭스 |
spring-boot-starter-rsocket | TCP, 웹소켓 | 리액터 네티의 스프링 웹플럭스 |
8.6.1. 그래프QL 스키마(GraphQL Schema)
스프링 그래프QL 애플리케이션은 시작 시 정의된 스키마가 필요하다. 기본적으로 src/main/resources/graphql/**
아래에 “.graphqls” 또는 “.gqls” 스키마 파일을 작성할 수 있으며 스프링 부트는 이를 자동으로 선택한다. spring.graphql.schema.locations
를 사용하여 위치를 커스텀하고 spring.graphql.schema.file-extensions
를 사용하여 파일 확장자를 커스텀할 수 있다.
스프링 부트가 모든 애플리케이션 모듈에서 스키마 파일과 해당 위치에 대한 의존성을 감지하도록 하려면 spring.graphql.schema.locations
를 “classpath*:graphql/**/
“로 설정할 수 있다(classpath*
: 접두사 참고).
다음 내용에서는 두 가지 타입과 두 가지 쿼리를 정의하는 이 샘플 그래프QL 스키마를 살펴본다.
type Query {
greeting(name: String! = "Spring"): String!
project(slug: ID!): Project
}
""" A Project in the Spring portfolio """
type Project {
""" Unique string id used in URLs """
slug: ID!
""" Project name """
name: String!
""" URL of the git repository """
repositoryUrl: String!
""" Current support status """
status: ProjectStatus!
}
enum ProjectStatus {
""" Actively supported by the Spring team """
ACTIVE
""" Supported by the community """
COMMUNITY
""" Prototype, not officially supported yet """
INCUBATING
""" Project being retired, in maintenance mode """
ATTIC
""" End-Of-Lifed """
EOL
}
기본적으로 그래프iQL과 같은 도구에 필요하므로 스키마에서 필드 자체 검사가 허용된다. 스키마에 대한 정보를 노출하지 않으려면 spring.graphql.schema.introspection.enabled
를 false
로 설정하여 자체 검사를 비활성화할 수 있다.
8.6.2. 그래프QL 런타임와이어링(GraphQL RuntimeWiring)
그래프QL 자바 RuntimeWiring.Builder
를 사용하여 커스텀 스칼라 타입, 지시문(directive), 타입 리졸버(type resolver), 데이터패처(DataFetcher) 등을 등록할 수 있다. 스프링 구성에서 런타임와이어링컨피규어러(RuntimeWiringConfigurer) 빈을 선언하여 RuntimeWiring.Builder
에 접근할 수 있다. 스프링 부트는 이러한 빈을 감지하여 그래프Ql소스(GraphQlSource) 빌더에 추가한다.
그러나 일반적으로 애플리케이션은 데이터패처(DataFetcher)를 직접 구현하지 않고 대신 어노테이션이 달린 컨트롤러를 생성한다. 스프링 부트는 어노테이션 핸들러 메서드가 있는 @Controller
클래스를 자동 감지하고 이를 데이터패처(DataFetchers)로 등록한다. @Controller
클래스를 사용한 인사말 쿼리의 샘플 구현은 다음과 같다.
자바
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
@Controller
public class GreetingController {
@QueryMapping
public String greeting(@Argument String name) {
return "Hello, " + name + "!";
}
}
코틀린
import org.springframework.graphql.data.method.annotation.Argument
import org.springframework.graphql.data.method.annotation.QueryMapping
import org.springframework.stereotype.Controller
@Controller
class GreetingController {
@QueryMapping
fun greeting(@Argument name: String): String {
return "Hello, $name!"
}
}
8.6.3. 쿼리dsl과 쿼리예제 리포지터리 지원(Querydsl and QueryByExample Repositories Support)
스프링 데이터는 쿼리dsl 및 QueryByExample 리포지터리를 모두 지원한다. 스프링 그래프QL은 쿼리dsl 및 QueryByExample 리포지터리를 데이터패터(DataFetcher)로 구성할 수 있다.
@GraphQlRepository
로 어노테이션이 추가되고 다음 중 하나를 확장하는 스프링 데이터 리포지터리
- 쿼리dsl프레디케이드익스큐터(QuerydslPredicateExecutor)
- 리액티브쿼리dsl프레디케이트익스큐터(ReactiveQuerydslPredicateExecutor)
- 쿼리바이이그잼플익스큐터(QueryByExampleExecutor)
- 리액티브쿼리바이이그잼플익스큐터(ReactiveQueryByExampleExecutor) 스프링 부트에 의해 감지되고 탑 레벨 쿼리 매칭을 위한 데이터패처(DataFetcher)의 후보로 여겨진다.
8.6.4. 전송(Transports)
HTTP와 웹소켓(HTTP and WebSocket)
그래프QL HTTP 엔드포인트는 기본적으로 HTTP POST /graphql
에 있다. 경로는 spring.graphql.path
로 커스텀할 수 있다.
스프링 MVC와 스프링 웹플럭스 모두에 대한 HTTP 엔드포인트는 @Order
가 0인 라우터펑션(RouterFunction) 빈에 의해 제공된다. 자신만의 라우터펑션(RouterFunction) 빈을 정의하는 경우 적절한 @Order
어노테이션을 추가하여 올바르게 정렬되도록 할 수 있다.
그래프QL 웹소켓 엔드포인트는 기본적으로 꺼져 있다. 활성화하려면:
- 서블릿 애플리케이션의 경우 웹소켓 스타터
spring-boot-starter-websocket
을 추가하자. - 웹플럭스 애플리케이션의 경우 추가 의존성이 필요하지 않는다.
- 두 경우 모두
spring.graphql.websocket.path
애플리케이션 프로퍼티를 설정해야 한다.
스프링 그래프QL은 웹 인터셉션 모델(Web Interception model)을 제공한다. 이는 HTTP 요청 헤더에서 정보를 검색하여 그래프QL 컨텍스트에 설정하거나 동일한 컨텍스트에서 정보를 가져와 응답 헤더에 쓰는 데 매우 유용하다. 스프링 부트를 사용하면 웹인터셉터(WebInterceptor) 빈을 선언하여 웹 전송에 등록할 수 있다.
스프링 MVC와 스프링 웹플럭스는 CORS(Cross-Origin Resource Sharing) 요청을 지원한다. CORS는 다른 도메인을 사용하는 브라우저에서 섭근하는 그래프QL 애플리케이션에 대한 웹 구성의 중요한 부분이다.
스프링 부트는 spring.graphql.cors.*
네임스페이스에서 많은 구성 프로퍼티를 지원한다. 여기에 간단한 구성 샘플이 있다.
프로퍼티스(Properties)
spring.graphql.cors.allowed-origins=https://example.org
spring.graphql.cors.allowed-methods=GET,POST
spring.graphql.cors.max-age=1800s
Yaml
spring:
graphql:
cors:
allowed-origins: "https://example.org"
allowed-methods: GET,POST
max-age: 1800s
R소켓(RSocket)
R소켓(RSocket)은 웹소켓 또는 TCP 위 전송도 지원된다. R소켓(RSocket) 서버가 구성되면 spring.graphql.rsocket.mapping
을 사용하여 특정 경로에서 그래프QL 핸들러를 구성할 수 있다. 예를 들어 해당 매핑을 “graphql”로 구성하면 R소켓그래프Ql클라이언트(RSocketGraphQlClient)로 요청을 보낼 때 이를 경로로 사용할 수 있다.
스프링 부트는 컴포넌트에 삽입할 수 있는 RSocketGraphQlClient.Builder<?>
빈을 자동 구성한다.
자바
@Component
public class RSocketGraphQlClientExample {
private final RSocketGraphQlClient graphQlClient;
public RSocketGraphQlClientExample(RSocketGraphQlClient.Builder<?> builder) {
this.graphQlClient = builder.tcp("example.spring.io", 8181).route("graphql").build();
}
}
코틀린
@Component
class RSocketGraphQlClientExample(private val builder:
RSocketGraphQlClient.Builder<*>) {
}
그런 다음 요청을 보낸다.
자바
Mono<Book> book = this.graphQlClient.document("{ bookById(id: \"book-1\"){ id name pageCount author } }")
.retrieve("bookById")
.toEntity(Book.class);
코틀린
val book = graphQlClient.document(
"""
{
bookById(id: "book-1"){
id
name
pageCount
author
}
}
""" ).retrieve("bookById").toEntity(Book::class.java)
8.6.5. 예외 핸들링(Exception Handling)
스프링 그래프QL을 사용하면 애플리케이션이 순차적으로 호출되는 하나 이상의 스프링 데이터패처익셉션리졸버(DataFetcherExceptionResolver)
컴포넌트를 등록할 수 있다. 예외는 graphql.GraphQLError
객체 목록으로 해결되어야 한다. 스프링 그래프QL 예외 처리 문서를 참고하자. 스프링 부트는 데이터패처익셉션리졸버(DataFetcherExceptionResolver) 빈을 자동으로 감지하고 이를 GraphQlSource.Builder
에 등록한다.
8.6.6. 그래피QL과 스키마 프린터(GraphiQL and Schema printer)
스프링 그래프QL은 개발자가 그래프QL API를 사용하거나 개발할 때 도움을 주기 위한 인프라를 제공한다.
스프링 그래프QL은 기본적으로 “/graphiql”에 노출되는 기본 그래피QL(GraphiQL) 페이지와 함께 제공된다. 이 페이지는 기본적으로 비활성화되어 있으며 spring.graphql.graphiql.enabled
속성으로 활성화할 수 있다. 이러한 페이지를 노출하는 많은 애플리케이션은 커스텀 빌드를 선호한다. 기본 구현은 개발 중에 매우 유용하므로 개발 중에 spring-boot-devtools
를 사용하면 자동으로 노출된다.
spring.graphql.schema.printer.enabled
프로퍼티가 활성화되면 /graphql/schema
에서 그래프QL 스키마를 텍스트 포맷으로 노출하도록 선택할 수도 있다.
8.7. 스프링 헤이티오스(Spring HATEOAS)
하이퍼미디어를 활용하는 레스트풀(RESTful) API를 개발하는 경우 스프링 부트는 대부분 애플리케이션에서 잘 작동하는 스프링 헤이티오스에 대한 자동 구성을 제공한다. 자동 구성은 @EnableHypermediaSupport
를 사용할 필요성을 대체하고 링크디스커버러(LinkDiscoverers)(클라이언트 측 지원용) 및 응답을 원하는 표현으로 올바르게 마샬링하도록 구성된 오브젝트매퍼(ObjectMapper)를 포함하여 하이퍼미디어 기반 애플리케이션을 쉽게 구축할 수 있도록 여러 빈을 등록한다. 오브젝트매퍼(ObjectMapper)
는 다양한 spring.jackson.*
프로퍼티를 설정하거나, 존재하는 경우 잭슨2오브젝트매퍼빌더(Jackson2ObjectMapperBuilder) 빈을 사용하여 커스텀한다.
@EnableHypermediaSupport
를 사용하여 스프링 헤이티오스 구성을 제어할 수 있다. 이렇게 하면 앞에서 설명한 오브젝트매퍼 커스텀이 비활성화된다.
spring-boot-starter-hateoas
는 스프링 MVC에만 적용되며 스프링 웹플럭스와 결합하면 안 된다. 스프링 웹플럭스와 함께 스프링 헤이티오스를 사용하려면 spring-boot-starter-webflux
와 함께 org.springframework.hateoas:spring-hateoas
에 직접 의존성을 추가할 수 있다.
8.8. 다음에 읽을 내용(What to Read Next)
이제 스프링 부트를 사용하여 웹 애플리케이션을 개발하는 방법을 잘 이해하게 됐다. 다음 몇 장에서는 스프링 부트가 다양한 데이터 기술, 메시징 시스템 및 기타 IO 기능과 통합되는 방법을 설명한다. 애플리케이션의 요구 사항에 따라 이들 중 하나를 선택할 수 있다.