Spring Framework/Spring Boot

[Spring Boot] 6. Interceptor

hyomee2 2024. 9. 10. 17:00

Spring Interceptor란?

Spring Interceptor는 Spring 프레임워크에서 제공하는 기능 중 하나로, 클라이언트의 요청을 가로채서 처리하는 역할을 한다. 

이를 통해 공통적인 로직(로깅, 성능 측정, 캐싱)을 처리하거나,

보안(인증, 권한) 등의 목적으로 특정 조건을 검사하고 해당 요청을 처리하거나 무시할 수 있다.

 

Dispathcer Servlet이 핸들러 매핑을 통해 controller을 찾도록 요청하는데,

그 결과로 실행 체인(HandlerExecutionChain)을 돌려준다.

여기서 1개 이상의 interceptor가 등록되어 있다면 순차적으로 intercept들을 거쳐 controller가 실행되도록 하고,

interceptor가 없다면 바로 controller를 실행한다.

Interceptor가 직접 controller로 요청을 위임하는 것은 아니다.


Spring MVC request life cycle


Filter vs Interceptor

1. Filter

요청과 응답을 거른 뒤 정제하는 역할을 한다.

DispatcherServlet에 요청이 전달되기 전/후에 URL 패턴에 맞는 모든 요청에 대해 부가 작업을 처리할 수 있는 기능을 제공한다.즉, 스프링 컨테이너가 아닌 톰캣과 같은 웹 컨테이너에 의해 관리가 되고, 스프링 범위의 밖에서 처리가 된다.

2. Interceptor

특정 요청 URL에만 적용되도록 매핑할 수 있다는 점이 filter와 유사하지만,

filter와 달리 interceptor는 Spring web appplication context에 구성하기 때문에

컨테이너의 기능을 자유롭게 활용할 수 있으며 그 내부에 선언된 모든 빈을 참조할 수 있다.

(filter는 웹 컨테이너에서 동작하지만 interceptor는 스프링 컨텍스트에서 동작)

또한 filter와 달리 Dispatcher servlet이 controller를 호출하기 전/후에 끼어들어

요청과 응답을 참조하거나 가공할 수 있는 기능을 제공한다.


Interceptor의 메소드

Interceptor를 추가하려면 org.springframework.web.wervlet의 HandlerInterceptor 인터페이스를 구현해야 하며,

아래와 같은 메소드를 갖는다.

1. preHandle()

Controller가 호출되기 전에 실행되며,

controller 이전에 처리해야 하는 전처리 작업이나 요청 정보를 가공 또는 추가하는 경우에 사용할 수 있다.

2. postHandle()

Controller가 호출된 후(view 렌더링 전)에 실행되며,

controller 이후에 처리해야 하는 후처리 작업이 있을 때 사용할 수 있다.

이 메소드는 controller가 반환하는 ModelAndView 타입의 정보가 제공되는데,

최근에는 JSON 형태로 데이터를 제공하는 RestAPI 기반의 controller(@RestController)를 만들면서 자주 사용되지 않는다.

3. afterCompletion()

모든 view에서 최종 결과를 생성하는 일을 포함해 모든 작업이 완료된 후(view 렌더링 후)에 실행되며,

요청 처리 중 사용한 리소스를 반환할 때 사용할 수 있다.

 

실제 사용은 아래의 'Interceptor 적용하기' 에서 확인할 수 있다.


Interceptor vs AOP

Interceptor 대신에 controller에 적용할 부가기능을 advice로 만들어 AOP를 적용할 수도 있지만

controller는 타입과 실행 메소드가 모두 제각각이라 pointcut(적용할 메소드 선별)의 작성이 어렵고,

파라미터나 리턴 값이 일정하지 않아

controller에 AOP를 적용하려면 번거로운 작업들이 생기게 된다.

따라서 controller 호출 과정에 적용되는 부가기능들은 interceptor을 사용하는 것이 낫다.


Interceptor 적용하기

* 디렉토리 구조 및 공통 클래스

package org.example.interceptor;

@SpringBootApplication
public class Chap05InterceptorApplication {

    public static void main(String[] args) {
        SpringApplication.run(Chap05InterceptorApplication.class, args);
    }

}
package org.example.interceptor;

@Service
public class MenuService  {
    public void method() {
        System.out.println("MenuService 클래스의 method 호출");
    }
}
package com.ohgiraffers.interceptor;

@Controller
public class MainController {

    @GetMapping(value = {"/", "/main"})
    public String main() {
        return "main";
    }
}

 

Interceptor을 이용하여 handler의 수행 시간을 측정하는 테스트를 진행해보자


클라이언트 측에서 수행 시간을 확인하는 요청을 보낸다.

<!--/main/resources/templates/main.html-->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>main</title>
</head>
<body>
    <h1>Handler Interceptor 이용하여 Controller의 Handler Method 요청 가로채기</h1>
    <button onclick="location.href='/stopwatch'">수행 시간 확인하기</button>
</body>
</html>

 

Controller의 핸들러 메소드에서 무언가 수행하는 상황을 상정하기 위해

Thread.sleep(밀리세컨)를 사용하여 잠시 멈추었다가 진행되도록 한다.

package org.example.interceptor;

@Controller
public class InterceptorTestController {

    @GetMapping("/stopwatch")
    public String stopwatch() throws InterruptedException {
        System.out.println("핸들러 메소드 호출함");
        Thread.sleep(1000);
        return "result";
    }
}

 

핸들러로 가는 요청을 가로챌 interceptor를 작성한다.

위에서 언급한대로 interceptor는 HandlerInterceptor 인터페이스를 구현하여 개발할 수 있고,

preHandle(), postHandle(), afterCompletion() 등의 메소드를 오버라이드해서 사용한다.

package org.example.interceptor;

/* HandlerInterceptor 인터페이스를 구현한 클래스로 작성 */
@Component
public class StopWatchInterceptor implements HandlerInterceptor {

    /* Interceptor는 스프링 컨테이너에 존재하는 빈을 의존성 주입 받을 수 있다. */
    private final MenuService menuService;

    public StopWatchInterceptor(MenuService menuService) {
        this.menuService = menuService;
    }

    /* 전처리 메소드 */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle 메소드 호출");

        long startTime = System.currentTimeMillis();
        request.setAttribute("startTime", startTime);

        return true;    // true이면 controller을 이어서 호출하고, false이면 핸들러 메소드를 호출하지 않는다.
    }

    /* 후처리 메소드 */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

        System.out.println("postHandle 메소드 호출함");

        long endTime = System.currentTimeMillis();
        long startTime = (Long) request.getAttribute("startTime");

        modelAndView.addObject("interval", endTime - startTime);
    }

    /* 뷰가 랜더링 된 이후 동작하는 메소드 */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("afterCompletion 메소드 호출");
        menuService.method();
    }
}

 

 

응답할 view를 작성해주어 호출 수행시간을 출력해봤다.

<!--/main/resources/templates/result.html-->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>result</title>
</head>
<body>
    <h1 th:text="|메소드 호출 수행 시간 : ${interval} (ms)|"></h1>
</body>
</html>

 

Interceptor을 사용하려면, 

WebMvcConfigurer 인터페이스를 구현하는 설정 클래스를 만들고

addInterceptors() 메소드를 오버라이드해서 등록해야 한다.

이 등록 순서에 따라 interceptor가 실행되는 순서가 결정된다.

package org.example.interceptor;

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

    private final StopWatchInterceptor stopWatchInterceptor;

    public WebConfiguration(StopWatchInterceptor stopWatchInterceptor) {
        this.stopWatchInterceptor = stopWatchInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(stopWatchInterceptor).addPathPatterns("/stopwatch");
    }
}

 

만약 모든 요청에 대해 interceptor를 적용하고 싶다면 아래와 같이 작성해줄 수 있다.

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

	@Autowired
    private StopWatchInterceptor stopWatchInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(stopWatchInterceptor)
                .addPathPatterns("/*")
                /* static 하위의 정적 리소스는 인터셉터가 적용되지 않도록 한다. */
                .excludePathPatterns("/css/**")
                .excludePathPatterns("/images/**")
                .excludePathPatterns("/js/**")
                /* /error 로 포워딩 되는 경로도 제외해주어야 한다. */
                .excludePathPatterns("/error");
    }
}

 

'Spring Framework > Spring Boot' 카테고리의 다른 글

[Spring Boot] 8. Thymeleaf  (2) 2024.09.11
[Spring Boot] 7. File Upload  (0) 2024.09.11
[Spring Boot] 5. Exception Handler  (0) 2024.09.09
[Spring Boot] 4. View Resolver  (1) 2024.09.09
[Spring Boot] 3. Handler Method  (2) 2024.09.09