개발자로 후회없는 삶 살기

spring PART.필터 본문

[백엔드]/[spring | 학습기록]

spring PART.필터

몽이장쥰 2023. 4. 24. 12:52

서론

※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.

https://www.inflearn.com/roadmaps/373

 

우아한형제들 최연소 기술이사 김영한의 스프링 완전 정복 - 인프런 | 로드맵

Spring, MVC 스킬을 학습할 수 있는 개발 · 프로그래밍 로드맵을 인프런에서 만나보세요.

www.inflearn.com

 

본론

 

- 서블릿 필터

요구사항을 보면 로그인된 사용자만 상품 목록을 볼 수 있게 해야합니다. 근데 지금은 url로 맵핑 요청을 직접하여 접근할 수 있습니다.

 

방법이 있긴 있습니다. 상품 관리 컨트롤러에서 등록, 수정, 삭제, 조회 등등의 처리를 할 때 로그인 여부를 다 체크하면 됩니다. 홈 컨트롤러에서 한 것처럼 세션을 봐서 @SessionAttribute를 모든 GET 맵핑 메서드마다 다 로그인 안 했으면 return login 뷰하면 됩니다. 그러면 개발자 입장에서 똑같은 기능이 반복되니 너무 번거롭습니다.

 

이렇게 여러 로직에서 공통으로 관심이 있는 것을 공통 관심사라고 하고 여기서는 상품 등록, 수정, 삭제, 조회 등에서 지금 사용자가 인증이 된 사용자인지 인증에 대해 공통으로 관심을 가지고 있습니다. 이러한 공통 관심사는 AOP로도 해결할 수 있지만 웹과 관련된 것은 서블릿 필터와 스프링 인터셉터를 사용하고 이런 것들이 웹과 관련된 HttpServletRequest 관련 공통 관심사를 처리해줍니다. 이 둘은 특정 url이 오는 건 다 막는다는 등 웹 관련 부가 사항을 다 제공합니다. 이로써 로그인 여부 체크하는 로직을 한방에 다 해결할 수 있습니다.

 

-> 소개

서플렛이 지원하는 ★ 수문장 ★입니다. 

 

1. 필터의 흐름

필터의 흐름은 http 요청이 오고 > was가 필터를 호출하고 서블릿을 호출합니다. 원래는 was가 req 메세지를 받아서 서블릿을 호출하는 줄 알았는데 필터가 먼저 호출됩니다. 그래서 모든 고객의 요청 로그를 남기는 요구사항 ("모든 고객의 요청 로그를 다 남겨 주세요")이 오면 필터를 하나 넣으면 됩니다. 필터는 호출 url을 적용할 수 있어서 "/*"라고 하면 모든 url 요청을 이 필터가 다 받을 수 있습니다. 여기서 서블릿은 디스패쳐 서블릿이다. (MVC의 시작이 디스패쳐 서블릿이고 모든 요청이 다 디스패쳐 서블릿으로 가기 때문입니다.)

 

2. 필터 제한

제한이 가능합니다. 로그인 한 사용자만 들어오게 하라는 것이 가능합니다. 로그인한 사용자는 was > 필터 > 서블릿 > 컨트이지만 로그인 안 한 사용자는 필터에서 딱 걸려서 서블릿을 호출하지 않습니다. 필터는 적절하지 않은 요청이라고 판단되면 거기에서 끝낼 수 있어서 로그인 여부를 체크하기 딱 좋습니다.

 

3. 필터 체인

필터를 여러개 꽂아서 로그를 남기는 필터, 로그인 여부 체크 필터를 순서대로 적용할 수 있습니다.

 

4. 인터페이스

필터도 자바 코드입니다. doFilter가 중요한데 was에서 doFilter를 호출하고 doFilter를 통과해서 체인에 연결된 모든 필터의 doFilter를 호출하고 더 이상 필터가 없으면 서블릿이 호출됩니다. 인터를 구현하고 등록하면 서블릿 컨테가 필터를 싱글톤 컨테이너로 보고 관리합니다.

1) init() : 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출됩니다.
2) doFilter() : 고객의 요청이 올 때마다 이 메서드가 호출되고 필터의 로직을 구현하면 됩니다.
3) destroy() : 필터 종료 메서드로 서블릿 컨테이너 종료될 때 호출됩니다.

 

이렇게 구현하고 등록만 해 놓으면 알아서 스프링이 호출해 주는 것이 프레임워크입니다.

 

- 요청 로그

public class LogFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        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 init(FilterConfig filterConfig) throws ServletException {
        log.info("init filter");
    }

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

모든 고객의 요청 로그를 남기는 필터를 구현해보자 필터 패키지와 새로운 클래스를 만듭니다. Filter를 구현합니다. 3가지 메서드를 구현해야합니다. 초기화와 파괴는 그냥 log로 컨테이너가 뜨고 떨어질 때 호출이 되는지만 볼 것입니다.

> doFilter에 로직을 넣자 일단 필터의 흐름과 어떻게 호출이 되는지 보게 위해 log를 찍어보겠습니다. http 요청이 올 때마다 doF가 호출이 된다고 했습니다. 보면 req, resp, chain이 있습니다. ServletRequest는 기능이 없는 부모라서 자식 HttpRequest로 다운 캐스팅하고 getRequestURI로 모든 사용자의 uri, 요청을 구분하기 위해 uuid를 찍습니다.

예외문이 필요한데 여기에 try에 로그를 남기고 반드시 필요한 게 있습니다. 다음 필터의 doF를 호출해야하고 다음 필터가 없으면 서블릿이 호출됩니다. chain.doFilter에 req, resp를 그대로 넘겨주면 됩니다. 따라서 다음 필터의 doF도 호출이 되고 필터가 다 끝나기 전까지는 finally가 호출이 안됩니다. 예외가 발생하면 예외를 던지고 모든 필터를 다 호출하고 finally가 호출됩니다.

 

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

        return filterFilterRegistrationBean;
    }
}

필터를 사용하기 위해서는 등록을 해야합니다. WebConf를 만들어서 직접 환경설정(@Configuration)을 할 것입니다. 스프링 부트로 FilterRegistrationBean을 사용하면 필터를 등록할 수 있습니다. config는 메서드 이름으로 빈이 등록되며 return 값을 등록하는데 FilterRegistrationBean을 반환하면서 set으로 만든 필터를 넣어주면 됩니다. 필터는 체인이 여러개이니 순서를 정해줘야하고 url 패턴도 적어줍니다. 이렇게 /* 하면 모든 url에 등록할 필터가 적용이 됩니다. chain.doFilter를 작성하지 않으면 아예 다음으로 진행이 되지 않아서 컨트롤러도 호출되지 않습니다.

 

-> 실행

스프링이 뜨면서 서블릿 컨테이너가 뜨고 아무것도 하지 않아도 init이 호출됩니다. 

 

그리고 그 어떤 요청인 조회, 로그인, 로그아웃이 호출되든 간에 무조건 log가 찍힙니다.

로그인 할 때 빈 검증오류를 터뜨려보면 try와 finally 사이에 오류 메세지가 뜹니다. 즉 http 요청이 오면 try에서 chain으로 모든 필터를 다 거치고 서블릿 거치고 컨트롤러까지 거치고 finally가 호출되는 필터 흐름을 보입니다.

 

-> 정리

필터를 사용하려면 필터를 구현하면되고 Http 요청이 오면 필터의 doF가 호출이 됩니다. 필터는 등록을 해야하는데 스프링 부트가 알아서 서블릿 컨테이너 올릴 때 필터를 같이 등록해줍니다. URL 패턴은 서블릿을 호출할 때와 같은 개념입니다.

 

- 로그인 된 사용자만 허용(인증 체크 필터)

public class LoginCheckFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        String requestURI = httpRequest.getRequestURI();
    }
}

로그인이 되지 않은 사용자는 그 어떤 페이지도 접근하지 못하게 해야합니다. 새로운 필터 구현체를 만듭니다. 이름은 로그인 체크 필터이다. 3가지를 다 구현할 필요는 없습니다. 인터페이스면 무조건 구현을 해야하는데 init, destroy는 defualt 키워드가 붙어서 안 해도 됩니다. doF 메서드를 구현합니다. 똑같이 HttpservletRequest로 캐스팅하고 URI를 구합니다. 얘는 response도 필요해서 쓸 수 있도록 캐스팅합니다.

 

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

이제 로직을 작성합니다. 필터에서 세션을 봐서 로그인한 사용자인지 보면 되는 간단한 알고리즘입니다. try에서 체크 로직을 만들 것입니다. 화이트 리스트를 만들 것인데 경로 중에 로그인 안 해도 들어올 수 있는 목록입니다. 로그인 로그아웃, 루트, 회원가입, css는 로그인 안 됐다고 접근하지 못하면 안 돼서 이렇게 합니다.

 

private boolean isLoginCheckPath(String requestURI) {
    return !PatternMatchUtils.simpleMatch(whiteList, requestURI);
}

인증 체크 X 메서드를 만듭니다. URI를 받아서 패턴 매치 utils로 화이트 리스트와 URI가 매칭 되면 T, 아니면 F입니다. 화이트 리스트에 없는 것은 T가 되면서 인증 체크를 할 것입니다.

 

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);
            httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
            return;
        }
    }
    chain.doFilter(request, response);
}
catch (Exception e) {
    throw e;
}
finally {
    log.info("인증 필터 체크 종료 {}", requestURI);
}

try에서 지금 요청 들어온 URI가 화이트 리스트인지 물어서 아니면 인증 체크 로직을 실행합니다. 인증 체크 로직은 세션을 일단 가져옵니다. 만약 없거나 세션에서 getAttribute (get이 조회시 데이터 가져오는 것, set이 로그인 시 세션 생성하고 헤더에 담는 것)로 데이터를 가져왔는데 없으면 미인증 사용자 요청이 들어온 것으로 로그인 페이지로 쫓아냅니다.(세션이 없는 건 로그인 안 한 것이고 데이터가 없는 건 로그인은 했는데 너무 오래된 것이었습니다. 쿠키에서도 mycookie=1 이렇게 id를 주고 받으니 id가 없으면 로그인을 안 한 것이었고 데이터는 findById로 직접 찾아야했는데 세션도 세션이 없으면 로그인 안 한 것이고 getAttribute로 데이터가 없는 것을 알 수 있습니다.)

 

> httpResponse.sendRedirect하고 /login?redirectURL 하면 로그인이 안 되서 지금 쫓아내지더라도 로그인을 하고 오면 다시 쫓아낸 페이지로(요청한 requestURL) 바로 올 수 있게 할 수 있습니다. 안 그러면 로그인하고 페이지를 하나 하나 다시 찾아와야합니다. return을 해야 리다이렉트가 됩니다.

+ 만약 체크 인증 안해도 되는 로그인한 사용자면 다음 필터로 doFilter합니다. (실제로 doFilter를 안 쓰면 컨트롤러로 안 넘어가서 다음 실행이 안됩니다.) catch에서는 예외를 로깅해도 되지만 톰캣까지 예외를 보내야 합니다. (원래 catch에서 예외를 sout 하지만 throw 하니 로깅을 안하고 톰켓에게 throw하는 개념) finally에 체크 인증 종료를 합니다.

 

-> 필터 등록

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

    return filterFilterRegistrationBean;
}

setFilter와 순서만 바꾸면 됩니다. Url패턴으로 등록한 곳에서 화이트 리스트를 거를 수 있겠지만 유지보수를 위해서 등록 url에서는 /*를 쓰고 필터 구현할 때 화이트 리스트를 만듭니다.

 

-> 실행

이제 url로 직접 요청하면 상품 관리 페이지를 못 들어옵니다. 또한 로그를 보면 1순위인 로그 필터가 되고 2순위인 인증 체크 필터가 호출이 됐습니다.

 

화이트 리스트에 포함되는 목록은 인증 체크 필터 시작과 종료만 되는데 로그인 안 한채로 상품 목록에 들어가려고 하면 미인증 사용자 요청이라고 보여주며 로그인 화면으로 리다이렉트 됩니다.

 

필터는 콘솔 로그에 경로가 다 나옵니다. > 근데 로그인하면 홈 화면으로 옵니다. 우리는 쫓아내진 페이지로 가고 싶은데 말입니다. ?redirectURL을 받아서 다시 돌아가는 로직을 추가해야합니다.

 

-> 로그인 컨트롤러 수정

@PostMapping("/login")
public String login4(@Validated @ModelAttribute LoginForm form, BindingResult bindingResult,
                     @RequestParam(defaultValue = "/") String redirectURL, // 여기
                     HttpServletRequest request) {
                     
                     

// 이전
return "redirect:/";

// 이후
return "redirect:" + redirectURL;

sendRedirect해서 받은 ?뒤에 경로를 가지고 로직을 작성해야합니다. requestParam에 defualtValue를 /로 줘서 정상적으로 로그인을 하려고 온 것이면 /를 줘서 원래 로그인 맵핑 메서드처럼 동작하게 하고 실제 로그인 안 한 거였으면 로그인 후에는 다시 쫓아내진 페이지로 돌아가게 합니다.

 

이제 /items를 로그인하지 않은 상태에서 가면 로그인으로 튕기지만 다시 로그인하면 홈 화면으로 가지 않고 /items로 바로 잘 갑니다. 만약 상품 관련 컨트롤러에서 @SessionAttribute로 다 만들었으면 복잡했을텐데 필터에서 인증 체크를 하도록 단일 책임 원칙을 잘 지킨 것입니다.

 

- 스프링 인터셉터

서블릿 필터와 같이 웹과 관련된 공통 관심 사항을 처리하는 기술입니다. 적용되는 순서와 범위, 사용 범위가 다르고 스프링 인터셉터가 더 편리하고 훨씬 좋습니다.

 

-> 흐름

http 요청 > was > 서블릿 필터 > 서블릿 > 스프링 인터셉터 > 컨트롤러로 컨트롤러 호출 직전에 호출됩니다.

 

-> 제한

역시나 비 로그인 사용자는 서블릿 이후 스프링 인터셉터에서 막 히고 컨트롤러를 호출하지 않습니다.

 

-> 체인

인터셉터 1, 2, 3을 묶을 수 있습니다. 역시나 로그 인터셉터와 로그인 여부 인터셉터를 만들 수 있습니다.

 

- 인터페이스

얘도 인터셉터를 구현해야 하며 HandlerInterceptor를 구현하면 됩니다. 3가지 메서드가 제공되며 컨트롤러 호출 전에 pre가 호출되고 컨트롤러 호출 후에 post, 완전 호출 후 after를 호출합니다.

서블릿 필터와 다르게 3가지를 제공하기에 더욱 세분화되어 개발할 수 있습니다. 서블릿 필터의 경우 req, resp만 제공했지만 얘는 handler로 어떤 핸들러(컨트롤러)가 호출되는지 호출 정보도 받을 수 있고, 어떤 모델 엔 뷰가 반환되는지 등 제공하는게 많습니다.

 

-> 그림

http 요청이 오면 was > 필터 > 서블릿으로 옵니다. 서블릿의 어댑터가 핸들러를 호출했었습니다. 이때 서블릿은 먼저 인터셉터의 프리를 호출합니다. 그 다음 핸들러 어댑터로 핸들러를 호출합니다. 얘가 모델뷰를 반환하는데 (옛날엔 무조건 모델 뷰를 반환했는데 @기반은 인터페이스를 구현한 게 아니라서 유연하게 String을 반환합니다.) 포스트를 호출하면서 모델 뷰를 넣어줍니다. 그리고 render로 뷰를 랜더하고 마지막에 에프터가 호출됩니다.

 

+ 더 자세히

1) 프리 : 응답값이 true면 다음으로 진행하고 f면 더 진행하지 않고 핸들러 어댑터도 호출하지 않습니다. (마치 필터와 같습니다.)
2) 포스트 : 컨트롤러 호출 후에 호출됩니다.
3) 에프터 : 뷰가 렌더링 된 이후에 호출됩니다.

 

-> 예외사항

포스트와 에프터의 차이를 보자 프리를 호출하고 컨트롤러를 호출했는데 예외가 터져서 서블릿으로 예외가 돌아옵니다. 예외가 오면 포스트가 호출이 안됩니다. 컨트롤러에서 예외가 발생하면 포스트는 호출되지 않습니다. 에프터는 예외가 터지든 말든 항상 호출됩니다. 이 경우 예외를 파라미터로 받아서 어떤 예외인지 로그로 출력할 수도 있습니다. 정상 흐름에서는 예외가 null입니다.

-> 정리

인터셉터는 스프링 MVC에 특화된 필터 기능을 제공한다고 이해하면 됩니다. 스프링을 쓰면 웬만하면 인터셉터를 사용합니다.

 

- 인터셉터 요청 로그

서블릿 필터로 만든 요청 로그 인터셉터로 만들어보겠습니다.

 

-> 구현

인터셉터 패키지와 클래스를 만드고 Handler 인터셉터 인터페이스를 구현합니다. 

 

1. 프리핸들

@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;
}

얘는 필터와 다르게 HttpServlertRequest가 들어옵니다. 필터에서 한 것처럼 URI와 uuid를 만듭니다. 포스트는 예외 상황에서 안 찍힌다고 했으니 종료 로그를 에프터에서 찍어야하니 request.set에 담고 get으로 넘깁니다. request는 하나의 사용자에서 동일한 request가 사용되는게 보장이 되는데 프리가 호출되고 랜더 끝나고 에프터가 호출이 되니 에프터까지 request에 넣어놓은 set 값이 들어있을 것입니다.

> 이렇게 해야 프리와 에프터에서 uuid를 같이 쓸 수 있습니다. 필터는 doFilter에서 try, catch, finally를 해서 갔다 왔다 이런 로그를 찍었는데 인터셉터는 세분화되어 이런게 용이합니다.

 

그 다음에 프리의 로그를 찍는데 프리 다음에 어떤 컨트롤러가 호출되는지 핸들러를 찍을 수 있습니다. 근데 디스패쳐 서블릿 처음 배울 때 처음에 v3 핸들러만 가능하다가 디스패쳐 서블릿을 사용하면서 Object로 바꾸고 각 다른 버전의 컨트롤러를 호출할 수 있는 어댑터를 넣어서 굉장히 유연해 졌었습니다. 그래서 여기서도 핸들러 어댑터가 대신 호출해주기 때문에 Object handelr에 아무거나 다 들어갈 수 있습니다.

참고로 @RequestMapping하는 경우 핸들러가 HandlerMethod가 사용이 됩니다. 여기에 호출할 메서드의 모든 정보가 포함되어있습니다. 정적 리소스를 사용하는경우에는 ResourceHttpRequestHandler가 사용이 되는데 그냥 우리가 일반적으로 사용하는 컨트롤러는 HandlerMethod 타입이 맞는지 확인하고 사용하면 되는 구나 정도로 이해하면 됩니다. ★ 지금 로그를 찍는데 uuid, requestURI 외에 프리 이후에 호출할 컨트롤러도 찍을 수 있다는 것을 보고 있습니다.

 

> 프리에서는 핸들러를 찍을 수 있고 포스트에서는 모델뷰를 찍을 수 있고 에프터에서는 예외도 찍을 수 있구나를 보고자 하는 것이고 이것이 스프링 MVC에 특화되어있다는 것입니다. > 그 다음에 return을 true로 하면 핸들러 어댑터가 호출이 되고  f로 하면 핸들러 어댑터가 호출되지 않고 여기서 끝냅니다. 바로 return true부터 적고 시작합니다.

 

2. 포스트핸들

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    log.info("Request [{}]", modelAndView);
}

모델 뷰를 찍습니다.

 

3. 애프터

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    String requestURI = request.getRequestURI();
    String uuid = (String) request.getAttribute(LOG_ID);

    log.info("Request [{}][{}][{}]", uuid, requestURI, handler);
    if(ex != null) {
        log.error("afterCompletion error!! {}", ex);
    }
}

에프터는 완전히 끝날 때 호출이 되는데 핸들러도 있고 예외도 있습니다. if로 만약 예외가 있으면 에러를 찍습니다. response 로그는 종료 로그로 포스트가 아니라 에프터에 쓴 이유는 에프터가 예외가 발생하더라도 호출되는 것이 보장되기 때문입니다.

 

-> 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .order(1)
                .addPathPatterns("/**")
                .excludePathPatterns("/css/**", "/*.ico", "/error");
    }

얘도 webconf에서 직접 등록하는데 webMVcConfigurer를 구현해야하고 빈 등록이 아니라 재정의를 해야합니다. addInterceptor를 아까 만든 인터셉터를 더하고 순서와 url 패턴을 넣는데 /**로 해야합니다. 그리고 인터셉터를 하지 않은 경로도 넣을 수 있습니다. 얘들은 로그를 남기지 않을 것이라는 것으로 exclude에 있는 경로로 들어오면 지금 등록하는 인터셉터가 호출이 안 됩니다.

 

-> 실행

해보면 필터가 인터셉터보다 먼저 호출이 된다고 했으니 로그에 그렇게 남습니다.

 

> 로그를 보면 프리핸들에 작성한 대로 request uuid URI("/") 핸들러(HomeController)가 찍힙니다.

 

핸들러는 호출되는 맵핑 메서드의 파라미터까지 다 나오므로 get으로 꺼내서 다 사용할 수 있습니다. 

 

> 포스트 핸들에서는 모델 엔 뷰를 찍으니 모델엔뷰에 있는 2가지인 뷰 이름과 모델에 담긴 것까지 다 보여줍니다. 

> 에프터에서는 response에서 uuid가 하나의 request니 똑같고 uri, 핸들러 잘 찍힙니다. 보면 필터와 비교했을 때 정말 다양하고 세분화된 로그를 찍을 수 있습니다.

 

-> 정리

필터에서는 doF로 만하니 try에서 request uuid 찍고 finally에서 종료 로그로 response에서 uuid를 찍는게 되는데 인터셉터는 3개의 메서드를 쓰니 uuid를 공유하기 어려웠고 그래서 HttpRequest에 담아서 보냈습니다. 필터는 그냥 서블릿보다 앞에 호출되는 수문장으로 모든 요청이 다 들어오는 특성을 이용해서 로그를 찍었고 화이트 리스트를 만들어서 화이트에 속하지 않는 요청이 오면 지금 들어오는 HTTP 요청의 세션이 있는지 없는지 봐서 리다이렉트를 하게 했습니다.

 

> 이 로직을 전부 다 doF에 작성해서 했습니다. 인터셉터는 동일하게 컨트롤러가 호출되기 직전에 모든 http 요청이 들어오는 수문장으로 3개의 메서드가 호출이 되는데 각각 호출되는 시점이 다르고 파라미터도 달라서 3 메서드 모두 다 로그를 찍어서 각 호출시점마다 어떤 값을 보여주는지 로그를 봤습니다. 이제 인터셉터로 인증체크를 하면 또 3개의 메서드의 실행 시점을 고려하여 각 파트에 맞는 구현을 하면 될 것입니다. (제 생각은 그냥 프리에서 막아야 하지 않나 싶은데 정답이었습니다.)

 

- 인터셉터 인증 체크

@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(false);

        if(session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
            log.info("미인증 사용자 요청");
            response.sendRedirect("/login?redirectURL" + requestURI);
            return false;
        }

        return true;
    }
}

이번에 진짜 필터말고 인터셉터를 쓰는 참 의미를 알 수 있을 것입니다. 로그인 체크 인터셉터를 만들고 구현합니다. 3개의 메서드를 재정의할 것입니다. 로그인 체크는 프리만 구현하면 된다. 3개의 메서드 모두 defualt가 붙어있어서 프리만 재정의합니다.

> 필터에서 한 것처럼 uri를 구하고 http 세션을 얻습니다. 만약 세션이 null이거나 데이터가 없으면 미인증 사용자 요청을 찍습니다. 이 경우 로그인으로 리다이렉트 하는 것까지 필터와 똑같습니다. 여기서 return false를 해서 끝내버립니다. 근데 이상합니다. 필터에서는 화이트 리스트도 체크하고 그랬는데 로그인 체크 필터는 그런 게 필요가 없습니다. 이런 복잡한 어떤 경로는 되고 어떤 건 안 되고를 인터셉터 등록할 때 다 할 수 있습니다. 프리 핸들만 필요한데 프리핸들도 간결해집니다.

 

-> 등록

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

    registry.addInterceptor(new LoginCheckInterceptor())
            .order(2)
            .addPathPatterns("/**")
            .excludePathPatterns("/", "/members/add", "/login", "/logout","/css/*");
}

빈을 등록하듯이 여러개 재정의 할 필요없고 아까 만든 로그 인터셉터 재정의하는 것에 작성하면 되는데 registry에 add하여 인증 체크 인터셉터를 더하고 순서를 2로 하고 모든 경로의 요청을 받습니다. exclude로 화이트 리스트를 넣습니다. 생각해보면 아까 필터에서 첫 if문이 화이트리스트에 포함되지 않으면이었습니다. 근데 인터셉터는 화이트리스트를 등록할 때처리하면 그 외에 것들은 다 인증체크 인터셉터로 가서 세션 확인하고 바로 리다이렉트 해버릴 수 있습니다.

> 애초에 필터는 doF에서 request, response를 다 찍어야하니 try catch finally가 필요해서 복잡해 보이는 건데 거기에 화이트 리스트 넣는 if문이 많으니 더 복잡해진 것입니다. 필터는 내 관심사가 아니더라도 doF 하나 뿐이니 다 구현해야 하는게 별로인 것입니다.

> 이게 인터셉터의 장점입니다. 등록할 때 패턴을 정말 세밀하게 할 수 있습니다. addPath에서는 모든 경로에 적용하지만 exclude는 아예 지금 등록하는 인증체크 인터셉터가 호출 자체가 안 됩니다. 인터셉터의 프리핸들에 들어온 것은 무조건 인증 체크를 해야하는구나 하고 로직을 짜면 됩니다.


+ 필터는 무조건 모든 요청이 필터를 거치니 필터내부에서 화이트리스트를 구현해야하는 건데 인터셉터는 인터셉터 등록시에 인터셉터를 호출하지 않을 경로를 지정할 수 있으니 인터셉터가 호출이 아예 안 될 수 있는 것이고 호출되면 인증 체크만 하면 되니 간결해집니다. 특별한 문제가 없으면 인터셉터를 쓰면 됩니다.

 

-> 실행

미인증 사용자 요청은 리다이렉트로 로그인으로 잘 보냅니다.

 

- ArgumentResolver 활용

@GetMapping("/")
public String homeLoginArgumentResolver(@Login Member loginMember, Model model) {
    // 로그인 안한 사용자
    if(loginMember == null) {
        return "home";
    }

    model.addAttribute("member", loginMember);
    return "loginHome";
}

이전에 홈 컨트롤러에서 @SessionAttribute 길게 넣고 했어야했는데 간단하게 할 수 있습니다. @Login만 있으면 너는 로그인한 사용자인지 보는 세션에서 찾아서 넣어주는 과정을 @하나로 할 수 있습니다. 왜 여기서 AR이 필요하냐면 AR가 핸들러에게 파라미터 넣어주는 애였는데 AR을 직접 구현하여 개발자 마음애도 편리하게 파라미터를 넣어주겠다는 것입니다.

 

-> 애너테이션 만들기

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

@Login을 만들어야합니다. AR 패키지를 만들고 Login 애너테이션을 만듭니다. Target, Retention을 넣는다. 이렇게만 하면 MA로 봅니다. 여기서 AR을 만들어서 이 @Login의 동작방식을 바꿔줘야합니다. @Login이 있고 뒤에 Member member 이렇게 있는 조건을 만족하면 MA가 아니고 AR가 동작하도록 해야합니다.

 

-> AR 구현

@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        log.info("서포트 실행");

        boolean hasParameterAnnotation = parameter.hasParameterAnnotation(Login.class);
        boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());
        
        return hasMemberType && hasParameterAnnotation;
    }

핸들러메서드AR을 구현합니다. 서포트에서 들어오는 파라미터에 Login 어노가 있냐를 물어봅니다. 이 파라미터는 컨트롤러 호출 전에 homeLoginArgumentResolver 맵핑 메서드에 @Login 어노가 붙어있냐는 것입니다. 그리고 멤버 클래스가 들어오는지도 확인합니다. 이 두 조건이 T면 resolveArgument가 실행되고 하나라도 F면 실행이 안됩니다.

 

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
    log.info("리졸버 실행");

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

    return session.getAttribute(SessionConst.LOGIN_MEMBER);
}

이제 위 정보를 가지고 resolveArgument에서 작업을 해야합니다. (실제 SessionA처럼 동작하도록 resolveArgument에 로직을 짭니다.) 여기서 파라미터를 만들어서 반환을 해줘야합니다. httpservletRequest가 필요해서 getNativeRequest로 뽑고 세션을 꺼내고 세션이 널이면 로그인하지 않은 사용자로 @SessionAttribute때처럼 Member 파라미터에 null을 넣어버립니다. 그게 아니면 getAttribute를 해서 세션 데이터를 꺼내서 리턴합니다. resolverArgument는 컨트롤러 호출 직전에 호출되어 필요한 파라미터 정보를 생성합니다. 여기서는 세션 데이터인 member객체를 찾아서 반환해줍니다.

 

-> 등록

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

webconf에 재정의를 해야합니다.addArgumentResolvers를 해서 우리가 만든 리졸버를 add하면 됩니다. 이렇게 하면 @Login과 멤버가 있는 파라미터를 만족하면 @Login이 세션을 확인하고 멤버를 반환하도록 @SessionAttribute처럼 동작합니다. 두번째 실행부터는 서포트는 실행되지 않고 쿠키에 저장됩니다. AR을 응용하면 컨트롤러에서 공통으로 사용하는 파라미터를 간편하게 확 줄일 수 있습니다.

 

'[백엔드] > [spring | 학습기록]' 카테고리의 다른 글

spring PART.API 예외처리  (0) 2023.04.27
spring PART.예외 처리와 오류 페이지  (0) 2023.04.26
spring PART.로그인, 세션관리  (0) 2023.04.23
spring PART.빈 검증  (0) 2023.04.21
spring PART.검증  (0) 2023.04.20
Comments