티스토리 뷰

스프링은 공통적으로 여러 작업을 처리함으로써 중복된 코드를 제거할 수 있도록 많은 기능들을 지원하고 있습니다.

 

저번 시간에는 로그인을 처리하는 데 사용되는 쿠키와 세션에 대해 알아보았습니다.

 

[Spring] 로그인 처리 (1/2) - 쿠키(Cookie)와 세션(Session)

개발자(혹은 우리)가 로그인 기능을 구현할 때는 고려할 부분이 생각보다 많습니다.예를 들어, 로그인 페이지에서 아이디와 비밀번호를 서버로 전송했을 때, 서버에서 로그인을 처리하는 로직

developshrimp.com

 

 

이번에는 필터와 인터셉터의 차이에 대해 알아보도록 하겠습니다.

 

대부분 많은 웹 서비스는 로그인을 해야 서비스를 이용할 수 있습니다.

로그인을 하지 않은 사용자는 접근할 수 있는 페이지가 제한적이며 로그인이 필요한 페이지 접근이 허용되서는 안됩니다. 

하지만, 그렇다고 로그인이 필요한 모든 컨트롤러 로직에 로그인 여부를 확인하는 코드를 작성하는 것은 너무 비효율적이고 수정에도 취약합니다.

 

이렇게 많은 로직에서 공통으로 관심이 있는 부분을 공통 관심사(cross-cutting concerns)라고 합니다.

여러 로직에서 공통으로 로그인에 관심을 가지고 있는데, 이러한 공통 관심사는 스프링에서 AOP로 처리할 수 있습니다.

 

하지만, 웹에 관련된 공통 관심사는 스프링 AOP 보다는 서블릿 필터, 스프링 인터셉터에서 처리하는게 더 좋다고 합니다.

 

웹과 관련된 공통 관심사를 처리할 때는 HTTP의 헤더나 URL 정보가 필요한데 서블릿 필터나, 스프링 인터셉터는 HttpServletRequest를 제공하기 때문입니다.

 


 

필터(Filter)

 

필터(Filter)J2EE 표준 스펙 기능으로 디스패처 서블릿(Dispatcher Servlet)에 요청이 전달되기 전/후에 URL 패턴에 맞는 모든 요청에 대해 부가작업을 처리할 수 있는 기능을 제공합니다.

 

디스패처 서블릿(Dispatcher Servlet)은 스프링의 가장 앞단에 존재하는 프론트 컨트롤러입니다.

그렇기에 필터는 스프링 범위 밖에서 처리가 됩니다.

 

즉, 스프링 컨테이너가 아닌 톰캣과 같은 웹 컨테이너(서블릿 컨테이너)에 의해 관리가 되는 것이고 디스패처 서블릿 전/후에 처리하는 것입니다.

[필터 흐름] HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러

 

 

필터(Filter)의 메소드

 

필터를 추가하기 위해서는 jakarta.servlet의 Filter 인터페이스를 구현(implements) 해야 하며, 이는 아래와 같은 3가지 메소드를 가지고 있습니다. 

public interface Filter {

    default void init(FilterConfig filterConfig) throws ServletException { }

    void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException;
            
    default void destroy() { }
}

 

init 메소드

init 메소드는 필터 객체를 초기화하고 서비스에 추가하기 위한 메소드입니다.

웹 컨테이너가 1회 init 메소드를 호출하여 필터 객체를 초기화하면 이후의 요청들은 doFilter를 통해 처리됩니다.

 

 

doFilter 메소드

doFilter 메소드는 url-pattern 에 맞는 모든 HTTP 요청이 디스패처 서블릿으로 전달되기 전에 웹 컨테이너에 의해 실행되는 메소드입니다. 

 

doFilter 메소드의 파라미터로는 FilterChain이 있는데, FilterChain의 doFilter를 통해 다음 대상으로 요청을 전달하게 됩니다.

 

chain.doFilter() 전/후에 우리가 필요한 처리 과정을 넣어줌으로써 원하는 처리를 진행할 수 있습니다.

 

 

destory 메소드

destroy 메소드는 필터 객체를 서비스에서 제거하고 사용하는 자원을 반환하기 위한 메소드입니다.

이는 웹 컨테이너에 의해 1번 호출되면 이후에는 이제 doFilter 에 의해 처리되지 않습니다.

 

 

 

(필터 활용) API 요청 로그 필터 구현해보기

필터를 직접 사용해보기 위해 모든 요청에 대해 로그를 남기는 필터를 개발해보겠습니다.

 

우선 모든 요청에 대해 로그를 남기는 LogFilter 라는 클래스를 만들었습니다.

 

Filter 인터페이스를 구현하며 init, doFilter, destroy 메서드를 재정의해줍니다.

@Slf4j
public class LogFilter implements Filter {

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

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("log filter doFilter");

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String requestURI = httpRequest.getRequestURI();

        String uuid = UUID.randomUUID().toString();

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

    }

    @Override
    public void destroy() {
        log.info("log filter destroy");
    }
}

 

 

doFilter(ServletRequest request, ServletResponse response, FilterChain chain)

HTTP 요청이 오면 doFilter 가 호출된다.

ServletRequest request 는 HTTP 요청이 아닌 경우도 고려해서 만든 인터페이스 입니다.

HTTP를 사용한다면 HttpServletRequest로 다운캐스팅 후 사용하면 됩니다.

 

 

UUID.randomUUID().toString()

HTTP 요청을 구분하기 위해 요청당 임의의 uuid를 만듭니다. UUID로 만드는 값이 중복될 일은 거의 없습니다.

 

 

try-catch 문에 chain.doFilter(request, response);

가장 중요한 부분입니다.

다음 필터가 있다면 다음 필터를 호출하고 필터가 없으면 서블릿을 호출합니다.

만약 이 로직을 호출하지 않는다면 다음 단계로 진행되지 않습니다.

 

해당 필터가 다 만들어졌다면 Configuration을 만들어 해당 필터를 등록해주어야 합니다.

그 전에 이번에는 로그인 인증 체크 필터를 만들어보도록 하겠습니다.

 

 

(필터 활용) 로그인 인증 체크 필터

whitelist로 지정한 경로를 제외하고는 모두 로그인 상태를 검사 후 페이지 접근 여부를 결정하게 하였습니다.

 

예를 들어, 로그인을 하지 않은 사용자가 물품 구매 페이지(예를 들어 url = '/add') 에 들어갈 수 없게 하는 것입니다.

@Slf4j
public class LoginCheckFilter implements Filter {

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

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String requestURI = httpRequest.getRequestURI();

        HttpServletResponse httpResponse = (HttpServletResponse) response;

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

            if (isLoginCheckPath(requestURI)) {
                log.info("인증 체크 로직 실행 {}", requestURI);
                HttpSession session = httpRequest.getSession(false);
                if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {

                    log.info("미인증 사용자 요청 {}", requestURI);
                    //로그인으로 redirect
                    httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
                    return;
                }
            }

            chain.doFilter(request, response);
        } catch (Exception e) {
            throw e; //예외 로깅 가능 하지만, 톰캣까지 예외를 보내주어야 함
        } finally {
            log.info("인증 체크 필터 종료 {} ", requestURI);
        }

    }

    /**
     * 화이트 리스트의 경우 인증 체크X
     */
    private boolean isLoginCheckPath(String requestURI) {
        return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
    }
}

 

 

private static final String[] whitelist = {"/", "/members/add", ... };

모든 곳에 로그인이 되어있어야 하는 건 아니겠죠. 정적 리소스와 로그인, 로그아웃의 경우 로그인을 하지 않아도 접근이 가능해야 합니다. 이 화이트리스트를 이용해서 이러한 경로 검사를 해줍니다.

 

 

isLoginCheckPath(String requestURI)

매개변수로 전달받은 requestURI가 화이트리스트와 일치하는지 검사합니다. 

이때 PatternMatchUtils 라는 정적 헬퍼 클래스를 이용하여 쉽게 경로 검사가 가능합니다.

 

 

httpResponse.sendRedirect("/login?redirectURL="+requestURI);

로그인을 안했는데 로그인이 필요한 페이지에 접근시 로그인페이지로 이동시킵니다.

 

그런데 redirectURL 이라고 쿼리 스트링을 작성하였습니다. 

이렇게 작성된 이유는 예를 들어 사용자가 로그인이 필요한 페이지에 접속을 하려는 상황에서 로그인이 되어있지 않아 로그인 페이지로 이동했다고 가정해보겠습니다.

그리고 사용자가 로그인을 완료하면 메인 페이지나 기본 페이지로 이동할 수 있겠지만, 사용자가 원래 접근하려는 페이지로 다시 이동시켜주면 사용자 입장에서는 편리함을 느낄 것이기 때문에 기존에 들어가려했던 페이지로 이동을 시켜줍니다.

 

아래 사진을 통해 위 상황을 좀더 쉽게 보겠습니다.

우선 기본 페이지에 접속하였습니다. 

현재 로그인이 되지 않은 상황입니다. ( null )

 

그리고 로그인이 필요한 /items/add 페이지에 접속하기 위해 /items/add 경로를 입력하고 페이지 접속을 시도합니다.

 

하지만 /items/add 페이지는 로그인이 되어있어야 접근이 가능하기 때문에

내부적으로 필터를 통해 걸러져서 아래와 같은 주소로 리다이렉트 됩니다.

 

그리고 로그인을 완료한다면 아래와 같은 URL 주소로 바로 이동할 수 있게됩니다.

사용자 입장에서는 기존에 접속하려던 페이지로 로그인만 하면 알아서 보내주니까 더 편리함을 느낄 수 있을것입니다.

 

 

 

WebConfig 설정

 

필터를 만들기만 한다고 자동으로 등록되지 않기 때문에 Configuration을 만들어 필터를 등록해주겠습니다.

필터 등록을 위해 FilterRegistrationBean 을 사용합니다.

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Bean
    public FilterRegistrationBean logFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new LogFilter());
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.addUrlPatterns("/*");

        return filterRegistrationBean;
    }

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

        return filterRegistrationBean;
    }
}

 

setFilter(new LogFilter());

등록 할 필터를 지정합니다.

 

 

setOrder(1)

필터는 체인으로 동작하기에 순서가 필요합니다. 순서가 낮을수록 먼저 동작합니다.

위 코드에서는 LogFilter 가 먼저 실행되고 그 이후에 doFilter() 를 통해 다음 필터인 LoginCheckFilter 가 실행됩니다.

 

 

addUrlPatterns("/*")

필터를 적용할 URL 패턴을 지정하며, 하나 이상의 패턴을 지정할 수도 있습니다.

 

 

정리하자면, 서블릿에서 제공하는 필터 기능을 사용해서 로그인을 하지 않은 유저의 접근 권한을 제한시킬 수 있고, 로그인 유효성 검증이라는 공통 관심사를 묶어 중복되는 코드들을 하나로 모아 관리하기에도 편하고 가독성도 높은 간결한 코드가 완성되었습니다.

 

만약 로그인 정책이 변경되거나 새로운 whitelist 가 추가되어도 이제 필터부분만 수정하면 됩니다.


 

인터셉터(Interceptor)

 

인터셉터(Interceptor)J2EE 표준 스펙인 필터(Filter)와 달리 Spring이 제공하는 기술로, 디스패처 서블릿(Distpatcher Servlet)이 컨트롤러를 호출하기 전과 후에 요청과 응답을 참조하거나 가공할 수 있는 기능을 제공합니다.

 

즉, 웹 컨테이너(서블릿 컨테이너)에서 동작하는 필터와 달리 인터셉터는 스프링 컨텍스트에서 동작을 합니다.

 

스프링 인터셉터는 스프링 MVC가 제공하는 기술로 서블릿 필터와 스프링 인터셉터 둘 다 공통 관심사항을 처리한다는 공통점을 가지고 있지만 적용되는 순서와 범위, 그리고 사용방법이 다릅니다.

 

디스패처 서블릿은 핸들러 매핑을 통해 적절한 컨트롤러를 찾도록 요청하는데, 그 결과로 실행 체인(HandlerExecutionChain) 을 돌려줍니다.

 

그래서 이 실행 체인은 1개 이상의 인터셉터가 등록되어 있다면 순차적으로 인터셉터들을 거쳐 컨트롤러가 실행되도록 하고, 인터셉터가 없다면 바로 컨트롤러를 실행합니다.

 

인터셉터는 스프링 컨테이너 내에서 동작하므로 필터를 거쳐 프론트 컨트롤러인 디스패처 서블릿이 요청을 받은 이후에 동작하게 되는데, 이러한 호출 순서를 그림으로 표현하면 아래와 같습니다.

[인터셉터의 흐름] HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러

 

 

스프링 인터셉터의 호출 흐름을 좀 더 자세히 들어가보면 서블릿 필터는 단순히 서블릿 호출 전 doFilter 하나만 호출되어 사용되는데, 스프링 인터셉터는 컨트롤러의 호출 전, 호출 후, 요청 완료 이후 3가지나 세분화되어 호출 됩니다.

 

인터셉터(Interceptor)의 메소드

인터셉터를 추가하기 위해서는 org.springframework.web.servlet 의 HandlerInterceptor 인터페이스를 구현(implements) 해야 하며, 이는 다음의 3가지 메소드를 가지고 있습니다.

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 { }
          
}

 

 

preHandle 메소드

preHandle 메소드는 컨트롤러가 호출되기 전에 실행됩니다.

그렇기 때문에 컨트롤러 이전에 처리해야 하는 전처리 작업이나 요청 정보를 가공하거나 추가하는 경우에 사용할 수 있습니다.

 

preHandle의 3번째 파라미터인 handler 파라미터는 핸들러 매핑이 찾아준 컨트롤러 빈에 매핑되는 HandlerMethod 라는 새로운 타입의 객체로써, @RequestMapping 이 붙은 메소드의 정보를 추상화한 객체입니다.

 

또한, preHandle의 반환 타입은 boolean인데 반환값이 true이면 다음 단계로 진행이 되지만, false 라면 작업을 중단하여 이후의 작업(다음 인터셉터 혹은 컨트롤러)은 진행되지 않습니다.

 

 

postHandle 메소드

postHandle 메소드는 컨트롤러를 호출된 후에 실행됩니다.

그렇기 때문에 컨트롤러 이후에 처리해야 하는 후처리 작업이 있을 때 사용할 수 있습니다.

 

이 메소드에는 컨트롤러가 반환하는 ModelAndView 타입의 정보가 제공되는데, 최근에는 Json 형태로 데이터를 제공하는 RestAPI 기반의 컨트롤러(@RestController)를 만들면서 자주 사용되지는 않습니다.

 

또한, 컨트롤러 하위 계층에서 작업을 진행하다가 중간에 예외가 발생하면 postHandle은 호출되지 않습니다.

 

 

afterCompletion 메소드

afterCompletion 메소드는 이름 그대로 모든 뷰에서 최종 결과를 생성하는 일을 포함해 모든 작업이 완료된 후에 실행된다.

요청 처리 중에 사용한 리소스를 반환할 때 사용하기에 적합합니다.

postHandler 메소드와 달리 컨트롤러 하위 계층에서 작업을 진행하다가 중간에 예외가 발생하더라도 afterCompletion 메소드는 반드시 호출 됩니다.

 

 

방금 위에서 설명 했다시피, 인터셉터 혹은 컨트롤러에서 예외가 발생한다면 각 시점에 따라 호출 여부가 달라집니다.

컨트롤러에서 예외가 발생한다면 postHandler는 호출되지 않습니다.

afterCompletion은 항상 호출됩니다. 그렇기 때문에 이전에 발생한 예외가 있을 경우 이를 파라미터로 받아서 어떤 예외가 발생했는지 확인할 수 있습니다.

 

예외가 발생하면 postHandler() 같은 경우 호출되지 않기 때문에 예외 처리가 필요하다면 afterCompletion()을 사용해야 합니다. afterCompletion은 Exception ex 를 매개변수로 받고 있으며 Nullable 하기 때문에 notNull인 경우 해당 처리를 수행하면 됩니다.

 

 

(인터셉터 활용) API 요청 로그 필터 구현해보기

인터셉터 구현은 HandlerInterceptor 를 구현하면 됩니다.

필터 활용하여 구현했던 API 로그를 남기는 스프링 인터셉터를 구현해보겠습니다.

@Slf4j
public class LogInterceptor implements HandlerInterceptor {

    public static final String LOG_ID = "logId";

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

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

        request.setAttribute(LOG_ID, uuid);

        //@RequestMapping: HandlerMethod
        //정적 리소스: ResourceHttpRequestHandler
        if (handler instanceof HandlerMethod) {
            HandlerMethod hm = (HandlerMethod) handler;//호출할 컨트롤러 메서드의 모든 정보가 포함되어 있다.
        }

        log.info("REQUEST [{}][{}][{}]", uuid, requestURI, 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 requestURI = request.getRequestURI();
        String logId = (String) request.getAttribute(LOG_ID);
        log.info("RESPONSE [{}][{}][{}]", logId, requestURI, handler);
        if (ex != null) {
            log.error("afterCompletion error!!", ex);
        }

    }
}

 

request.setAttribute(LOG_ID, uuid)

서블릿 필터와는 다르게 스프링 인터셉터는 호출 시점이 분리되어 있기에 각각의 메서드가 호출되는 시점에 변수들의 값 유지가 되지 않습니다.

 

그렇기 때문에 preHandler에서 지정한 값을 postHandler 이나 afterCompletion 에서 사용하려면 어딘가에 담아두어야 하는데, 이 인터셉터는 싱글톤처럼 사용되기에 멤버변수를 사용하면 안됩니다. 

그렇기에 값을 request 인스턴스에 담아두었습니다.

 

위 코드에서 request에 담은 LOG_ID(uuid)는 afterCompletion에서 getAttribute로 찾아서 사용합니다.

 

 

HandlerMethod hm = (HandlerMethod) handler;

스프링에서는 일반적으로 @Controller, @RequestMapping 을 활용해 핸들러 매핑을 사용하는데, 이 경우 스프링 인터셉터의 Object handler 매개변수에는 핸들러 정보로 HandlerMethod 가 넘어옵니다

 

 

생성한 스프링 인터셉터(LogInterceptor)를 설정(Config)에 등록해보도록 하겠습니다.

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/css/**", "/*.ico", "/error");
 	}
}

 

WebMvcConfigurer 인터페이스를 구현하여 addInterceptor 메서드를 재정의해서 인터셉터 등록이 가능합니다.

 

위에서 필터를 등록할 때와 같은 Config 파일인데, 필터를 등록할 때에 WebConfig 파일이 WebMvcConfigurer 를 구현하고 있었는데, 만약 인터셉터없이 필터만 등록하겠다고 하면, WebMvcConfigurer 인터페이스를 구현할 필요가 없습니다.

 

인터셉터를 등록하기 위해 메소드를 오버라이딩 하기 위해 필요하며, 아래에서 살펴볼 ArgumentResolver 를 활용하고 등록하기 위해(메소드 오버라이딩) 구현한 것입니다.

 

addInterceptor : 인터셉터를 등록한다.

order(1) : 인터셉터의 호출 순서를 지정하며 낮을 수록 먼저 호출됩니다.

addPathPatterns("/**") : 인터셉터를 적용할 URL 패턴을 지정합니다.

excludePathPatterns("/css/**", "/*.ico", "/error") : 인터셉터에서 제외할 패턴을 지정합니다.

 

위 코드에서 허용 경로, 비허용 경로로 /**, /css/**, /*.ico 등 다양한 경로를 지정했습니다.

간단하게 다음과 같은 포맷을 제공하니 참고하여 원하는 경로를 설정해주면 되겠습니다.

? 한 문자 일치
* 경로(/) 안에서 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 에 대해 더 자세한 사항은 아래 공식 사이트를 참고해주세요.

 

PathPattern (Spring Framework 6.1.10 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

 


 

ArgumentResolver 활용

 

이전에 포스팅에서 ArgumentResolver 에 대해 간단하게 알아본적이 있습니다.

 

[Spring] HTTP 메시지 컨버터 이해쉽게 개념 정리

스프링을 공부하다 보면 @RequestBody 어노테이션을 사용하여 JSON에서 자바객체로 변환하여 반환해 주고, 또 자바객체에서 JSON으로 간편하게 변환하는 것을 알고 있을 겁니다. 하지만 이게 어떻게

developshrimp.com

 

 

여기서는 ArgumentResolver 를 활용해보는 시간을 가져보겠습니다.

 

 

클라이언트로부터 받은 Request 정보에서 Session 정보를 꺼내 해당하는 세션키로 로그인 정보를 찾는 방법을 다양하게 사용해보았습니다. 기존에는 직접 HttpServletRequest 객체에서 꺼내는 방식과 애노테이션을 활용했었습니다.

 

 

HttpServletRequest에서 직접 꺼내기

@GetMapping("/")
public String homeLoginV3(HttpServletRequest request, Model model) {
    HttpSession session = request.getSession(false);
    //...
    Member loginMember = (Member) session.getAttribute(SessionConst.LOGIN_MEMBER);
    //...
}

 

 

@SessionAttribute 애노테이션 활용하기

public String homeLoginV3Spring(
        @SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginMember,
        HttpServletReqeust request, Model model) {
    
    if (loginMember == null) {
        return "home";
    }
    
    model.addAttribute("member", loginMember);
    return "loginHome";
}

 

 

위 코드중에서 애노테이션 기반의 매핑방식도 직접 로직을 구현하는 것보다 훨씬 간결한것을 확인할 수 있다.

하지만, 여기도 매번 속성들(name, required)를 작성해주는건 번거롭고 해당 애노테이션을 통해 해당 객체에 대한 명시성도 부족하다.

 

지금 매핑하는 객체는 로그인한 사용자의 정보(Member)인데 이를 @Login 이라는 애노테이션을 직접 만들어 매핑해주는 로직을 구현하면 사용자 입장에서 사용하기도 편하고 가독성도 높아질 것입니다.

 

우리가 ArgumentResolver 를 사용해서 만들 애노테이션 기능은 자동으로 세션에 있는 로그인 회원을 찾아주되 만약 세션에 없을 경우 Null 을 반환하도록 만들어 볼 것입니다.

 

 

@Login 애노테이션 구현하기

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {

}

 

@Target(ElementType.PARAMETER) : 파라미터에만 붙힐 수 있는 애노테이션이라는 뜻입니다.

@Retention(RetentionPolicy.RUNTIME) : 리플렉션 등을 활용할 수 있도록 런타임까지 애노테이션 정보가 남아있도록 해줍니다.

 

 

LoginMemberArgumentResolver 구현

ArgumentResolver 는 HandlerMethodArgumentResolver 를 구현하면 됩니다.

@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        log.info("supportsParameter 실행");

        boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
        boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());

        return hasLoginAnnotation && hasMemberType;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

        log.info("resolveArgument 실행");

        HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
        HttpSession session = request.getSession(false);
        if (session == null) {
            return null;
        }

        return session.getAttribute(SessionConst.LOGIN_MEMBER);
    }
}

 

supportsParameter()

컨트롤러 호출 시 각 매개변수들은 ArgumentResolver 에 의해 매핑이 되는데, 많은 ArgumentResolver 가 각각 대응할 수 있는 객체는 제한되어 있을 것인데, 이를 책임사슬 패턴을 이용해 처리하는데, 각각의 ArgumentResolver 는 이 메서드(supportsParameter())를 이용해 매핑가능 여부를 Boolean 타입으로 반환합니다.

위 코드에서는 @Login 애노테이션이 붙어있고 Member 객체인 경우 지원이 가능하다고 로직을 구현하였습니다.

 

resolverArgument()

실제로 컨트롤러에 필요한 파라미터 정보를 생성해주는 메소드로, 여기서는 세션에서 로그인 회원 정보인 member 객체를 찾아 반환해줍니다.

 

필터나 인터셉터와 동일하게 WebMvcConfigurer에 리졸버를 등록해주어야 합니다.

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new LoginMemberArgumentResolver());
    }
    
    //...
}

 

 


 

참고(출처)

https://mangkyu.tistory.com/173

https://catsbi.oopy.io/9ed2ec2b-b8f3-43f7-99fa-32f69f059171#a67f4dd5-2efe-46ff-9bf6-22c8aaab1031

[인프런] 김영한님의 스프링 MVC 2편 강좌