[Spring] 29. Interceptor란?

[Spring] 29. Interceptor란?

이전 글에서는 업로드한 파일을 조회하고 다운로드하는 방법을 살펴봤습니다. 파일 자체는 서버 폴더나 외부 저장소에 저장하고, DB에는 원본 파일명과 저장 파일명을 저장한 뒤, ResponseEntity<Resource>로 파일을 응답할 수 있었습니다.

이번 글에서는 Spring MVC에서 공통 요청 처리를 담당하는 Interceptor에 대해 정리하겠습니다. 로그인 체크, 권한 체크, 관리자 접근 제한, 요청 로그 기록처럼 여러 Controller에서 반복되는 작업은 Interceptor로 분리할 수 있습니다.

핵심은 간단합니다. Interceptor는 요청이 Controller에 도착하기 전이나, Controller 실행 후에 공통 작업을 처리할 수 있는 Spring MVC 기능입니다.


1. Interceptor란?

Interceptor는 Spring MVC 요청 처리 흐름 중간에 끼어들어 공통 작업을 수행하는 기능입니다. 사용자의 요청이 Controller 메서드에 도착하기 전에 먼저 확인하거나, Controller 실행 후 추가 작업을 할 수 있습니다.

사용자 요청
 → DispatcherServlet
 → Interceptor
 → Controller
 → Service
 → Repository / Mapper

예를 들어 로그인한 사용자만 게시글 작성 화면에 접근할 수 있다고 생각해보겠습니다. Controller마다 로그인 여부를 직접 확인하면 코드가 반복됩니다.

@GetMapping("/board/write")
public String writeForm(HttpSession session) {
    Object loginMember = session.getAttribute("loginMember");

    if (loginMember == null) {
        return "redirect:/member/login";
    }

    return "board/write";
}

이런 코드가 게시글 작성, 수정, 삭제, 마이페이지, 관리자 페이지마다 반복되면 Controller가 지저분해집니다. 이때 Interceptor를 사용하면 로그인 체크를 한 곳에서 처리할 수 있습니다.


2. Interceptor가 필요한 이유

웹 애플리케이션에는 여러 요청에서 공통으로 확인해야 하는 작업이 많습니다.

공통 작업 예시
로그인 체크 로그인한 사용자만 글쓰기, 마이페이지 접근 허용
권한 체크 관리자만 관리자 페이지 접근 허용
요청 로그 기록 요청 URL, 실행 시간, 사용자 정보 기록
공통 데이터 준비 메뉴, 사용자 요약 정보, 알림 수 등 공통 값 준비

이런 작업을 Controller마다 직접 작성하면 중복 코드가 많아집니다. 중복이 많아지면 수정할 때 빠뜨리기 쉽고, Controller가 요청 처리보다 검사 코드에 더 많이 묶이게 됩니다.

Controller
 → 요청 처리에 집중

Interceptor
 → 공통 요청 검사 담당

즉, Interceptor는 Controller를 깔끔하게 유지하기 위해 사용합니다.


3. Interceptor의 위치

Spring MVC에서 요청은 DispatcherServlet을 거쳐 Controller로 전달됩니다. Interceptor는 DispatcherServlet과 Controller 사이에서 동작합니다.

사용자 요청
 → DispatcherServlet
 → HandlerMapping
 → Interceptor preHandle()
 → Controller 실행
 → Interceptor postHandle()
 → View 렌더링
 → Interceptor afterCompletion()
 → 응답 반환

Interceptor는 Controller 실행 전, 실행 후, 요청 완료 후에 각각 동작할 수 있습니다. 이 흐름을 이해하면 어떤 작업을 어디에 넣어야 할지 판단하기 쉽습니다.


4. HandlerInterceptor란?

Spring MVC에서 Interceptor를 만들 때는 보통 HandlerInterceptor 인터페이스를 구현합니다.

import org.springframework.web.servlet.HandlerInterceptor;

HandlerInterceptor에는 대표적으로 세 가지 메서드가 있습니다.

메서드 실행 시점
preHandle() Controller 실행 전에 호출됩니다.
postHandle() Controller 실행 후, View 렌더링 전에 호출됩니다.
afterCompletion() 요청 처리가 모두 끝난 뒤 호출됩니다.

로그인 체크나 권한 체크는 보통 Controller 실행 전에 해야 하므로 preHandle()에서 처리합니다.


5. preHandle()

preHandle()은 Controller가 실행되기 전에 호출됩니다. 이 메서드의 반환값에 따라 요청을 계속 진행할지 막을지 결정할 수 있습니다.

@Override
public boolean preHandle(
        HttpServletRequest request,
        HttpServletResponse response,
        Object handler
) throws Exception {
    return true;
}
true 요청을 계속 진행합니다. Controller가 실행됩니다.
false 요청을 중단합니다. Controller가 실행되지 않습니다.

따라서 로그인하지 않은 사용자를 로그인 페이지로 보내고 싶다면, response.sendRedirect()를 실행한 뒤 false를 반환하면 됩니다.


6. 로그인 체크 Interceptor 만들기

로그인 여부를 확인하는 Interceptor를 만들어보겠습니다. 세션에 loginMember가 없으면 로그인 페이지로 이동시키는 구조입니다.

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.web.servlet.HandlerInterceptor;

public class LoginCheckInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler
    ) throws Exception {
        HttpSession session = request.getSession(false);

        if (session == null || session.getAttribute("loginMember") == null) {
            response.sendRedirect("/member/login");
            return false;
        }

        return true;
    }
}

코드 흐름은 다음과 같습니다.

1. 요청이 들어온다.
2. preHandle()이 먼저 실행된다.
3. 세션이 있는지 확인한다.
4. loginMember가 있는지 확인한다.
5. 없으면 /member/login으로 redirect한다.
6. false를 반환해 Controller 실행을 막는다.
7. 로그인 상태라면 true를 반환해 Controller로 진행한다.

여기서 request.getSession(false)는 기존 세션이 있을 때만 가져옵니다. 세션이 없는데도 새 세션을 만들지 않기 위해 false를 사용합니다.


7. Interceptor 등록하기

Interceptor 클래스를 만들었다고 바로 동작하는 것은 아닙니다. Spring MVC 설정에 Interceptor를 등록해야 합니다.

Spring Boot에서는 WebMvcConfigurer를 구현한 설정 클래스를 만들고, addInterceptors()에서 등록할 수 있습니다.

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginCheckInterceptor())
                .addPathPatterns("/board/write", "/board/*/edit", "/mypage/**")
                .excludePathPatterns("/member/login", "/member/join", "/css/**", "/js/**", "/images/**");
    }
}

이 설정은 특정 경로에 로그인 체크 Interceptor를 적용합니다.

addInterceptor() 등록할 Interceptor를 지정합니다.
addPathPatterns() Interceptor를 적용할 URL 패턴을 지정합니다.
excludePathPatterns() Interceptor 적용에서 제외할 URL 패턴을 지정합니다.

8. 적용 경로와 제외 경로

Interceptor는 모든 요청에 무조건 적용하기보다, 필요한 경로에만 적용하는 것이 좋습니다.

registry.addInterceptor(new LoginCheckInterceptor())
        .addPathPatterns("/**")
        .excludePathPatterns(
                "/",
                "/member/login",
                "/member/join",
                "/css/**",
                "/js/**",
                "/images/**"
        );

위 설정은 전체 요청에 Interceptor를 적용하되, 홈, 로그인, 회원가입, 정적 리소스는 제외합니다.

정적 리소스를 제외하지 않으면 CSS, JS, 이미지 요청까지 로그인 체크에 걸려 화면이 깨질 수 있습니다.

제외해야 하는 경우가 많은 경로

/
 /member/login
 /member/join
 /css/**
 /js/**
 /images/**
 /favicon.ico

9. URL 패턴 이해하기

Interceptor를 등록할 때 URL 패턴을 사용합니다. 자주 쓰는 패턴은 다음과 같습니다.

패턴 의미
/board/* /board/list, /board/write처럼 한 단계 하위 경로에 적용됩니다.
/board/** /board 아래 모든 하위 경로에 적용됩니다.
/** 전체 요청에 적용됩니다.
/admin/** 관리자 관련 모든 경로에 적용됩니다.

처음에는 addPathPatterns()로 넓게 적용하고, excludePathPatterns()로 예외 경로를 빼는 방식이 이해하기 쉽습니다.


10. 관리자 권한 체크 Interceptor

로그인 체크뿐만 아니라 관리자 권한 체크도 Interceptor로 처리할 수 있습니다. 예를 들어 세션의 로그인 사용자 정보에 권한이 들어 있다고 가정해보겠습니다.

public class AdminCheckInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler
    ) throws Exception {
        HttpSession session = request.getSession(false);

        if (session == null || session.getAttribute("loginMember") == null) {
            response.sendRedirect("/member/login");
            return false;
        }

        LoginMember loginMember = (LoginMember) session.getAttribute("loginMember");

        if (!"ADMIN".equals(loginMember.getRole())) {
            response.sendRedirect("/error/403");
            return false;
        }

        return true;
    }
}

이 Interceptor는 로그인 여부를 먼저 확인하고, 로그인한 사용자가 관리자 권한을 가지고 있는지 확인합니다.

로그인 안 됨
 → /member/login

로그인 됨 + 관리자 아님
 → /error/403

로그인 됨 + 관리자
 → Controller 실행

설정에서는 관리자 경로에만 적용할 수 있습니다.

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new AdminCheckInterceptor())
            .addPathPatterns("/admin/**")
            .excludePathPatterns("/css/**", "/js/**", "/images/**");
}

11. postHandle()

postHandle()은 Controller가 실행된 뒤, View가 렌더링되기 전에 호출됩니다.

@Override
public void postHandle(
        HttpServletRequest request,
        HttpServletResponse response,
        Object handler,
        ModelAndView modelAndView
) throws Exception {
    System.out.println("Controller 실행 후 호출");
}

이 메서드는 Controller 실행 결과를 다룰 수 있습니다. 다만 API 응답이나 예외 상황에서는 기대한 대로 항상 사용하기 어려울 수 있으므로 처음에는 preHandle()을 중심으로 이해하면 됩니다.


12. afterCompletion()

afterCompletion()은 요청 처리가 완전히 끝난 뒤 호출됩니다. View 렌더링까지 끝난 뒤 실행되는 단계입니다.

@Override
public void afterCompletion(
        HttpServletRequest request,
        HttpServletResponse response,
        Object handler,
        Exception ex
) throws Exception {
    System.out.println("요청 완료 후 호출");
}

요청 처리 시간 로그, 예외 로그, 정리 작업 같은 용도로 사용할 수 있습니다.

preHandle()
 → Controller 실행 전

postHandle()
 → Controller 실행 후, View 렌더링 전

afterCompletion()
 → 요청 완료 후

13. 요청 시간 측정 예시

Interceptor를 사용하면 요청 처리 시간을 측정할 수도 있습니다.

public class LoggingInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler
    ) {
        request.setAttribute("startTime", System.currentTimeMillis());
        return true;
    }

    @Override
    public void afterCompletion(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler,
            Exception ex
    ) {
        Long startTime = (Long) request.getAttribute("startTime");
        long endTime = System.currentTimeMillis();

        System.out.println("요청 URI: " + request.getRequestURI());
        System.out.println("처리 시간: " + (endTime - startTime) + "ms");
    }
}

이 구조에서는 요청이 시작될 때 시간을 저장하고, 요청이 끝난 뒤 처리 시간을 계산합니다.


14. Filter와 Interceptor의 차이

Interceptor와 함께 자주 등장하는 개념이 Filter입니다. 둘 다 요청 중간에 공통 처리를 할 수 있지만 위치가 다릅니다.

구분 Filter Interceptor
동작 위치 Servlet 영역 Spring MVC 영역
Controller 접근 Controller에 도착하기 전 Controller 실행 전후
주 사용 인코딩, 보안 필터, 요청 전처리 로그인 체크, 권한 체크, 요청 로그
Spring MVC 정보 활용 상대적으로 제한적 Handler 정보 등 Spring MVC 흐름과 가깝습니다.

처음에는 이렇게 기억하면 됩니다.

Filter
 → 더 앞단의 공통 처리

Interceptor
 → Spring MVC Controller 주변의 공통 처리

로그인 체크나 관리자 권한 체크처럼 Controller 요청 흐름과 가까운 작업은 Interceptor로 처리하기 좋습니다.


15. 자주 하는 실수

1) Interceptor를 만들고 등록하지 않는 경우

Interceptor 클래스를 만들기만 하면 동작하지 않습니다. 반드시 WebMvcConfigurer에서 등록해야 합니다.

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginCheckInterceptor());
    }
}

2) 로그인 페이지까지 로그인 체크에 걸리는 경우

전체 경로에 Interceptor를 적용하면서 로그인 페이지를 제외하지 않으면 로그인 페이지에 접근할 때도 로그인 체크가 걸립니다. 그러면 계속 로그인 페이지로 redirect되는 문제가 생길 수 있습니다.

.excludePathPatterns("/member/login", "/member/join")

3) CSS, JS, 이미지 요청을 제외하지 않는 경우

정적 리소스 요청까지 Interceptor에 걸리면 화면 스타일이나 이미지가 깨질 수 있습니다.

.excludePathPatterns("/css/**", "/js/**", "/images/**", "/favicon.ico")

4) false를 반환하지 않는 경우

로그인하지 않은 사용자를 redirect한 뒤에도 true를 반환하면 Controller가 계속 실행될 수 있습니다. 요청을 막아야 한다면 false를 반환해야 합니다.

response.sendRedirect("/member/login");
return false;

5) Controller에 로그인 체크 코드를 계속 남겨두는 경우

Interceptor로 로그인 체크를 분리했다면 Controller에서는 중복 검사 코드를 줄이는 것이 좋습니다. Controller는 요청 처리와 Service 호출에 집중해야 합니다.


16. 전체 흐름 정리

로그인 체크 Interceptor의 전체 흐름을 정리하면 다음과 같습니다.

1. 사용자가 보호된 URL로 요청한다.
2. DispatcherServlet이 요청을 받는다.
3. Controller 실행 전에 Interceptor의 preHandle()이 실행된다.
4. 세션에서 loginMember를 확인한다.
5. 로그인 정보가 없으면 로그인 페이지로 redirect한다.
6. false를 반환해 Controller 실행을 막는다.
7. 로그인 정보가 있으면 true를 반환한다.
8. Controller가 정상 실행된다.

정리

Interceptor는 Spring MVC 요청 처리 흐름 중간에서 공통 작업을 수행하는 기능입니다. Controller 실행 전에 로그인 체크나 권한 체크를 할 수 있고, 요청 완료 후 로그를 남길 수도 있습니다.

preHandle() Controller 실행 전에 호출됩니다. 로그인 체크, 권한 체크에 자주 사용합니다.
postHandle() Controller 실행 후, View 렌더링 전에 호출됩니다.
afterCompletion() 요청 처리가 끝난 뒤 호출됩니다. 로그나 정리 작업에 사용할 수 있습니다.
WebMvcConfigurer Interceptor를 등록할 때 사용합니다.

처음에는 다음 기준만 기억하면 됩니다.

공통 로그인 체크
 → Interceptor

Controller 실행 전 검사
 → preHandle()

요청을 계속 진행
 → return true

요청을 중단
 → response.sendRedirect(...)
 → return false

등록 위치
 → WebMvcConfigurer의 addInterceptors()

연습해보기

아래 요구사항을 기준으로 Interceptor를 설계해보세요.

요구사항

1. /mypage/** 경로는 로그인한 사용자만 접근할 수 있다.
2. /board/write 경로도 로그인한 사용자만 접근할 수 있다.
3. /admin/** 경로는 관리자만 접근할 수 있다.
4. /member/login, /member/join, /css/**, /js/**, /images/** 는 검사에서 제외한다.

다음 글에서는 Filter와 Interceptor의 차이를 조금 더 자세히 정리하겠습니다. 요청이 Spring MVC에 도착하기 전과 후의 흐름을 비교하면 공통 처리 위치를 더 정확히 잡을 수 있습니다.