728x90
반응형
SMALL

참고자료

 

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 강의 | 김영한 - 인프런

김영한 | 웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습

www.inflearn.com

 

서블릿 필터

이전 포스팅에서 세션을 활용해 로그인 관련 기능을 적용했다. 그런데 한가지 문제가 남아있다. 로그인 기능 자체는 잘 적용했지만 로그인 되지 않은 사용자가 특정 URI만 알고 있다면 해당 URI로 직접 요청해서 바로 들어갈 수 있다. 로그인 된 사용자만 볼 수 있는 화면도 그렇게 들어갈 수 있는 상태이다. 이것을 해결하려면 필터 기능을 사용해서 요청이 컨트롤러에 닿기 전 먼저 로그인 됐는지 확인을 하면 된다. 그리고 그때 필터라는 기능을 사용할 수 있다.

 

서블릿 필터는 서블릿이 지원하는 수문장이다. 필터의 특성은 다음과 같다.

 

필터 흐름

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러

 

필터를 적용하면, 필터가 호출된 다음 서블릿이 호출된다. 그래서 모든 고객의 요청 로그를 남기는 요구사항이 있다면 필터를 적용하는 방법을 고려할 수 있다. 참고로 필터는 특정 URL 패턴에 적용할 수 있다. `/*` 이라고 하면 모든 요청에 필터가 적용된다. 참고로 스프링을 사용한다면 여기서 말하는 서블릿은 디스패처 서블릿이라고 생각하면 된다.

 

필터 제한

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러 //로그인 사용자
HTTP 요청 -> WAS -> 필터(적절하지 않은 요청이라 판단, 서블릿 호출X) //비 로그인 사용자

필터에서 적절하지 않은 요청이라고 판단하면, 거기에서 끝을 낼 수 있다. 그래서 로그인 여부를 체크하기에 딱 좋다.

 

필터 체인

HTTP 요청 -> WAS -> 필터1 -> 필터2 -> 필터3 -> 서블릿 -> 컨트롤러

필터는 체인으로 구성될 수 있다. 중간에 필터를 자유롭게 추가할 수 있다. 예를 들어서 로그를 남기는 필터를 먼저 적용하고, 다음에 로그인 여부를 체크하는 필터를 만들 수 있다.

 

필터 인터페이스

package javax.servlet;

import java.io.IOException;

public interface Filter {
    default void init(FilterConfig filterConfig) throws ServletException {
    }

    void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;

    default void destroy() {
    }
}
  • 필터 인터페이스를 구현하고 등록하면 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고 관리한다.
  • init(): 필터 초기화 메서드. 서블릿 컨테이너가 생성될 때 호출된다.
  • doFilter(): 고객의 요청이 들어올 때마다 해당 메서드가 실행된다. 필터의 로직을 구현하는 부분
  • destroy(): 필터 종료 메서드. 서블릿 컨테이너가 종료될 때 호출된다.

필터에 대한 개념을 좀 더 이해하기 위해 요청 로그를 남기는 서블릿 필터를 만들어보자.

서블릿 필터 - 요청 로그

package hello.login.web.filter;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;

@Slf4j
public class LogFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("LogFilter init");
    }

    @Override
    public void doFilter(ServletRequest servletRequest,
                         ServletResponse servletResponse,
                         FilterChain filterChain) throws IOException, ServletException {
        log.info("LogFilter doFilter");

        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        String requestURI = request.getRequestURI();
        String uuid = UUID.randomUUID().toString();

        try {
            log.info("REQUEST [{}][{}]", uuid, requestURI);
            filterChain.doFilter(request, response);
        } catch (Exception e) {
            throw new ServletException(e);
        } finally {
            log.info("RESPONSE [{}][{}]", uuid, requestURI);
        }

    }

    @Override
    public void destroy() {
        log.info("LogFilter destroy");
    }
}
  • 필터를 사용하려면 필터 인터페이스를 구현해야 하므로 Filter를 구현한다.
  • doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
    • HTTP 요청이 들어오면 doFilter가 호출된다.
    • ServletRequest, ServletResponse는 HTTP 요청이 아닌 경우까지 고려해서 만든 인터페이스이다. HTTP를 사용하면 HttpServletRequest, HttpServletResponse로 다운 캐스팅하면 된다. 
  • HTTP 요청을 구분하기 위해 요청당 임의의 랜덤값인 UUID를 생성한다.
  • log.info("REQUEST [{}][{}]", uuid, requestURI); 필터에서 요청 로그를 출력한다.
  • chain.doFilter(request, response):
    • 이 부분이 제일 중요하다. 다음 필터가 있으면 다음 필터를 호출하고, 필터가 없으면 서블릿을 호출한다. 만약 이 로직을 호출하지 않으면 다음 단계로 진행되지 않는다.
  • 서블릿을 호출하고 모든 작업이 다 끝나면 다시 이 필터로 돌아온다. 응답은 역순이다. 그래서 WAS를 거쳐 다시 고객한테로 응답이 돌아가는 방식이다. 그러면 이 필터로 돌아왔을 때 WAS로 응답이 돌아가기 전 filterChain.doFilter(request, response) 이후에 있는 코드들이 실행된다. 여기서는 finally 구문이 실행될 것이다.

 

필터를 이렇게 만들면 끝난게 아니다. 필터를 만들고 필터를 등록해야 한다.

WebConfig

package hello.login;

import hello.login.web.filter.LogFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class WebConfig {

    @Bean
    public FilterRegistrationBean<LogFilter> logFilter() {
        FilterRegistrationBean<LogFilter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
        filterFilterRegistrationBean.setFilter(new LogFilter());
        filterFilterRegistrationBean.setOrder(1);
        filterFilterRegistrationBean.addUrlPatterns("/*");

        return filterFilterRegistrationBean;
    }
}

필터를 등록하는 방법은 여러가지가 있지만, 스프링 부트를 사용한다면 FilterRegistrationBean을 사용해서 등록하면 된다.

  • setFilter(new LogFilter()); → 등록할 필터를 지정한다.
  • setOrder(1) → 필터는 체인으로 동작하기 때문에 순서가 필요하다. 낮을수록 먼저 동작한다.
  • addUrlPatterns("/*") → 필터를 적용할 URL 패턴을 지정한다. 한번에 여러 패턴을 지정할 수 있다.

이 상태로 우리의 서비스에 요청을 날려보면 로그가 이렇게 남게 된다.

2024-09-04 11:27:50.577  INFO 74694 --- [nio-8080-exec-7] hello.login.web.filter.LogFilter         : REQUEST [5cde931b-fc7e-4a08-a9a8-5f841529a891][/items/add]
2024-09-04 11:27:50.608  INFO 74694 --- [nio-8080-exec-7] hello.login.web.item.ItemController      : errors=org.springframework.validation.BeanPropertyBindingResult: 3 errors
Field error in object 'item' on field 'price': rejected value [null]; codes [NotNull.item.price,NotNull.price,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.price,price]; arguments []; default message [price]]; default message [must not be null]
Field error in object 'item' on field 'quantity': rejected value [null]; codes [NotNull.item.quantity,NotNull.quantity,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.quantity,quantity]; arguments []; default message [quantity]]; default message [must not be null]
Field error in object 'item' on field 'itemName': rejected value []; codes [NotBlank.item.itemName,NotBlank.itemName,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.itemName,itemName]; arguments []; default message [itemName]]; default message [must not be blank]
2024-09-04 11:27:50.613  INFO 74694 --- [nio-8080-exec-7] hello.login.web.filter.LogFilter         : RESPONSE [5cde931b-fc7e-4a08-a9a8-5f841529a891][/items/add]

 

참고로, 실무에서 HTTP 요청 시 같은 요청의 로그에 모두 같은 식별자를 자동으로 남기는 방법은 `logback mdc`로 검색해보자.

 

서블릿 필터 - 인증 체크

이제 인증 관련 필터를 만들어보자. 위에서 만든 로그 필터는 필터의 동작을 알아보기 위한 맛보기였다.

LoginCheckFilter

package hello.login.web.filter;

import hello.login.web.session.SessionConst;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.PatternMatchUtils;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@Slf4j
public class LoginCheckFilter implements Filter {

    private static final String[] whitelist = {"/", "/members/add", "/login", "logout", "/css/*"};

    @Override
    public void doFilter(ServletRequest servletRequest,
                         ServletResponse servletResponse,
                         FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        String requestURI = request.getRequestURI();

        try {
            log.info("인증 체크 필터 시작 {}", requestURI);

            if (isLoginCheckPath(requestURI)) {
                log.info("인증 체크 로직 실행 {}", requestURI);
                HttpSession session = request.getSession(false);
                if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
                    log.info("미인증 사용자 요청 {}", requestURI);
                    response.sendRedirect(request.getContextPath() + "/login?redirectURL=" + requestURI);
                    return;
                }
            }

            filterChain.doFilter(request, response);
        } catch (Exception e) {
            throw new ServletException(e);
        } finally {
            log.info("인증 체크 필터 종료 {}", requestURI);
        }
    }

    private boolean isLoginCheckPath(String requestURI) {
        return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
    }
}
  • private static final String[] whitelist = {"/", "/members/add", "/login", "logout", "/css/*"};
    • 인증 필터를 적용해도 홈, 회원가입, 로그인 화면, CSS와 같은 리소스에는 접근할 수 있어야 한다. 이렇게 화이트 리스트 경로는 인증과 무관하게 항상 허용한다. 화이트 리스트를 제외한 나머지 모든 경로에는 인증 체크 로직을 적용한다.
  • isLoginCheckPath(String requestURI)
    • 화이트 리스트를 제외한 모든 경우에 인증 체크 로직을 적용한다. 스프링에서 지원하는 PatternMatchUtils를 사용해서 간단하게 구현할 수 있다.
  • response.sendRedirect(request.getContextPath() + "/login?redirectURL=" + requestURI);
    • 미인증 사용자는 로그인 화면으로 리다이렉트 한다. 그런데 로그인 이후에 다시 홈으로 이동해버리면 원하는 경로를 다시 찾아가야하는 불편함이 있다. 예를 들어, 상품 관리 화면을 보려고 들어갔다가 로그인 화면으로 이동하면 로그인 이후에 다시 상품 관리 화면으로 들어가는 것이 좋다. 이런 부분이 개발자 입장에서는 좀 귀찮을 수 있어도 사용자 입장으로 보면 편리한 기능이다. 이런 기능을 위해 현재 요청한 경로인 requestURI`/login`에 쿼리 파라미터로 함께 전달한다. `/login` 컨트롤러에서 로그인 성공 시 해당 경로로 이동하는 기능을 추가로 개발해주면 된다.
  • return;
    • 여기가 중요하다. 필터는 더는 진행하지 않는다. 이후 필터는 물론, 서블릿, 컨트롤러가 더는 호출되지 않는다. 앞서 redirect를 사용했기 때문에 redirect가 응답으로 적용되고 요청이 끝난다.

 

이제 이 필터를 등록하자.

package hello.login;

import hello.login.web.filter.LogFilter;
import hello.login.web.filter.LoginCheckFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class WebConfig {

    @Bean
    public FilterRegistrationBean<LogFilter> logFilter() {
        FilterRegistrationBean<LogFilter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
        filterFilterRegistrationBean.setFilter(new LogFilter());
        filterFilterRegistrationBean.setOrder(1);
        filterFilterRegistrationBean.addUrlPatterns("/*");

        return filterFilterRegistrationBean;
    }

    //@Bean
    public FilterRegistrationBean<LoginCheckFilter> loginCheckFilter() {
        FilterRegistrationBean<LoginCheckFilter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
        filterFilterRegistrationBean.setFilter(new LoginCheckFilter());
        filterFilterRegistrationBean.setOrder(2);
        filterFilterRegistrationBean.addUrlPatterns("/*");

        return filterFilterRegistrationBean;
    }
}
  • 위에서 먼저 등록한 LogFilter와 같이 등록한다.

RedirectURL 처리

@PostMapping("/login")
public String loginForm(@Validated @ModelAttribute LoginForm loginForm,
                        BindingResult bindingResult,
                        @RequestParam(defaultValue = "/") String redirectURL,
                        HttpServletRequest req) {
    if (bindingResult.hasErrors()) {
        return "login/loginForm";
    }

    Member loginMember = loginService.login(loginForm.getLoginId(), loginForm.getPassword());
    if (loginMember == null) {
        bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
        return "login/loginForm";
    }

    // 세션이 있으면 세션 반환, 없으면 신규 세션을 생성
    HttpSession session = req.getSession();
    session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);

    return "redirect:" + redirectURL;
}
  • 위에서 설명한 대로 사용자가 로그인에 성공하면 원래 진입하려고 했던 경로로 다시 보내주면 사용자 입장에서는 아주 편리한 기능이다. 그래서 로그인 처리 컨트롤러에서 @RequestParam을 이용해서 아까 쿼리 파라미터로 넘긴 `redirectURL`을 받는다. 없을 수도 있으니 defaultValue`/`로 지정한다. 
  • 그리고 마지막에 return "redirect:" + redirectURL; 이렇게 리다이렉트한다.

 

이렇게 필터와 로그인 처리를 해두고 실행해보자. 이제 특정 URI를 알고 있어도 로그인하지 않았다면 로그인 화면으로 리다이렉트 될 것이다. 필터가 잘 동작하고 있는것이다. 필터를 사용해도 충분히 원하는 기능을 구현할 수 있지만 스프링이 제공하는 인터셉터라는 기능이 있다. 아주아주 강력한 기능이고 스프링을 사용한다면 이 인터셉터를 사용하면 더 좋을 수 있다. 이 인터셉터에 대해 알아보자.

 

인터셉터

스프링에서 제공하는 인터셉터도 서블릿 필터와 같이 웹과 관련된 공통 관심 사항을 효과적으로 해결할 수 있는 기술이다.

서블릿 필터가 서블릿이 제공하는 기술이라면, 스프링 인터셉터는 스프링 MVC가 제공하는 기술이다. 둘다 웹과 관련된 공통 관심사항을 처리하지만, 적용되는 순서와 범위 그리고 사용방법이 다르다.

 

스프링 인터셉터의 흐름

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
  • 스프링 인터셉터는 디스패처 서블릿과 컨트롤러 사이에 존재한다.
  • 스프링 인터셉터는 스프링 MVC가 제공하는 기능이기 때문에 결국 디스패처 서블릿 이후에 등장하게 된다. 스프링 MVC의 시작점이 디스패처 서블릿이라고 생각해보면 이해가 될 것이다.
  • 스프링 인터셉터에도 URL 패턴을 적용할 수 있는데 서블릿 URL 패턴과는 다르고, 매우 정밀하게 설정할 수 있다.

스프링 인터셉터의 제한

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러 //로그인 사용자
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터(적절하지 않은 요청이라 판단, 컨트롤러 호출X) // 비 로그인 사용자

 

인터셉터에서 적절하지 않은 요청이라고 판단하면 거기에서 끝을 낼 수 있다. 그래서 로그인 여부를 체크하기에 딱 알맞다.

 

스프링 인터셉터 체인

HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터1 -> 인터셉터2 -> 컨트롤러

스프링 인터셉터는 체인으로 구성되는데, 중간에 인터셉터를 자유롭게 추가할 수 있다. 예를 들어서 로그를 남기는 인터셉터를 먼저 적용하고, 그 다음에 로그인 여부를 체크하는 인터셉터를 만들 수 있다.

 

스프링 인터셉터 인터페이스

package org.springframework.web.servlet;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;

public interface HandlerInterceptor {
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return true;
    }

    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
    }

    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
    }
}
  • 스프링 인터셉터를 사용하려면 HandlerInterceptor 인터페이스를 구현하면 된다.
  • preHandle: 컨트롤러 호출 전에 실행되는 메서드
  • postHandle: 컨트롤러 호출 후 실행되는 메서드
  • afterCompletion: 요청 완료 이후에 실행되는 메서드
  • 서블릿 필터의 경우, 단순히 request, response만 제공했지만, 인터셉터는 어떤 컨트롤러(handler)가 호출되는지 호출 정보도 받을 수 있고 어떤 modelAndView가 반환되는지도 알 수 있다.

 

스프링 인터셉터 정상 호출 흐름

  • preHandle: 컨트롤러 호출 전에 호출된다. (더 정확히는 핸들러 어댑터 호출 전에 호출된다.) 중요한 건 이 preHandle의 반환값이 true이면 다음으로 진행하고, false이면 더는 진행하지 않는다. false인 경우 나머지 인터셉터는 물론이고 핸들러 어댑터도 호출되지 않는다. 
  • postHandle: 컨트롤러 호출 후에 호출된다. 
  • afterCompletion: 뷰가 렌더링 된 이후에 호출된다. 

 

스프링 인터셉터 예외 상황

  • preHandle: 컨트롤러 호출 전에 호출된다.
  • postHandle: 컨트롤러에서 예외가 발생하면 postHandle은 호출되지 않는다.
  • afterCompletion: 항상 호출된다. 예외가 발생하면 파라미터로 받는 `ex`에 예외를 받아 어떤 예외가 발생했는지 로그를 출력할 수 있다.

afterCompletion은 예외가 발생하던 발생하지 않던 호출되기 때문에 예외와 무관하게 공통 처리가 필요한 부분은 여기서 처리하면 된다. 그리고 이 메서드는 예외 정보(ex)도 받는다. 

 

정리를 하자면

인터셉터는 스프링 MVC 구조에 특화된 필터 기능을 제공한다고 이해하면 된다. 스프링 MVC를 사용하고, 특별히 필터를 꼭 사용해야 하는 상황이 아니라면 인터셉터를 사용하는 것이 더 편리하다.

 

스프링 인터셉터 - 요청 로그

인터셉터 맛보기를 해보자. 사용자의 요청 로그를 찍어보자.

LogInterceptor

package hello.login.web.interceptor;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

@Slf4j
public class LogInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String uuid = UUID.randomUUID().toString();
        request.setAttribute("uuid", uuid);
        
        // @RequestMapping: HandlerMethod
        // 정적 리소스: ResourceHttpRequestHandler
        if (handler instanceof HandlerMethod) {
            HandlerMethod hm = (HandlerMethod) handler;
        }
        
        log.info("REQUEST [{}][{}][{}]", uuid, request.getRequestURI(), handler);
        return true;
    }

    @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 uuid = (String) request.getAttribute("uuid");
        String requestURI = request.getRequestURI();
        log.info("RESPONSE [{}][{}][{}]", uuid, requestURI, handler);

        if (ex != null) {
            log.error("afterCompletion err", ex);
        }
    }
}
  • 요청 로그를 구분하기 위한 UUID를 생성한다.
  • request.setAttribute("uuid", uuid); → 서블릿 필터의 경우 doFilter 메서드 안에서 모든것을 처리하기 때문에 상관없지만 인터셉터의 경우 호출 시점이 분리되어 있기 때문에 preHandle에서 지정한 값을 postHandle, afterCompletion에서 사용하려면 어딘가에 담아두어야 한다. 인터페이스도 싱글톤처럼 사용되기 때문에 멤버변수를 사용하면 멀티스레드 환경에서 위험하다. 따라서 request에 담는다. 
  • return true; true를 반환해야만 다음 인터셉터 또는 컨트롤러로 넘어간다.

위에서 말했지만, 어떤 컨트롤러를 호출할지 이미 preHandle 메서드가 호출되는 시점은 알고 있기 때문에 컨트롤러 정보도 알 수 있고 컨트롤러 정보를 통해 어떤 작업을 할 수 있음을 보여주기 위해 다음 코드를 작성했다.

// @RequestMapping: HandlerMethod
// 정적 리소스: ResourceHttpRequestHandler
if (handler instanceof HandlerMethod) {
    HandlerMethod hm = (HandlerMethod) handler;
}
  • 스프링에서는 일반적으로 @Controller, @RequestMapping 이러한 애노테이션 기반 핸들러 매핑을 사용하는데 이 경우, 핸들러 정보로 HandlerMethod가 넘어온다. 
  • @Controller가 아니라 /resources/static과 같은 정적 리소스가 호출되는 경우 ResourceHttpRequestHandler가 핸들러 정보로 넘어오기 때문에 타입에 따라 처리가 필요하다.

위에서 말했지만, 컨트롤러 이후 단계에서 에러가 발생해서 응답과정에 인터셉터까지 에러가 올라오는 경우, postHandle은 호출되지 않는다고 했다. 그렇기 때문에 afterCompletion 메서드에 종료 로그를 출력하게 했다.

 

인터셉터 등록

package hello.login;

import hello.login.web.interceptor.LogInterceptor;
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 LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/css/**", "/*.ico");
    }
}
  • WebMvcConfigurer가 제공하는 addInterceptors()를 사용해서 인터셉터를 등록할 수 있다.
  • addInterceptor(new LogInterceptor()): 인터셉터를 등록한다.
  • order(1): 인터셉터 호출 순서를 지정한다. 낮을수록 먼저 호출된다.
  • addPathPatterns("/**"): 인터셉터를 적용할 URL 패턴을 지정한다.
  • excludePathPatterns("/css/**", "/*.ico"): 인터셉터에서 제외할 패턴을 지정한다.

실행 로그

[2024-09-04 14:05:13] [http-nio-8080-exec-1] [] INFO  h.l.web.interceptor.LogInterceptor - REQUEST [e140ad1a-e3c8-42d7-8d20-598751f3adf0][/][hello.login.web.HttpServletSessionHomeController#homeWithSpring(Member, Model)]
[2024-09-04 14:05:13] [http-nio-8080-exec-1] [] INFO  h.l.web.interceptor.LogInterceptor - postHandle [ModelAndView [view="home"; model={}]]
[2024-09-04 14:05:13] [http-nio-8080-exec-1] [] INFO  h.l.web.interceptor.LogInterceptor - RESPONSE [e140ad1a-e3c8-42d7-8d20-598751f3adf0][/][hello.login.web.HttpServletSessionHomeController#homeWithSpring(Member, Model)]
  • 정말 신기하게도, 어떤 컨트롤러의 어떤 메서드를 호출하는지에 대한 정보도 출력되는 것을 볼 수 있다.
  • postHandle에서는 만약 뷰를 렌더링한다면, Model 정보와 View 정보도 출력한다.

스프링의 URL 경로

인터셉터 등록할 때 경로를 지정했었는데, 서블릿 기술이 제공하는 URL 경로와 완전히 다르다. 더욱 자세하고 세밀하게 설정할 수 있다.

? 한 문자 일치
* 경로(/) 안에서 0개 이상의 문자 일치
** 경로 끝까지 0개 이상의 경로(/) 일치
{spring} 경로(/)와 일치하고 spring이라는 변수로 캡처
{spring:[a-z]+} matches the regexp [a-z]+ as a path variable named "spring" {spring:[a-z]+} regexp [a-z]+ 와 일치하고, "spring" 경로 변수로 캡처
{*spring} 경로가 끝날 때 까지 0개 이상의 경로(/)와 일치하고 spring이라는 변수로 캡처
 /pages/t?st.html — matches /pages/test.html, /pages/tXst.html but not /pages/
 toast.html
 /resources/*.png — matches all .png files in the resources directory
 /resources/** — matches all files underneath the /resources/ path, including /
 resources/image.png and /resources/css/spring.css
 /resources/{*path} — matches all files underneath the /resources/ path and
 captures their relative path in a variable named "path"; /resources/image.png
will match with "path" → "/image.png", and /resources/css/spring.css will match
with "path" → "/css/spring.css"
 /resources/{filename:\\w+}.dat will match /resources/spring.dat and assign the
 value "spring" to the filename variable

참조 링크

 

PathPattern (Spring Framework 6.1.12 API)

Compare this pattern with a supplied pattern: return -1,0,+1 if this pattern is more specific, the same or less specific than the supplied pattern.

docs.spring.io

 

스프링 인터셉터 - 인증 체크

이제 인터셉터를 활용해서 인증을 체크해보자! 이걸 사용하면 서블릿 필터 생각도 안날것이다.

LoginCheckInterceptor

package hello.login.web.interceptor;

import hello.login.web.session.SessionConst;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {

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

        String requestURI = request.getRequestURI();
        log.info("인증 체크 인터셉터 실행: {}", requestURI);

        HttpSession session = request.getSession();

        if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
            log.info("미인증 사용자 요청");
            response.sendRedirect(request.getContextPath() + "/login?redirectURL=" + requestURI);
            return false;
        }
        return true;
    }
}
  • 인터셉터가 제공하는 세개의 메서드 중 우리가 필요한 것은 딱 하나 preHandle이다. 왜냐하면 컨트롤러에 도달하기 전 인증만 하면 끝이기 때문에 컨트롤러가 호출된 후의 메서드나 요청이 끝난 후 메서드는 필요가 없다.
  • 미인증 사용자라면 리다이렉트 후 `return false;`를 호출하면 된다.
  • 인증 사용자라면 `return true;`를 호출하면 된다.

이게 끝이다. 어? 서블릿 필터에서는 허용 URL을 따로 작성해서 그 URL에 일치하는지 확인하고 뭐하고 이런 코드가 여기선 하나도 없다. 왜냐? 인터셉터 등록할 때 아주 편리하고 세밀하게 적용하면 되니까.

 

인터셉터 등록

WebConfig

package hello.login;

import hello.login.web.interceptor.LoggingInterceptor;
import hello.login.web.interceptor.LoginCheckInterceptor;
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 LoggingInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/css/**", "/*.ico");

        registry.addInterceptor(new LoginCheckInterceptor())
                .order(2)
                .addPathPatterns("/**")
                .excludePathPatterns("/", "/members/add", "/login", "/logout", "/css/**", "/*.ico", "/error");
    }
}
  • 두번째 인터셉터를 등록한다. 일단 모든 경로에 이 인터셉터를 적용한다음 excludePathPatterns로 인증이 필요없는 경우를 걸러버리면 끝이다. 너무 편리하다.

 

정리를 하자면

서블릿 필터를 사용하거나 스프링 MVC의 인터셉터를 사용해서 인증 처리를 공통적으로 할 수 있다. 그러나, 둘 다 사용해본 입장에서 인터셉터를 두고 필터를 사용할 필요가 없다는 게 느껴진다. 이렇게 인터셉터를 알아보았다. 다음 포스팅에서는 ArgumentResolver를 사용해서 세션에 있는 로그인 한 유저를 굉장히 간편하게 가져올 수 있는 방법을 알아보자. 이것을 배우면 세션에 있는 로그인 한 유저를 간편하게 가져오는 방법 뿐만 아니라, 컨트롤러에서 공통 작업이 필요한 경우 전부 다 응용할 수 있다.

 

728x90
반응형
LIST

+ Recent posts