Spring MVC Interceptor

 

본 포스팅은 코드로 배우는스프링 웹프로젝트를 참조하여 작성한 내용입니다. 개인적으로 학습한 내용을 복습하기 위해 기록한 내용이기 때문에 오류가 있다면 지적 부탁드리겠습니다. 포스팅의 예제는 STS 또는 Eclipse를 사용하지 않고 IntelliJ를 통해 구현하고 있습니다. 그래서 기존의 STS에서 생성된 Spring 프로젝트의 스프링관련 설정 파일명과 프로젝트 구조가 약간 다를 수 있습니다. IntelliJ를 통한 Spring MVC 프로젝트 생성 포스팅을 참고해주시면 감사하겠습니다. 현재 프로젝트의 전체코드는 Github 저장소에서 확인하실 수 있습니다.


Spring-MVC 기본 개념 및 테스트 예제 관련 포스팅 링크

  1. IntelliJ에서 Spring MVC Project 생성하기
  2. Spring MVC - MySQL 연결테스트
  3. Spring MVC - MyBatis 설정 및 테스트
  4. Spring MVC 구조
  5. Spring MVC - Controller 작성 연습, WAS없이 Controller 테스트 해보기
  6. SpringMVC + MyBatis, DAO구현 테스트
  7. Spring - RestController, ResponseEntity, AJAX

Spring-MVC 게시판 예제 관련 포스팅 링크

  1. IntelliJ에서 Spring MVC Project 생성하기
  2. Bootstrap 템플릿 세팅 (AdminLTE)
  3. 기본적인 CRUD 구현 : 영속계층, 비지니스계층
  4. 기본적인 CRUD 구현 : Control, Presentation 계층
  5. 예외처리
  6. 페이징처리 : Persistence, Business 계층
  7. 페이징처리 : Control, Presentation 계층
  8. 페이징처리 개선, 목록페이지 정보 유지
  9. 프로젝트 구조 변경 및 수정사항
  10. 검색처리, 동적 SQL
  11. AJAX 댓글처리 : Persistence, Business, Control 계층
  12. AJAX 댓글처리 : Presentation 계층
  13. AOP를 이용한 LogAdvice 작성
  14. 댓글 갯수, 게시글 조회수 구현, 트랜잭션처리

1. Filter와 Interceptor의 공통점과 차이점

1.1 공통점

Servlet 기술의 Filter와 스프링 MVC의 HandlerInterceptor는 특정 URI에 접근할 때 제어하는 용도로 사용된다

1.2 차이점

실행 시점에 속하는 영역(Context)에 차이점이 있다.

spring-filter

  • Filter
    • 동일한 웹 어플리케이션의 영역 내에서 필요한 자원들을 활용
    • 웹 어플리케이션 내에서 동작하므로, 스프링 Context에 접근하기 어려움

spring-interceptor

  • Interceptor
    • 스프링에서 관리되기 때문에 스프링 내의 모든 객체에 접근 가능
    • 스프링 Context 내에서 존재하므로 Context 내의 모든 객체를 활용 가능

HandlerInterceptor의 경우 스프링 빈으로 등록된 컨트롤러나 서비스 객체를 주입받아 사용할 수 있기 때문에 기존의 구조를 그대로 활용하면서 추가적인 작업이 가능하다.

2. Spring AOP와 HandlerInterceptor의 차이

특정 객체 동작 전에 또는 후에 처리는 이미 AOP를 통해 확인해보았지만, 일반적인으로 컨트롤러를 이용할 때에는 AOP의 BeforeAdvice등을 활용하기보다는 HandlerInterceptor 인터페이스 또는 HandlerInterceptorAdaptor 클래스를 활용하는 경우가 더 많다.

  • AOP의 Adivice : JoninPointProceedingJoinPoint 등을 활용해서 호출 대상이 되는 메서드의 파라미터를 처리하는 방식
  • Interceptor의 HandlerInterceptor : Filter와 유사하게 HttpServletRequest, HttpServletResponse를 파라미터로 받는 구조

일반적인 경우라면 Controller에서 DTO나 VO 타입을 주로 파라미터로 활용하고, Servlet API에 해당하는 HttpServletRequestHttpServletResponse를 활용하는 경우는 그다지 많지 않다.

HandlerInterceptor는 기존의 컨트롤러에서는 순수하게 필요한 파라미터와 결과 데이터를 만들어내고, 인터셉터를 이용해 웹과 관련된 처리를 도와주는 역할을 한다. HandlerInterceptor의 메서드는 아래와 같이 사용된다.

  • preHandle(request, response, handler) : 지정된 컨트롤러의 동작 이전에 가로채는 역할로 사용
  • postHandle(request, response, handler, modelAndView) : 지정된 컨트롤러의 동작 이후에 처리, Spring MVC의 FrontController인 DispatcherServlet이 화면을 처리하기 전에 동작
  • afterCompletion(request, response, handler, exception) : DispatcherServletdml 화면처리가 완료된 상태에서 처리

HandlerInterceptorAdaptorHandlerInterceptor인터페이스를 구현한 추상 클래스로 설계되어 있는데 일반적으로 디자인패턴에서 Adaptor라는 용어가 붙게 되면 특정 인터페이스를 미리 구현해서 사용하기 쉽게 하는 용도인 경우가 많다. HandlerInterceptorAdaptorHandlerInterceptor를 쉽게 사용하기 위해서 인터페이스의 메서드를 미리 구현한 클래스이다.

3. Interceptor 예제

프로젝트에 Interceptor를 적용하기

3.1 Interceptor 클래스 작성

예제를 위해 아래와 같이 /tutorial/interceptor패키지에 SampleInterceptor 클래스를 생성하고, HandlerInterceptorAdapter를 상속한다.

public class SampleInterceptor extends HandlerInterceptorAdapter {

}

3.2 Interceptor 설정

위에서 작성한 SampleInterceptor를 스프링에서 인식하기 위해 dispatcher-servlet.xml(servlet-context.xml)에 아래와 같이 설정을 해준다. 그리고 인터셉터를 사용할 요청 uri를 작성해주면된다.

<bean id="sampleInterceptor" class="com.doubles.mvcboard.tutorial.interceptor.SampleInterceptor"/>

<mvc:interceptors>
    <mvc:interceptor>
        <mvc:mapping path="/interceptor/doA"/>
        <mvc:mapping path="/interceptor/doB"/>
        <ref bean="sampleInterceptor"/>
    </mvc:interceptor>      
</mvc:interceptors>

3.3 Interceptor를 위한 Controller, JSP 작성

간단한 테스트를 위해 /tutorial/controller패키지에 아래와 같이 Controller를 생성하고 코드를 작성해준다.

@Controller
@RequestMapping("/interceptor")
public class InterceptorController {

    private static final Logger logger = LoggerFactory.getLogger(InterceptorController.class);

    @RequestMapping(value = "/doA", method = RequestMethod.GET)
    public String doA() {
        logger.info("doA() called");

        return "/tutorial/interceptor_test";
    }

    @RequestMapping(value = "/doB", method = RequestMethod.GET)
    public String doB(Model model) {
        logger.info("doB() called");
        model.addAttribute("result", "doB result data...");

        return "/tutorial/interceptor_test";
    }

}

/views/tutorial디렉토리에 interceptor.jsp를 생성하고 결과 페이지를 아래와 같이 작성해준다.

<div class="col-lg-12">
    <div class="box box-primary">
        <div class="box-header with-border">
            <h3 class="box-title">인터셉터 결과 데이터</h3>
        </div>
        <div class="box-body">
            <a href="${path}/interceptor/doB">doB 페이지로 이동</a>
        </div>
        <div class="box-footer">
            <p>결과데이터 : ${result}</p>
        </div>
    </div>
</div>

3.4 SampleInterceptor 코드 작성

SampleInterceptor에서 preHandle()postHandle()을 이용해서 콘솔 출력 코드를 아래와 같이 작성해준다.

public class SampleInterceptor extends HandlerInterceptorAdapter {

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

        System.out.println("post handle....");

   }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        System.out.println("pre handle....");

        return true;
    }
}

3.5 실행 및 결과

/interceptor/doA 경로로 이동하면 아래와 같이 콘솔창에 로그가 찍히는 것을 확인할 수 있다.

pre handle....
INFO : com.doubles.mvcboard.tutorial.controller.InterceptorController - doA() called
INFO : com.doubles.mvcboard.commons.aop.LogAdvice - Controller : com.doubles.mvcboard.tutorial.controller.InterceptorController.doA()
INFO : com.doubles.mvcboard.commons.aop.LogAdvice - Argument/Parameter : []
INFO : com.doubles.mvcboard.commons.aop.LogAdvice - Return Value : /tutorial/interceptor_test
INFO : com.doubles.mvcboard.commons.aop.LogAdvice - Running Time : 5
INFO : com.doubles.mvcboard.commons.aop.LogAdvice - ----------------------------------------------------------------
post handle....

4. Interceptor의 request, response 활용

인터셉터를 좀더 적극적으로 활용하면 컨트롤러가 웹에서 필요한 자원을 처리할 때 겪는 불편을 해소하는데 도움이 된다.

4.1 preHandle()Object 파라미터

preHandle() 메서드의 경우 다음과 같은 3개의 파라미터를 사용한다.

  • HttpServletRequest request
  • HttpServletResponse response
  • Object handler

마지막 handler는 현재 실행하려는 메서드 자체를 의미하는데, 이를 활용해 현재 실행되는 컨트롤러를 파악하거나 추가적인 메서드를 실행하는 등의 작업이 가능하다.

아래의 코드는 이전에 작성한 예제의 SampleInterceptorpreHandle()을 이용하여 현재 실행되는 컨트롤러와 메서드 정보를 파악할 수 있게 작성했다.

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

    System.out.println("pre handle....");

    HandlerMethod method = (HandlerMethod) handler;
    Method methodObj = method.getMethod();

    System.out.println("Bean : " + method.getBean());
    System.out.println("Method : " + methodObj);

    return true;
}

파라미터로 넘어온 handlerHandlerMethod타입으로 형변환한 후 원래 메서드와 객체를 확인 할 수 있다. 아래는 실행 결과 콘솔창에 출력된 결과이다.

pre handle....
Bean : com.doubles.mvcboard.tutorial.controller.InterceptorController@6ef34a13  
Method : public java.lang.String com.doubles.mvcboard.tutorial.controller.InterceptorController.doA()  
INFO : com.doubles.mvcboard.tutorial.controller.InterceptorController - doA() called
INFO : com.doubles.mvcboard.commons.aop.LogAdvice - Controller : com.doubles.mvcboard.tutorial.controller.InterceptorController.doA()
INFO : com.doubles.mvcboard.commons.aop.LogAdvice - Argument/Parameter : []
INFO : com.doubles.mvcboard.commons.aop.LogAdvice - Return Value : /tutorial/interceptor_test
INFO : com.doubles.mvcboard.commons.aop.LogAdvice - Running Time : 1
INFO : com.doubles.mvcboard.commons.aop.LogAdvice - ----------------------------------------------------------------
post handle....
pre handle....
Bean : com.doubles.mvcboard.tutorial.controller.InterceptorController@6ef34a13  
Method : public java.lang.String com.doubles.mvcboard.tutorial.controller.InterceptorController.doB(org.springframework.ui.Model)
INFO : com.doubles.mvcboard.tutorial.controller.InterceptorController - doB() called
INFO : com.doubles.mvcboard.commons.aop.LogAdvice - Controller : com.doubles.mvcboard.tutorial.controller.InterceptorController.doB()
INFO : com.doubles.mvcboard.commons.aop.LogAdvice - Argument/Parameter : [{result=doB result data...}]
INFO : com.doubles.mvcboard.commons.aop.LogAdvice - Return Value : /tutorial/interceptor_test
INFO : com.doubles.mvcboard.commons.aop.LogAdvice - Running Time : 1
INFO : com.doubles.mvcboard.commons.aop.LogAdvice - ----------------------------------------------------------------
post handle....

4.2 postHandle()을 이용해 추가적인 작업 수행하기

postHandle() 메서드의 경우 컨트롤러 메서드의 실행이 끝나고, 아직 화면처리가 되지 않은 상태이므로 필요하다면 메서드 실행 이후에 추가적인 작업이 가능하다.

예를 들어 특정 메서드의 실행결과를 HttpSession 객체에 같이 담아야할 경우, 컨트롤러에서는 Model객체에 결과 데이터를 저장하고, 인터셉터의 postHandle()에서 이를 이용해 HttpSession에 결과를 담는다면 컨트롤러에서 HttpSession을 따로 처리할 필요가 없게 된다.

아래의 코드는 컨트롤러에서 result라는 변수가 저장되었다면 HttpSession 객체에 이를 보관하는 예이다.

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    System.out.println("post handle....");

    Object result = modelAndView.getModel().get("result");

    if (result != null) {
        request.getSession().setAttribute("result", result);
        response.sendRedirect("/interceptor/doA");
    }
}

컨트롤러의 코드에서 /doB를 호출하면, result라는 이름으로 하나의 문자열이 보관된다. 이후에 postHandle() 메서드에서 result라는 변수가 ModelAndView에 존재하면 이를 추출해 HttpSession에 추가한다. 그리고 최종적으로 /doA페이지로 리다이렉트한다.

실제로 화면에서 /doB를 호출하면 HttpSession에 보관된 result변수에 저장한 객체를 가지고 /doA로 이동하게 된다.

interceptor_ex1

interceptor_ex2

첫번째 사진은 /doA에서 /doB를 요청하기 전이고, 두번째 사진은 /doB 요청을 처리하고 리다이렉트되어 /doA로 이동한 모습이다. 위에서 설명한 것처럼 리다이렉트 되기 전에 인터셉터에서 result변수에 담긴 문자열을 HttpSession에 보관하여 /doA로 이동하게 되고 화면에 결과 데이터가 보여지게 된다.

5. 정리

위 예제와 같은 내용을 로그인 처리에서 유용하게 사용할 수 있다. 컨트롤러에서 로그인 처리 후의 결과를 반환하고, 인터셉터를 이용해서 HttpSession에 로그인이 필요한 객체를 보관하는 형태로 작성하면 컨트롤러에서 직접 HttpSession을 사용하지 않는 코드를 만들 수 있다.

다음 포스팅에서는 게시판예제에 실제로 로그인기능을 추가하는 내용을 정리할 예정이다.