@PostMapping("/login")publicStringlogin(@Valid @ModelAttributeLoginForm form,BindingResult bindingResult,HttpServletRequest request) {//... 로그인 성공// 세션이 있으면 기존 세션 반환, 없으면 신규 세션 생성HttpSession session =request.getSession();// 세션에 로그인 회원 정보 보관session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);return"redirect:/";}
세션 조회
스프링은 세션을 더 편리하게 사용할 수 있도록 @SessionAttribute 지원
세션과 세션 데이터를 찾는 번거로운 과정을 스프링이 한번에 처리
@GetMapping("/")public String homeLoginV3Spring(@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember, Model model) {
// 세션에 회원 데이터가 있을 경우if (loginMember ==null) {return"home"; }// 세션이 있을 경우model.addAttribute("member", loginMember);return"loginHome";}
HttpSession session =request.getSession(false);session.getAttributeNames().asIterator().forEachRemaining(name ->log.info("session name={}, value={}", name,session.getAttribute(name)));// Session ID (JSESSIONID 값)log.info("sessionId={}",session.getId()); // 세션 유효 시간 (sec.)log.info("maxInactiveInterval={}",session.getMaxInactiveInterval()); // 세션 생성 일시 (Long)log.info("creationTime={}",newDate(session.getCreationTime())); // 세션과 연결된 사용자가 최근에 서버에 접근한 시간 (Long) -> 클라이언트에서 서버로 sessionId(JSESSIONID)를 요청한 경우 갱신log.info("lastAccessedTime={}",newDate(session.getLastAccessedTime())); // 새로 생성된 세션인지 확인log.info("isNew={}",session.isNew());
세션 타임아웃
사용자가 로그아웃을 직접 호출하지 않고 웹 브라우저를 종료할 경우 세션이 무한정 남아있는 문제 발생
세션과 관련된 쿠키(JSESSIONID)를 탈취 당했을 경우 오랜 시간이 지나도 해당 쿠키로 악의적인 요청을 할 수 있음.
세션은 기본적으로 메모리에 생성
메모리 크기가 무한하지 않으므로 세션에는 최소한의 데이터만 보관하는 것이 중요
메모리 사용량(보관한 데이터 용량 * 사용자 수)이 급격하게 늘어나 장애 발생 가능성 존재
세션 시간을 너무 길게 가져가면 메모리 사용이 계속 누적 될 수 있으므로 적당한 시간을 선택하는 것이 필요
세션 종료 시점
사용자가 서버에 최근에 요청한 시간을 기준으로 30분 정도를 유지
사용자가 서비스를 사용하고 있으면, 세션의 생존 시간이 30분으로 계속 증가
HttpSession 은 이 방식을 사용
세션 타임아웃 설정
글로벌 설정
server.servlet.session.timeout=1800 # sec
특정 세션 단위 설정
session.setMaxInactiveInterval(1800); // sec.
세션 타임아웃 발생
세션의 타임아웃 시간은 해당 세션과 관련된 JSESSIONID 를 전달하는 HTTP 요청이 있으면 현재 시간을 기준으로 다시 초기화
이렇게 초기화되면 세션 타임아웃으로 설정한 시간동안 세션을 추가로 사용 가능
session.getLastAccessedTime(): 최근 세션 접근 시간
LastAccessedTime 이후로 timeout 시간이 지나면, WAS 가 내부에서 해당 세션 제거
Filter, Interceptor
공통 관심사(cross-cutting concern): 애플리케이션의 여러 로직에서 공통으로 관심을 갖는 것
ex. 여러 컨트롤러에서 로그인 여부 확인
웹과 관련된 공통 관심사는 서블릿 필터 또는 스프링 인터셉터 사용 권장
HttpServletRequest 제공 (HTTP header, URL 정보 등..)
Servlet Filter
필터는 서블릿이 지원하는 수문장.
.
필터 흐름
HTTP Request ➔ WAS ➔ filter ➔ (dispatcher)Servlet ➔ Controller
@Slf4jpublicclassLogFilterimplementsFilter { @Overridepublicvoidinit(FilterConfig filterConfig) throwsServletException {log.info("log filter init"); } /** * HTTP 요청이 오면 호출 * - 고객의 요청 응답 정보를 한 번에 확인 가능 * - 시간 정보를 추가해서 요청 시간 확인 및 성능 최적화 가능 */ @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;String requestURI =httpRequest.getRequestURI(); // 요청 URI 정보String uuid =UUID.randomUUID().toString(); // HTTP 요청 구분 목적try {log.info("REQUEST [{}][{}]", uuid, requestURI);/** * 다음 필터가 있으면 필터 호출. 필터가 없으면 서블릿 호출 * (doFilter 를 호출하지 않으면 다음 단계로 진행되지 않음) */chain.doFilter(request, response); } catch (Exception e) {throw e; } finally { log.info("RESPONSE [{}][{}]", uuid, requestURI); } } @Overridepublicvoiddestroy() {log.info("log filter destroy"); }}
필터 설정
@ConfigurationpublicclassFilterWebConfig { /** * FilterRegistrationBean 를 사용하여 필터 등록 * * @ServletComponentScan, @WebFilter(filterName = "logFilter", urlPatterns = "/*") 로 필터 등록이 가능하지만 필터 순서 조절 불가 * Spring Boot 는 WAS 를 들고 함께 띄우기 때문에, WAS 를 띄울 때 필터를 같이 세팅 */ @BeanpublicFilterRegistrationBeanlogFilter() {FilterRegistrationBean<Filter> filterRegistrationBean =newFilterRegistrationBean<>();filterRegistrationBean.setFilter(newLogFilter()); // 등록할 필터 지정filterRegistrationBean.setOrder(1); // 필터는 체인으로 동작하므로 순서 지정filterRegistrationBean.addUrlPatterns("/*"); // 필터를 적용할 URL 패턴 지정return filterRegistrationBean; }}
preHandle(): Controller 호출 전 호출(Handler Adapter 호출 전)
return true ➔ 다음으로 진행
return false ➔ 진행 중단(나머지 인터셉터, 핸들러 어댑터 호출 X)
postHandle(): Controller 호출 후 호출(Handler Adapter 호출 후)
Controller 에서 예외 발생 시 postHandle 호출 X
afterCompletion(): HTTP 요청 종료 후 호출(View rendering 후)
예외 여부에 관계없이 항상 호출
예외 발생 시 예외 정보를 파라미터로 받아서 로그 출력 가능
요청 로그
요청 로그 인터셉터 구현
@Slf4jpublicclassLogInterceptorimplementsHandlerInterceptor {publicstaticfinalString LOG_ID ="logId"; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI =request.getRequestURI();String uuid =UUID.randomUUID().toString(); /** * 스프링 인터셉터는 호출 시점이 완전히 분리 * preHandle 에서 지정한 값을 postHandle, afterCompletion 에서 함께 사용하기 위해 request 에 세팅 * (HandlerInterceptor 구현체는 싱글톤처럼 사용되기 때문에 맴버변수를 사용하면 위험!) */ request.setAttribute(LOG_ID, uuid); /** * @Controller, @RequestMapping: HandlerMethod * 정적 리소스(/resources/static): ResourceHttpRequestHandler */if (handler instanceof HandlerMethod) {HandlerMethod hm = (HandlerMethod) handler; // 호출할 컨트롤러 메서드의 모든 정보가 포함 } log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);returntrue; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle [{}]", modelAndView); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
String requestURI =request.getRequestURI();String logId = (String) request.getAttribute(LOG_ID);log.info("RESPONSE [{}][{}]", logId, requestURI);if (ex !=null) {log.error("afterCompletion error!!", ex); } }}
HandlerMethod:
핸들러 정보는 어떤 핸들러 매핑을 사용하는가에 따라 다름
스프링을 사용하면 일반적으로 @Controller, @RequestMapping 을 활용한 핸들러 매핑을 사용
이 경우 핸들러 정보로 HandlerMethod 가 넘어온다.
ResourceHttpRequestHandler
@Controller 가 아니라 정적 리소스(/resources/static)가 호출 되는 경우
ResourceHttpRequestHandler 가 핸들러 정보로 넘어오기 때문에 타입에 따라서 처리가 필요
postHandle, afterCompletion
예외가 발생한 경우 postHandle 가 호출되지 않으므로, 종료 로그를 afterCompletion 에서 실행
.
인터셉터 등록
@ConfigurationpublicclassInterceptorWebConfigimplementsWebMvcConfigurer { /** * WebMvcConfigurer 가 제공하는 addInterceptors() 를 사용해서 인터셉터 등록 * 인터셉터는 addPathPatterns, excludePathPatterns 로 매우 정밀하게 URL 패턴 지정 */ @OverridepublicvoidaddInterceptors(InterceptorRegistry registry) {registry.addInterceptor(newLogInterceptor()) // 인터셉터 등록.order(1) // 인터셉터 호출 순서 지정.addPathPatterns("/**") // 인터셉터 적용 URL 패턴 지정.excludePathPatterns("/css/**","/*.ico","/error"); // 인터셉터 제외 패턴 지정 }}
@Slf4jpublicclassLoginMemberArgumentResolverimplementsHandlerMethodArgumentResolver { @OverridepublicbooleansupportsParameter(MethodParameter parameter) {boolean hasLoginAnnotation =parameter.hasParameterAnnotation(Login.class);boolean hasMemberType =Member.class.isAssignableFrom(parameter.getParameterType());/** * @Login 어노테이션이 있으면서 Member 타입이면 해당 ArgumentResolver 사용 * 결과가 true 일 경우 resolveArgument() 실행 */return hasLoginAnnotation && hasMemberType; } /** * 컨트롤러 호출 직전에 호출 되어서 필요한 파라미터 정보를 생성 * - 세션에 있는 로그인 회원 정보인 member 객체를 찾아서 반환 * - 이후 Spring MVC 는 컨트롤러의 메서드를 호출하면서 여기에서 반환된 member 객체를 파라미터에 전달 */ @OverridepublicObjectresolveArgument(MethodParameter parameter,ModelAndViewContainer mavContainer,NativeWebRequest webRequest,WebDataBinderFactory binderFactory) throwsException {HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();HttpSession session =request.getSession(false);if (session ==null) {returnnull; }returnsession.getAttribute(SessionConst.LOGIN_MEMBER); }}