여러 요청이 같은 데이터를 공유해야하는 상황(온라인 쇼핑몰 장바구니 등)에 세션 활용 가능.
스프링 MVC에서 HTTP 세션에 데이터 관리하는 방법 세 가지
하나의 컨트롤러에서 여러 요청 간에 데이터를 공유하는 경우에 효과적
HTTP 세션에서 관리하고자 하는 객체를 우선 Model에 저장한다.
그 후 세션에 관리하겠다고 지정한 객체만 세션에 저장(export) 된다. 반대로 세션에서 관리되는 객체 중에서 세션에서 관리하겠다고 지정한 객체가 Model에 저장(import)된다.
types 속성에 클래스명 지정
@Controller
@RequestMapping("/accounts")
@SessionAttributes(types = "AccountCreateForm.class")
public class AccountCreateController {
...
}
@ModelAttribute 애너테이션이 붙은 메서드나 Model의 addAttribute 메서드를 통해 Model에 추가한 객체 중에 types 속성에서 지정한 클래스의 객체가 있다면 그 객체를 HTTP 세션에 저장한다.
names 속성에 객체명 지정.
이 방법은 같은 클래스로 만들어지는 객체 중 세션에서 관리할 것과 관리하지 않을 것이 섞인 경우에 사용.
@Controller
@RequestMapping("/accounts")
@SessionAttributes(names = "password")
public class AccountCreateController {
...
}
@Controller
@RequestMapping("/accounts")
@SessionAttributes(types = "AccountCreateForm.class")
public class AccountCreateController {
@ModelAttribute("accountCreateForm")
public AccountCreateForm setUpAccountCreateForm() {
return new AccountCreateForm();
}
}
@ModelAttribute 애너테이션이 붙은 메서드에서 반환한 객체가 Model에 저장된다. 이 객체의 타입은 @SessionAttribute 에서 지정한 types에 해당하므로 이 객체는 세션에도 저장된다.
세션 스코프 빈은 여러 컨트롤러에 거쳐 화면을 이동해야 할 때 컨트롤러 간에 데이터를 공유하는 매개체 역할을 한다.
세션에서 관리할 객체를 DI 컨테이너에 세션 스코프 빈으로 등록. 스코프트 프락시(scoped proxy)가 활성화되게. 스코프트 프락시는 싱글턴 같이 수명이 긴 빈에 요청 스코프 같은 수명이 짧은 빈을 인젝션하기 위한 매커니즘을 제공한다.
@Component
@SessionScope
public class Cart implements Serializable {
// ...
}
@Bean
@SessionScope
public Cart cart() {
return new Cart();
}
세션 스코프 빈을 사용할 때는 @Autowired 를 통해 인젝션 하여 사용 가능하다.
부하가 커서 시간이 많이 걸리는 처리를 애플리케이션 서버가 관리하는 스레드에서 분리된 스레드에서 실행하게 만들어서 서버를 더 효율적으로 만들 수 있음.
실제 HTTP 응답은 비동기 처리가 완료된 후에 나오기 때문에 클라이언트 측에서는 동기처리를 한 것처럼 보일 수 있다.
스프링 MVC에서 두 가지 방법 가능
비동기 처리를 시작한 시점에 일단 HTTP 응답을 하고 그 후 비동기 처리 중의 임의의 타이밍에 응답 데이터를 전송.
클라이언트가 분할 응답('Transfer-Encoding: chunked')를 지원해야 한다.
web.xml <filter>
요소에 <async-supported>
를 true로 주어 서블릿 필터의 비동기 기능을 활성화한다.
<filter-mapping>
요소에 <dispatcher>
요소에 ASYNC를 주어 서블릿 필터의 처리 대상에 비동기 요청을 포함시킬 수 있다.
<servlet>
요소에 <async-supported>
요소에 true를 주어 DispatcherServlet의 비동기 기능을 활성화할 수 있다.
@Configuration
@EnableMvc
@ComponentScan("com.example.app")
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
configurer.setDefaultTimeout(5000); // timeout 시간 설정
}
}
스프링 MVC가 아닌 다른 스레드에서도 사용 가능한 방식. 스프링 프레임워크는 특정 메서드를 다른 스레드에서 실행하게 만들 수 있다. <- 다른 스레드로 실행하려는 메서드에 @Async를 붙여주기만 하면 된다.
@Configuration
@EnableAsync // @Async를 통한 비동기 기능 활성화
public class AsyncConfig {
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 스레드 풀을 사용하도록 커스터마이징한 TaskExecutor를 빈으로 정의
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
return executor;
}
}
@Async가 기본적으로 사용하는 TaskExecutor는 요청마다 새로운 스레드를 생성하는 구현 클래스(SimpleAsyncTaskExecutor)이다.
TaskExecutor를 커스터마이징할 때는 'taskExecutor'라는 이름의 빈을 정의한다.
@Autowired
AsyncUploader asyncUploader;
@RequestMapping(path = "upload", method = RequestMethod.POST)
public CompletableFuture<String> upload(MultipartFile file) {
return asyncUploader.upload(file); // 비동기 처리 호출
} // 핸들러 반환값으로 CompletableFuture<String> 반환. 이동할 뷰의 이름을 반환하고 있기 때문에 String 타입을 사용.
@Component
public class AsyncUploader {
@Autowired
UploadService uploadService;
@Async // 다른 스레드에서 처리
public CompletableFuture<String> upload(MultipartFile file) {
uploadService.upload(file);
return CompletableFuture.completedFuture("upload/complete");
}
}
위 예제에서는 파일 업로드를 비동기로 처리하고 비동기 처리가 종료되면 'upload/complete'라는 이름의 뷰로 이동.
@Autowired
GreetingMessageSender greetingMessageSender;
@RquestMapping(path="greeting", method=RequestMethod.GET)
public SseEmitter greeting() throws IOException, InterruptedException {
SseEmitter emitter = new SseEmitter();
greetingMessageSender.send(emitter);
return emitter;
}
@Component
public class GreetingMessageSender {
@Async
public void send(SseEmitter emitter) throws IOException, InterruptedException {
emitter.send(emitter.event()
.id(UUID.randomUUID.toString()).data("Good Morning!"));
TimeUnit.SECONDS.sleep(1);
emitter.send(emitter.event()
.id(UUID.randomUUID.toString()).data("Good Night!"));
emitter.complete();
}
}
스프링 MVC(DispatcherServlet)의 호출 전후에 공통된 처리. 자바 서블릿에서 지원하는 기능.
Filter 인터페이스의 구현 클래스를 만들거나 스프링에서 제공하는 지원 클래스를 이용하면 된다.
서블릿 필터 지원 클래스
public class ExampleFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// ...
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// preHandle
// filter-chain 실행
filterChain.doFilter(servletRequest, servletResponse);
// postHandle
}
@Override
public void destroy() {
// ...
}
}
web.xml
<filter>
<filter-name>ExampleFilter</filter-name>
<filter-class>com.example.filter.ExampleFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ExampleFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Java Config
@Configuration
public class WebFilterConfig {
@Bean
public FilterRegistrationBean exampleFilterRegistration() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(new ExampleFilter());
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.setName("ExampleFilter");
filterRegistrationBean.setOrder(1);
return filterRegistrationBean;
}
}
CharacterEncodingFilter는 반드시 사용해야 한다.
컨트롤러에서 처리되는 내용을 공통처리.
HandlerInterceptor 인터페이스를 구현하는 클래스를 만들면 됨.
HandlerInterceptor는 요청 경로에 대해 이를 받아줄 핸들러가 결정된 후에야 호출되기 때문에 어플리케이션에서 허용하는 요청에 대해서만 공통 처리를 할 수 있다.
HandlerInterceptor 세 가지 메서드
HandlerInterceptor 구현 예
public class SuccessLoggingInterceptor extends HandlerInterceptorAdapter {
private static final Logger LOGGER = LoggerFactory.getLogger(SuccessLoggingInterceptor.class);
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object hander, ModelAndView modelAndView) {
if (logger.isInfoEnabled()) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = ((HandlerMethod) handler).getMethod();
LOGGER.info("[SUCCESS CONTROLLER] {}.{}",
method.getDeclaringClass().getSimpleName(), method.getName();
}
}
}
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SuccessLoggingInterceptor())
.includePathPatterns("/**")
.excludePathPatterns("/resources/**");
}
}
컨트롤러 클래스에는 핸들러 메서드(@RequestMapping 부여한)와 별도로 컨트롤러 전용의 특수한 메서드(@InitBinder 메서드, @ModelAttribute 메서드, @ExceptionHandler 메서드)를 구현할 수 있다. 이런 메서드들을 여러 클래스에서 공유하려면 @ControllerAdvice를 붙인 클래스를 만들면 된다.
@ControllerAdvice // 모든 컨트롤러에 적용
public class GlobalExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String handleSystemException(Exception exception) {
LOGGER.error("System Error occurred.", exception);
return "error/system";
}
}
@ControllerAdvice에서 구현한 처리 내용의 적용 범위 애너테이션 속성으로 지졍 가능
컨트롤러의 핸들러 메서드 매개변수에 스프링 MVC가 지원하지 않는 독자적인 타입을 사용하려면 HandlerMethodArgumentResolver 인터페이스를 구현하면 된다.
공통 항목을 가진 자바빈즈
public class CommonRequestData {
private String userAgent;
private String sessionId;
...
}
public class CommonRequestDataMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
// 처리 가능한 인수 타입인지 판단한다. 이 메서드에서 true를 반환하면 resolveArgument 메서드가 호출된다.
return CommonRequestData.class.isAssignableFrom(parameter.getParameterType());
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpSession session = webRequest.getNativeRequest(HttpServletRequest.class).getSession(false);
String userAgent = webRequest.getheader(HttpHeaders.USER_AGENT);
String sessionId = Optional.ofNullable(session).map(HttpSession::getId).orElse(null);
CommonRequestData commonRequestData = new CommonRequestData();
commonRequestData.setUserAgent(userAgent);
commonRequestData.setSessionId(sessionId);
return commonRequestData;
}
}
HandlerMethodArgumentResovler 구현 클래스를 스프링 MVC에 적용.
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addArgumentResolvers(List<HanderMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new CommonRequestDataMethodArgumentResover());
}
}
웹 애플리케이션의 문서 루트는 메이븐이나 그레이들 프로젝트를 사용하는 경우 src/main/webapp 이다.
서블릿 사양에서는 루트 경로(/)에 매핑되는 서블릿을 '기본 서블릿'이라한다. 기본 서블릿을 통해 웹 애플리케이션의 분서 루트 이하 파일에 접근 가능.
스프링 MVC 애플리케이션에서 DispatcherServlet의 루트 경로에 매핑하는 스타일을 자주 볼 수 있는데, 그러면 웹 애플리케이션의 문서 루트 이하의 파일에는 더 이상 접근할 수 없게 된다. 이를 피하려면 DispatcherServlet이 받은 요청을 기본 서블릿에 전송하는 기능을 활성화하면 된다.
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
이 설정을 활성화했을 경우
/static/css/app.css 라는 요청이 들어왔을 때 DispatcherServlet에 요청 경로에 대응하는 핸들러 메서드가 존재하지 않으면 DefaultServletHttpRequestHandler를 통해 기본 서블릿에 요청을 전송한다.
ResourceHttpRequestHandler 클래스 사용. 이를 통해 정적 리소스를 저장해둔 임의의 디렉터리에 대해 파일 접근이나 HTTP 캐시를 손쉽게 할 수 있다.
ResourceHttpRequestHandler는 요청한 경로와 리소스의 물리적인 저장 경로를 매핑하는 역할을 함.
리소스의 저장 경로에는 클래스패스의 디렉터리, 웹 애플리케이션의 문서 루트 디렉터리, 임의의 디렉터리도 지정할 수 있다.
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
// ...
@Override
public void addResourceHandlers(ResourceHanderRegistry registry) {
// 요청 경로와 리소스의 물리적인 저장 경로를 매핑
registry
.addResourceHander("/static/**")
.addResourceLocations("classpath:/static/");
}
}
RequestHttpRequestHandler 에는 HTTP 캐시를 제어하는 기능이 있다. HTTP 요청의 If-Modified-Since 헤더 값과 리소스의 최종 수정 일시를 비교한 후 만약 리소스가 갱신되지 않았다면 304 HTTP Status code를 반환한다. 기본 구현에서는 캐시의 유효기간이 설정되지 않으므로 따로 설정하지 않으면 브라우저의 사양에 의존한다.
@Override
public void addResourceHandlers(ResourceHanderRegistry registry) {
registry
.addResourceHander("/static/**")
.addResourceLocations("classpath:/static/")
.setCachePeriod(604800); // 유효 기간을 초 단위로 지정 (7일)
}
유효기간을 지정하면 Cache-Control 헤더의 max-age 속성에 지정한 값이 출력. 0을 설정하면 Cache-Control 헤더에 no-store 속성이 출력