Spring MVC/01.Spring @MVC

05. Handler Interceptor

  • -
반응형

Handler Interceptor

Handler Interceptor는 말 그대로 handler(Controller)로 가는 요청을 가로채는 녀석으로 Servlet의 Filter와 유사한 녀석이다.

주요 역할은 여러 컨트롤러에서 공통적으로 사용되는 기능을 정의하는데 예를 들어 여러 컨트롤러에서 사용되는 공통적인 model attribute를 설정하거나 request에 대한 검사, response header 설정 등 무궁무진하다.

HandlerInterceptor interface

Handler Interceptor를 만들기 위해서는 org.springframework.web.servlet.HandlerInterceptor를 구현한다. 이 인터페이스에는 3개의 주요 메서드가 있는데 preHandle, postHandle, afterCompletion이 그것이다. 메서드 이름에 나와있듯이 메서스들은 handler 동작 전, 후 그리고 view의 response까지 완전히 끝난 시점에 동작한다. 

 

고맙게도 HandlerInterceptor의 메서드들은 파라미터로 HttpServletRequest, HttpServletResponse, Handler, Exception을 파라미터로 받기 때문에 웹과 관련된 모든 일을 처리할 수 있다.

 

성능 측정을 위한 interceptor 만들기

다음은 Controller의 성능을 로깅하는 PerformanceInterceptor의 작성 예이다. 

각 메서드에 대한 설명은 주석으로 대체한다.

package com.eshome.mvc.interceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
public class PerformanceInterceptor implements HandlerInterceptor {

    @Override
    // return true인 경우 실제 handler 호출, false인 경우는 중지
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        long startTime = System.currentTimeMillis();
        request.setAttribute("startTime", startTime);
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    @Override
    // handler의 동작이 종료된 후 호출
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
            throws Exception {
        long midTime = System.currentTimeMillis();
        // 정적 리소드들(css, js,...)은 modelAndView가 null
        if (modelAndView != null) {
            // modelAndView를 통해 view에 전달할 자료의 수정 가능
            // modelAndView.addObject("message", "Hello Interceptor");
        }
        request.setAttribute("midTime", midTime);
    }

    @Override
    // view에 대한 rendering 까지 종료된 후 호출
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        long endTime = System.currentTimeMillis();
        Object midTimeObj = request.getAttribute("midTime");
        Object startTimeObj = request.getAttribute("startTime");
        log.trace("servlet: {}", request.getServletPath());
        if (midTimeObj != null) {
            log.trace("Handler 동작시간: {}", endTime - (Long) midTimeObj);
        }
        log.trace("전체 동작시간: {}", endTime - (Long) startTimeObj);
    }
}

 

Handler Interceptor의 적용

handler interceptor를 적용하기 위해서는 WebMvcConfigurer의 addInterceptors 메서드를 재정의해서 InterceptorRegistry에 인터셉터와 인터셉터가 동작하기 위한 경로를 설정한다.

@Autowired
PerformanceInterceptor pi;
    
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(pi)
            .addPathPatterns("/**")
            .excludePathPatterns("/**/*.css", "/**/*.js");
}

 

addInterceptor를 이용해서 인터셉터를 추가한 후 별도의 경로 설정이 없다면 모든 요청에 대해서 적용하겠다는 의도이다. 적용할 경로를 추가할 때는 addPathPatterns, 제외할 경로를 추가할 때는 excludePathPatterns를 적용하는데 경로는 Ant 표현식을 따른다.

Ant 표현식으로 경로를 작성할 때는 3개의 특수문자를 사용한다.

  • ?: 1개의 글자
    • /test/member??.info: /test 아래 member로 시작하고 두 개의 임의의 문자가 나온 후. info로 끝나는 경로
  • *: 0개 이상의 글자
    • /test/?*.info: /test 아래 최소 한 글자가 있고 .info로 끝나는 모든 경로
  • **: 0개 이상의 디렉터리
    • /test/**/info:  /test의 하위 디렉터리에 depth에 상관없이 /info로 끝나는 모든 경로로 /test/math/info, /test/math/abs/info … 등

따라서 위의 예에서 pi는 .css, .js가 아닌 모든 요청을 대상으로 동작한다.

 

HandlerInterceptor를 이용한 session 관리

특정 웹 페이지에 접근할 때 이미 로그인한 사용자인지 확인하고 로그인하지 않은 경우는 로그인을 유도해야 하는 경우가 많다. 이때 페이지마다 로그인 여부를 체크하는 것은 매우 번거로운 일이다. 이때 HandlerInterceptor를 써먹어보자.

 

  1. 클라이언트가 session에 로그인 정보(loginUser) 없이 /secret에 접근한다. 이때 Session Handler Interceptor가 요청을 가로채서 session에서 loginUser 정보를 확인한다.
  2. Handler Interceptor가 session에서 정보를 확인하지 못한 경우 /login으로 redirect 시킴으로 로그인을 유도한다.
  3. 브라우저가 get 방식으로/login을 요청하고  login.html이 서비스된다.
  4. login.html에서 로그인 정보를 입력하고 post로 /login을 호출하면 service를 이용해 로그인 가능 여부를 파악한다.
  5. login이 성공했다면 session에 loginUser 속성을 설정 후 redirect:/secret를 반환해서 다시 /secret를 호출하도록 한다.
  6. 이제 session에 loginUser 정보가 있으므로 /secret에 접근이 허용된다.

 

로그인이 반드시 필요한 페이지

다음 페이지는 반드시 로그인해야 접근할 수 있는 페이지인 secret.html이다. 이 페이지가 열리면 세션에 loginUser 정보가 있을 것이고 그 정보를 화면에 출력하자.

<!--secret.html-->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<h1>여기는 중요한 페이지입니다.</h1>
	<!-- session 속성 중에서 loginUser를 출력한다. -->
	<h2 th:text="${session.loginUser}">로그인 사용자 정보</h2>
</body>
</html>

 

HandlerInterceptor에서 세션 확인 및 처리

이제 세션을 모니터링하는 HandlerInterceptor를 만들자. 만약 세션에 loginUser라는 속성이 있다면 그대로 진행하고 없다면 login 페이지로 유도한다.

package com.eshome.mvc.interceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@Component
public class SessionInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession();
        // 세션에 loginUser 정보가 있다면 그대로 진행한다.
        if (session.getAttribute("loginUser") != null) {
            return true;
            // 세션에 정보가 없다면 login을 유도한다.
        } else {
            // login 성공 후 다시 원래의 목적지로 갈 수 있게 prev 정보를 저장한다.
            response.sendRedirect(request.getContextPath() + "login?prev=" + request.getServletPath());
            return false;
        }
    }
}

 

SessionInterceptor 설정

MVCConfig에 SessionInterceptor를 설정해 주자. /secret 요청에 대해서 SessionInterceptor를 적용한다.

    @Autowired
    SessionInterceptor si;

    @Autowired
    PerformanceInterceptor pi;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 성능 측정을 위한 HandlerInterceptor 설정
        registry.addInterceptor(pi)
                .addPathPatterns("/**")
                .excludePathPatterns("/**/*.css", "/**/*.js");
        // 세션에서 loginUser를 체크하기 위한 HandlerInterceptor 설정
        registry.addInterceptor(si).addPathPatterns("/secret");
    }

 

login.html

다음으로 login 정보를 입력하기 위한 login.html을 작성해 보자. 로그인 성공 후 특정 페이지로 이동을 희망하기 때문에 해당 정보를 hidden 타입의 input에 저장해 두었다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<form th:action="@{/login}" method="post">
		<fieldset>
			<legend>로그인</legend>
			<input type="hidden" name="prev" th:value="${param.prev}">
			<input type="text" name="id" placeholder="아이디입력">
			<input type="text" name="password" placeholder="비번입력">
			<input type="submit">
		</fieldset>
	</form>
</body>
</html>

 

login 처리를 위한 서비스

HelloService와 HelloServiceImpl에 로그인할 수 있는 login 메서드를 구현해 주자. 단지 문자열 id가 hong과 같고 password가 1234인지 만 판단하도록 간단히 구성한다.

@Service
public class HelloServiceImpl implements HelloService {
    . . 
    @Override
    public boolean login(UserInfo user) {
        return "hong".equals(user.getId()) && "1234".equals(user.getPassword());
    }
}

 

login form 보여주기

/login을 get 방식으로 요청 시 login form을 보여주자.

    @GetMapping("/login")
    public String loginForm(Model model) {
        // 사용자에게 login form 제공
        return "login";
    }

 

간단히 form을 보여주기 위해서 ViewController의 사용을 고려할 수도 있다. 주의할 점은 사용하는 메서드와 상관없이 @RequestMapping에서 사용하는 URL과 같은 URL을 사용하면 ViewController는 매핑되지 못한다는 점이다. 

 

login 처리

마지막으로 post로 /login을 요청했을 때 Service를 연동해서 로그인 처리하고 적절한 페이지로 이동시키도록 handler method를 작성해 보자.

    @PostMapping("/login")
    public String doLogin(Model model, @ModelAttribute UserInfo user, @RequestParam String prev, HttpSession session) {
        StringBuilder to = new StringBuilder("redirect:");
        // 로그인 성공시
        if (service.login(user)) {
            // 사용자 정보를 세션에 담고
            session.setAttribute("loginUser", "홍길동");

            if (prev != null) {
                to.append(prev);
            } else {
                to.append("/");
            }
        }
        // 로그인 실패 시 다시 로그인으로
        else {
            to.append("/login");
        }
        return to.toString();
    }

 

Filter vs Interceptor vs AOP

[spring]filter vs interceptor vs AOP (tistory.com)

 

[spring]filter vs interceptor vs AOP

스프링을 사용하다 보면 자동으로 무언가를 해주는 3녀석 있다. filter, interceptor, AOP가 주인공인데 잘만 쓰면 우리의 코드를 확 줄여줘서 퇴근을 빠르게 해줄 수 있는 녀석들이다. 이번 포스트에

goodteacher.tistory.com

 

반응형

'Spring MVC > 01.Spring @MVC' 카테고리의 다른 글

07. 파라미터와 validation  (0) 2020.07.08
06. 파라미터의 formatting  (0) 2020.07.07
04. 웹과 관련된 설정  (0) 2020.07.02
03. Controller에 대한 단위 테스트  (2) 2020.07.01
02. Controller  (0) 2020.06.30
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.