HTTP 세션 이용

여러 요청이 같은 데이터를 공유해야하는 상황(온라인 쇼핑몰 장바구니 등)에 세션 활용 가능.

스프링 MVC에서 HTTP 세션에 데이터 관리하는 방법 세 가지

  • 세션 속성(@SessionAttributes)
  • 세션 스코프 빈 이용
  • HttpSession API 이용

세션 속성(@SessionAttributes)

하나의 컨트롤러에서 여러 요청 간에 데이터를 공유하는 경우에 효과적

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 응답을 하는 패턴

부하가 커서 시간이 많이 걸리는 처리를 애플리케이션 서버가 관리하는 스레드에서 분리된 스레드에서 실행하게 만들어서 서버를 더 효율적으로 만들 수 있음.
실제 HTTP 응답은 비동기 처리가 완료된 후에 나오기 때문에 클라이언트 측에서는 동기처리를 한 것처럼 보일 수 있다.

스프링 MVC에서 두 가지 방법 가능

  • 스프링 MVC의 스레드에서 비동기 처리
    컨트롤러의 핸들러 메서드에서 Callable, WebAsyncTask 반환
  • 스프링 MVC 외의 스레드에서 비동기 처리
    컨트롤러의 핸들러 메서드에서 DeferredResult, ListenableFuture, CompletableFuture 반환

비동기 실행이 처리되는 중에 HTTP 응답을 하는 패턴

비동기 처리를 시작한 시점에 일단 HTTP 응답을 하고 그 후 비동기 처리 중의 임의의 타이밍에 응답 데이터를 전송.
클라이언트가 분할 응답('Transfer-Encoding: chunked')를 지원해야 한다.

  • 롤 폴링을 이용한 비동기 처리
    핸들러 메서드에서 ResponseBodyEmitter 반환
  • SSE(Server-Sent Events)에 따른 비동기 처리
    SseEmitter 타입 반환. 클라이언트가 'Content-Type: text/event-stream' 같은 SSE를 지원해야함.

비동기 기능 활성화 설정

web.xml <filter>요소에 <async-supported> 를 true로 주어 서블릿 필터의 비동기 기능을 활성화한다.
<filter-mapping> 요소에 <dispatcher> 요소에 ASYNC를 주어 서블릿 필터의 처리 대상에 비동기 요청을 포함시킬 수 있다.
<servlet> 요소에 <async-supported> 요소에 true를 주어 DispatcherServlet의 비동기 기능을 활성화할 수 있다.

스프링 MVC에서 비동기 기능 활성화

@Configuration
@EnableMvc
@ComponentScan("com.example.app")
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        configurer.setDefaultTimeout(5000); // timeout 시간 설정
    }
}

비동기 처리 구현

@Async 이용

스프링 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'라는 이름의 빈을 정의한다.

CompletableFuture 사용 (비동기 실행 종료 후 응답)

@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'라는 이름의 뷰로 이동.

SseEmitter를 이용한 Push 형태의 비동기 처리 구현

@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 인터페이스의 구현 클래스를 만들거나 스프링에서 제공하는 지원 클래스를 이용하면 된다.

서블릿 필터 지원 클래스

  • GenericFilterBean: 서블릿 필터의 초기화 파라미터를 서블릿 필터 클래스의 프로퍼티에 바인드하는 기반 클래스
  • OncePerRequestFilter: 같은 요청에 대해서 단 한번만 수행되는 것을 보장하는 기반 클래스. GenericFilterBean 을 상속함, 스프링이 제공하는 서블릿 필터는 이 클래스의 자식 클래스로 만들어짐.
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는 반드시 사용해야 한다.

  • CharacterEncodingFilter: 요청과 응답의 문자 인코딩을 지정
  • CorsFilter
  • RequestContextFilter: HttpServletRequest, HttpServletResponse를 스레드 로컬에 설정하기 위함
  • ResourceUrlEncodingFilter: 정적 리소스 접근 URL을 ResourceResolver와 연계하여 만들어줌
    ...

HandlerInterceptor 이용

컨트롤러에서 처리되는 내용을 공통처리.
HandlerInterceptor 인터페이스를 구현하는 클래스를 만들면 됨.
HandlerInterceptor는 요청 경로에 대해 이를 받아줄 핸들러가 결정된 후에야 호출되기 때문에 어플리케이션에서 허용하는 요청에 대해서만 공통 처리를 할 수 있다.

HandlerInterceptor 세 가지 메서드

  • preHandle: 컨트롤러의 핸들러 메서드를 실행하기 전에 호출. false를 반환하여 핸들러 메서드를 호출하지 않게 할 수 있다.
  • postHandle: 컨트롤러 핸들러 메서드가 정상적으로 종료된 후 호출. 핸들러 메서드에서 예외가 발생하면 호출되지 않음.
  • afterCompletion: 핸들러 메서드가 종료된 후 호출. 핸들러 메서드에서 예외가 발생하더라도 호출.

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

@ControllerAdvice 이용

컨트롤러 클래스에는 핸들러 메서드(@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에서 구현한 처리 내용의 적용 범위 애너테이션 속성으로 지졍 가능

  • basePackages(value)
  • basePackageClasses
  • annotations
  • assignableTypes: 지정한 클래스나 인터페이스로 할당 가능(형변환 가능)한 컨트롤러에 대해 공통 처리가 적용된다.

HandlerMethodArgumentResolver 이용

컨트롤러의 핸들러 메서드 매개변수에 스프링 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 이다.

기본 서블릿과 DispatcherServlet의 공존

서블릿 사양에서는 루트 경로(/)에 매핑되는 서블릿을 '기본 서블릿'이라한다. 기본 서블릿을 통해 웹 애플리케이션의 분서 루트 이하 파일에 접근 가능.
스프링 MVC 애플리케이션에서 DispatcherServlet의 루트 경로에 매핑하는 스타일을 자주 볼 수 있는데, 그러면 웹 애플리케이션의 문서 루트 이하의 파일에는 더 이상 접근할 수 없게 된다. 이를 피하려면 DispatcherServlet이 받은 요청을 기본 서블릿에 전송하는 기능을 활성화하면 된다.

@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}

이 설정을 활성화했을 경우
/static/css/app.css 라는 요청이 들어왔을 때 DispatcherServlet에 요청 경로에 대응하는 핸들러 메서드가 존재하지 않으면 DefaultServletHttpRequestHandler를 통해 기본 서블릿에 요청을 전송한다.

스프링 MVC의 독자적인 정적 리소스 취급 방법

ResourceHttpRequestHandler 클래스 사용. 이를 통해 정적 리소스를 저장해둔 임의의 디렉터리에 대해 파일 접근이나 HTTP 캐시를 손쉽게 할 수 있다.

임의의 디렉터리에 저장된 파일에 접근

ResourceHttpRequestHandler는 요청한 경로와 리소스의 물리적인 저장 경로를 매핑하는 역할을 함.
리소스의 저장 경로에는 클래스패스의 디렉터리, 웹 애플리케이션의 문서 루트 디렉터리, 임의의 디렉터리도 지정할 수 있다.

@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    // ...
    @Override
    public void addResourceHandlers(ResourceHanderRegistry registry) {
        // 요청 경로와 리소스의 물리적인 저장 경로를 매핑
        registry
            .addResourceHander("/static/**")
            .addResourceLocations("classpath:/static/");
    }
}

HTTP 캐시 제어

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 속성이 출력

profile
가자~

0개의 댓글

관련 채용 정보

Powered by GraphCDN, the GraphQL CDN