[ 김영한 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 #7 ] 로그인 처리2 - 필터, 인터셉터 (2)

김수호·2023년 9월 20일
0
post-thumbnail

지난 포스팅에 이어, 이번 포스팅에서는 4) ~ 8) 까지의 내용을 정리한다.

👉 목차는 다음과 같다.

1) 서블릿 필터 - 소개
2) 서블릿 필터 - 요청 로그
3) 서블릿 필터 - 인증체크
4) 스프링 인터셉터 - 소개
5) 스프링 인터셉터 - 요청 로그
6) 스프링 인터셉터 - 인증 체크
7) ArgumentResolver 활용
8) 정리


4) 스프링 인터셉터 - 소개

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

스프링 인터셉터 흐름

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

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

  • 로그인 사용자
  • 비로그인 사용자

스프링 인터셉터 체인

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

 

지금까지 내용을 보면 서블릿 필터와 호출되는 순서만 다르고, 제공하는 기능은 비슷해 보인다.
앞으로 설명하겠지만, 스프링 인터셉터는 서블릿 필터보다 편리하고, 더 정교하고 다양한 기능을 지원한다.

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

  • 스프링의 인터셉터를 사용하려면 HandlerInterceptor 인터페이스를 구현하면 된다.
    • 서블릿 필터의 경우 단순하게 doFilter() 하나만 제공된다. 인터셉터는 컨트롤러 호출 전( preHandle ), 호출 후( postHandle ), 요청 완료 이후( afterCompletion )와 같이 단계적으로 잘 세분화 되어 있다.
    • 서블릿 필터의 경우 단순히 request , response 만 제공했지만, 인터셉터는 어떤 컨트롤러( handler )가 호출되는지 호출 정보도 받을 수 있다. 그리고 어떤 modelAndView 가 반환되는지 응답 정보도 받을 수 있다.

스프링 인터셉터 호출 흐름

  • 참고)
    • 정상 흐름
      • preHandle : 컨트롤러 호출 전에 호출된다. (더 정확히는 핸들러 어댑터 호출 전에 호출된다.)
        • preHandle 의 응답값이 true 이면 다음으로 진행하고, false 이면 더는 진행하지 않는다. false 인 경우 나머지 인터셉터는 물론이고, 핸들러 어댑터도 호출되지 않는다. 그림에서 1번에서 끝이 나버린다.
      • postHandle : 컨트롤러 호출 후에 호출된다. (더 정확히는 핸들러 어댑터 호출 후에 호출된다.)
      • afterCompletion : 뷰가 렌더링 된 이후에 호출된다.

스프링 인터셉터 예외 상황

  • 참고)
    • 예외가 발생시
      • preHandle : 컨트롤러 호출 전에 호출된다.
      • postHandle : 컨트롤러에서 예외가 발생하면 postHandle 은 호출되지 않는다
      • afterCompletion : afterCompletion 은 항상 호출된다. 이 경우 예외(ex)를 파라미터로 받아서 어떤 예외가 발생했는지 로그로 출력할 수 있다.
    • afterCompletion은 예외가 발생해도 호출된다.
      • 예외가 발생하면 postHandle() 는 호출되지 않으므로 예외와 무관하게 공통 처리를 하려면 afterCompletion() 을 사용해야 한다.
      • 예외가 발생하면 afterCompletion() 에 예외 정보(ex)가 포함된다.
      • (참고) 예외가 발생하지 않은 정상 흐름일 경우 예외 정보(ex) 가 null로 넘어가고, 예외가 발생했을 때만 예외 정보(ex)가 넘어온다.

 

✔️ 정리

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

✔️ 참고

  • 위 호출 흐름이 이해되지 않으면 디스패쳐 서블릿의 코드의 흐름을 실제로 확인해보면서 쭉 따라가 보자.

다음 내용에서는, 이전에 서블릿 필터를 통해서 요청 로그를 적용했던 것 처럼, 스프링 인터셉터를 사용해서 동일한 기능을 만들어보자.


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

👉 코드로 바로 확인해보자.

  • LogInterceptor - 요청 로그 인터셉터: src > main > java > hello > login > web > interceptor 패키지를 생성하고. 내부에 LogInterceptor 클래스를 생성하자.
    • 스프링 인터셉터를 적용하기 위헤서는 HandlerInterceptor 인터페이스를 구현해야한다.
  • LogInterceptor - preHandle(): 아래와 같이 적용하자.
    • handler 정보를 출력할수도 있다.
  • LogInterceptor - postHandle(): 아래와 같이 적용하자.
    • modelAndView 정보를 출력할수도 있다.
  • LogInterceptor - afterCompletion(): 아래와 같이 적용하자.
    • 예외 정보를 출력할수도 있다.

 

로직 분석 - preHandle

  • String uuid = UUID.randomUUID().toString()
    • 요청 로그를 구분하기 위한 uuid 를 생성한다.
  • request.setAttribute(LOG_ID, uuid)
    • 서블릿 필터의 경우 지역변수로 해결이 가능하지만, 스프링 인터셉터는 호출 시점이 완전히 분리되어 있다. 따라서 preHandle 에서 지정한 값을 postHandle , afterCompletion 에서 함께 사용하려면 어딘가에 담아두어야 한다. LogInterceptor 도 싱글톤 처럼 사용되기 때문에 맴버변수를 사용하면 위험하다. 따라서 request 에 담아두었다. 이 값은 afterCompletion 에서 request.getAttribute(LOG_ID) 로 찾아서 사용한다.
  • return true
    • true 면 정상 호출이다. 다음 인터셉터나 컨트롤러가 호출된다.
  • 참고)
    • HandlerMethod
      • 핸들러 정보는 어떤 핸들러 매핑을 사용하는가에 따라 달라진다. 스프링을 사용하면 일반적으로 @Controller , @RequestMapping 을 활용한 핸들러 매핑을 사용하는데, 이 경우 핸들러 정보로 HandlerMethod 가 넘어온다.
    • ResourceHttpRequestHandler
      • @Controller 가 아니라 /resources/static 와 같은 정적 리소스가 호출 되는 경우 ResourceHttpRequestHandler 가 핸들러 정보로 넘어온다.
    • 때문에, 사용시 타입에 따라서 위와 같이 분기 처리가 필요할 수 있다.

 

로직 분석 - postHandle, afterCompletion

  • 종료 로그를 postHandle 이 아니라 afterCompletion 에서 실행한 이유는, 예외가 발생한 경우 postHandle 가 호출되지 않기 때문이다. afterCompletion 은 예외가 발생해도 호출되는 것을 보장한다.

 

👉 이제 인터셉터를 등록 후 실행해보자.

  • WebConfig - 인터셉터 등록: 다음과 같이 적용하자.
    • WebMvcConfigurer 가 제공하는 addInterceptors() 를 사용해서 인터셉터를 등록할 수 있다.
    • registry.addInterceptor(new LogInterceptor()) : 인터셉터를 등록한다.
    • order(1) : 인터셉터의 호출 순서를 지정한다. 낮을 수록 먼저 호출된다.
    • addPathPatterns("/**") : 인터셉터를 적용할 URL 패턴을 지정한다.
    • excludePathPatterns("/css/**", "/*.ico", "/error") : 인터셉터에서 제외할 패턴을 지정한다.
    • 필터와 비교해보면 인터셉터는 addPathPatterns , excludePathPatterns 로 매우 정밀하게 URL 패턴을 지정할 수 있다.
  • 실행해보자.
    • 기존에 적용한 필터와 실행 순서를 확인해보자. (LogFilter가 먼저 적용되고 -> 이후에 LoginCheckFilter가 적용된다. -> 그리고 이후에 LogInterceptor가 적용된다.)
    • 인터셉터 preHandle 로그를 보면, 핸들러 정보가 출력됨을 확인할 수 있다. 자세히 보면 파라미터 정보도 출력된다. ( 인터셉터 내부 로직에서 핸들러와 관련한 수많은 정보를 조회할 수 있다. )
    • 인터셉터 preHandle 로그를 보면, modelAndView 정보가 출력됨을 확인할 수 있다. (viewName이 무엇이고, model에는 어떤 정보가 담겼는지 출력되는 것을 확인할 수 있다.)
    • 인터셉터 afterCompletion 로그도 정상적으로 출력됨을 확인할 수 있다.
  • 인터셉터와 필터가 중복되지 않도록 필터를 등록하기 위한 logFilter()@Bean 은 주석처리하자. 그리고 다시 실행해보자.
    • LoginCheckFilter가 먼저 적용되고 -> 그 다음 LoginInterceptor가 적용된 것을 확인할 수 있다.
    • (참고) 학습 시 상품 등록 등도 해보면서 로그를 확인해보자.

 

✔️ 참고


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

서블릿 필터에서 사용했던 인증 체크 기능을 스프링 인터셉터로 개발해보자.

👉 코드로 바로 적용해보자.

  • LoginCheckInterceptor - preHandle: 다음과 같이 적용하자.
    • 서블릿 필터와 비교해서 코드가 매우 간결하다. 인증이라는 것은 컨트롤러 호출 전에만 호출되면 된다. 따라서 preHandle 만 구현하면 된다.
    • (로직을 보면) 서블릿 필터에서 해줬던 whitelist 관련된 로직이 없다. 스프링 인터셉터를 사용하면, 인터셉터 등록시에 경로에 대한 모든 처리가 가능하다.
  • WebConfig - 인터셉터 등록: 다음과 같이 적용하자. ( 순서 주의, 세밀한 설정 가능 )
    • 인터셉터를 적용하거나 또는 적용하지 않을 부분은 addPathPatternsexcludePathPatterns 에 작성하면 된다. (이 경로를 정밀하게 작성할 수 있다는 것은 정말 큰 장점이다.)
    • 기본적으로 모든 경로에 해당 인터셉터를 적용하되 ( /** ), 홈( / ), 회원가입( /members/add ), 로그인( /login ), 리소스 조회( /css/** ), 오류( /error )와 같은 부분은 로그인 체크 인터셉터를 적용하지 않는다. 서블릿 필터와 비교해보면 매우 편리한 것을 알 수 있다.
  • 실행해보자. (홈 화면에서 상품 관리 URL을 직접 요청한 경우)
    • 비로그인 상태에서 홈 화면에서 상품 관리 URL을 직접 요청한 경우, 로그인하지 않았으므로 로그인 페이지로 리다이렉트 된다.
    • 로그를 보면, 상품 관리( /items )를 요청할때는 LogInterceptor와 LoginCheckInterceptor가 정상적으로 동작하였다. (이때, preHandle에서 response.sendRedirect를 하고 false를 리턴하기 때문에, postHandle은 호출되지 않는다.)
    • 그리고 로그인 페이지로 리다이렉트 될 때 로그를 보면, 로그인 URL은 LoginCheckInterceptor의 적용 대상 경로가 아니므로, LogInterceptor만 동작함을 확인할 수 있다.
    • (참고) 테스트시 인터셉터와 필터가 중복되지 않도록 필터를 등록하기 위한 logFilter() , loginCheckFilter()@Bean 은 주석처리하였다.

 

✔️ 정리

  • 서블릿 필터와 스프링 인터셉터는 웹과 관련된 공통 관심사를 해결하기 위한 기술이다.

  • 서블릿 필터와 비교해서 스프링 인터셉터가 개발자 입장에서 훨씬 편리하다는 것을 코드로 이해했을 것이다. 특별한 문제가 없다면 인터셉터를 사용하는 것이 좋다.


7) ArgumentResolver 활용

스프링 MVC 1편 6. 스프링 MVC - 기본 기능 요청 매핑 헨들러 어뎁터 구조에서 ArgumentResolver 를 학습했다.

이번에는 해당 기능을 사용해서 로그인 회원을 조금 더 편리하게 찾아보자.

 

👉 코드로 바로 확인해보자.

  • Login 애노테이션 생성: src > main > java > hello > login > web 내부에 argumentresolver 패키지를 생성하고, 내부에 Login 애노테이션을 생성하자.
    • 애노테이션의 문법상 @Target을 넣어줘야 한다.
    • @Target(ElementType.PARAMETER) : 파라미터에만 사용
    • @Retention(RetentionPolicy.RUNTIME) : 리플렉션 등을 활용할 수 있도록 런타임까지 애노테이션 정보가 남아있음 (거의 사용할 때는 RUNTIME으로 사용한다.)
  • 기존 코드를 유지하기 위해, HomeController 컨트롤러에 있는 homeLoginV3Spring()@GetMapping("/") 을 주석 처리하자.
  • HomeController - homeLoginV3ArgumentResolver 생성: 주석처리한 homeLoginV3Spring() 메서드를 복사해서 homeLoginV3ArgumentResolver() 메서드를 생성하자.
    • 여기까지만 적용하면, @Login 이 뭔지 인식을 못하기 때문에 ModelAtrribute가 동작한다. 따라서, 해당 애노테이션이 우리가 의도한 대로 동작하도록 적용해보자. (= 스프링 MVC 1편에서 학습한 HandlerMethodArgumentResolver 를 구현해보자.)
  • LoginMemberArgumentResolver 생성: src > main > java > hello > login > web > argumentresolver 패키지 내부에 LoginMemberArgumentResolver 클래스를 생성하자.
    • (참고) HandlerMethodArgumentResolver를 구현해야 한다.
    • supportsParameter() : @Login 애노테이션이 있으면서 Member 타입이면 해당 ArgumentResolver 가 사용된다.
    • resolveArgument() : 컨트롤러 호출 직전에 호출되어서, 필요한 파라미터 정보를 생성해준다. 여기서는 세션에 있는 로그인 회원 정보인 member 객체를 찾아서 반환해준다. 이후 스프링MVC는 컨트롤러의 메서드를 호출하면서 여기에서 반환된 member 객체를 파라미터에 전달해준다.
  • WebMvcConfigurer - 설정 추가 (LoginMemberArgumentResolver 등록): src > main > java > hello > login 패키지 내부 WebConfig 클래스에 다음과 같이 추가하자.
    • 앞서 개발한 LoginMemberArgumentResolver 를 등록하자.
  • 실행해보자. (홈 화면 접속시)
    • 정상적으로 argumentResolver가 실행됨을 확인할 수 있다. (@Login 애노테이션이 있으면 직접 만든 ArgumentResolver 가 동작해서 자동으로 세션에 있는 로그인 회원을 찾아주고, 만약 세션에 없다면 null 이 반환된다.)
    • 실행해보면, 결과는 동일하지만, 더 편리하게 로그인 회원 정보를 조회할 수 있다. 이렇게 ArgumentResolver 를 활용하면 공통 작업이 필요할 때 컨트롤러를 더욱 편리하게 사용할 수 있다.
    • (참고) 두 번째 요청을 해보면 "supportsParameter 실행" 로그는 안나오는 것을 확인할 수 있다. 이는 내부에 캐시가 적용되서 그렇다.

9) 정리

  • 서블릿 필터 - 소개
    • 서블릿 필터에 대해 알아보았다.
      • 결국 공통 관심 사항을 처리하고자 하는 것이다. AOP로도 해결할 수 있지만, 서블릿 필터나 스프링 인터셉터는 HTTP 헤더나, URL 정보들을 제공해준다. 그래서 편리하게 웹과 관련된 공통 관심 사항을 처리할 수 있다.
    • 필터 흐름, 필터 제한, 필터 체인, 필터 인터페이스에 대해서 알아보았다.
  • 서블릿 필터 - 요청 로그
    • 요청 경로를 로그로 남기는 기능을 개발해보았다.
  • 서블릿 필터 - 인증 체크
    • 서블릿 필터로 인증 체크를 개발해보았다.
  • 스프링 인터셉터 - 소개
    • 스프링 인터셉터에 대해서 알아보았다.
    • 스프링 인터셉터 흐름, 제한, 체인 기능에 대해서 알아보았다.
  • 스프링 인터셉터 - 요청 로그
    • 스프링 인터셉터를 사용해서 요청 경로를 로그로 남기는 기능을 개발해보았다.
  • 스프링 인터셉터 - 인증 체크
    • 스프링 인터셉터를 사용해서 인증 체크를 개발해보았다.
  • ArgumentResolver 활용
    • ArgumentResolver를 사용해서 회원 정보를 편리하게 조회해보았다.

강의를 듣고 정리한 글입니다. 코드와 그림 등의 출처는 김영한 강사님께 있습니다.

profile
현실에서 한 발자국

0개의 댓글