[Spring MVC] [2] 7. 로그인 처리2 - 필터, 인터셉터

윤경·2021년 9월 25일
0

Spring MVC

목록 보기
22/26
post-thumbnail

[1] 서블릿 필터 - 소개

문제점: 현재는 로그인하지 않은 사용자도 URL을 직접 호출하면 상품 관리 화면에 접속할 수 있다.

그렇다면 상품 관리에 관련된 모든 로직에 로그인 여부를 체크하는 코드를 넣으면 된다. 하지만 향후 로직을 변경해야 할 때마다 대규모 공사가 필요하다.

이렇게 애플리케이션 여러 로직에서 공통으로 관심있는 것을 공통 관심사(cross-cutting concern)라고 한다.
➡️ 웹과 관련된 공통 관심사는 서블릿 필터 or 스프링 인터셉터를 사용

웹과 관련된 공통 관심사 처리시 HTTP 헤더 or URL 정보가 필요한데 이는 서블릿 필터나 스프링 인터셉터가 HttpServletRequest를 제공한다.

서블릿 필터

: 서블릿이 지원하는 수문장

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

필터를 적용하면 필터 호출 후 서블릿이 호출
그래서모든 고객의 요청 로그를 남기는 요구사항이 있을 경우 필터를 사용하자.

필터는 특정 URL 패턴에 적용할 수 있으며 /* 이렇게 적용하면 모든 요청에 필터를 적용할 수 있다.

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

이렇게 필터를 제한할 수 있다.

또한, 필터를 여러개 거칠 수도 있다.

필터 인터페이스를 구현하고 등록하면 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고 관리한다.
init(): 필터 초기화 메소드. 서블릿 컨테이너가 생성될 때 호출
doFilter(): 고객 요청이 올 때마다 해당 메소드 호출
destroy(): 필터 종료 메소드. 서블릿 컨테이너가 종료될 때 호출


[2] 서블릿 필터 - 요청 로그

📌 참고

⭐️ doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
: HTTP 요청이 오면 doFilter 호출
String uuid = UUID.randomUUID().toString();
: HTTP 요청 구분을 위해 임의의 uuid 생성
⭐️ chain.doFilter(request, response);
: 다음 필터가 있으면 호출, 없으면 서블릿 호출
이 로직을 호출하지 않으면 다음 단계로 진행되지 않음

필터 등록시 FilterRegistrationBean 사용
setFilter(new LogFilter()): 등록할 필터 지정
setOrder(1): 필터는 체인으로 동작하기 때문에 순서가 필요하다. 1부터 시작.
addUrlPatterns("/*"): 필터를 적용할 URL 패턴 지정. 한 번에 여러 패턴 지정 가능.


[3] 서블릿 필터 - 인증 체크

📌 참고

whiteList={};: 인증 필터를 적용해도 (지금 예제에서는) 홈, 로그인, 회원가입, CSS와 같은 리소스에 접근할 수 있어야 한다. {} 안에 경로를 입력.
화이트 리스트 경로는 인증과 무관하게 항상 허용!!
화이트 리스트를 제외한 나머지 모든 경로에는 인증 체크 로직을 적용한다.
isLoginCheckPath(): 화이트 리스트를 제외한 모든 경우에 인증 체크 로직을 적용
httpResponse.sendRedirect();: 미인증 사용자는 로그인 화면으로 리다이렉트.

이때 우리 예제에서는 로그인 후 다시 홈으로 이동해버리면 다시 원하는 경로로 이동해야 되어 불편하다. 그래서 사용자의 편리함을 위해 개발자가 요청한 경로를 /login에 쿼리 파라미터로 전달한다. /login 컨트롤러에서 로그인 성공시 해당 경로로 이동하는 기능은 추가로 개발해야 한다.

setFilter(new LoginCheckFilter()): 로그인 필터 등록
setOrder(2): 두번째 순서. 로그 필터 다음에 로그인 필터 적용.
addUrlPatterns("/*"): 여기서 일일이 거를 수도 있지만 whitelist로 걸러냈으므로 이렇게 모든 요청에 로그인 필터를 적용해도 됨.


[4] 스프링 인터셉터 - 소개

📎 디스패처 서블릿

dispatcher-servlet에서 dispatch는 "보내다"

디스패처 서블릿은 가장 앞단에서 HTTP 프로토콜로 들어오는 모든 요청을 가장 먼저 받아 적합한 컨트롤러에 위임해주는 프론트 컨트롤러(front controller)

클라이언트로부터 어떤 요청이 오면 톰캣과 같은 서블릿 컨테이너가 요청을 받고 이 모든 요청을 먼저 프론트 컨트롤러 즉, 디스패처 서블릿이 받게 된다.
디스패처 서블릿은 공통 작업을 먼저 처리한 후 해당 요청을 처리해야 하는 세부 컨트롤러 getBean()으로 가져오고, 정해진 메소드를 실행시켜 작업을 위임한다.

예외가 발생했을 때도 일관된 방식으로 처리하는 것 또한 프론트 컨트롤러(디스패처 서블릿)이 담당한다.

* 프론트 컨트롤러: 서블릿 컨테이너의 제일 앞에서 서버로 들어오는 클라이언트의 모든 요청을 받아 처리해주는 컨트롤러. MVC 구조에서 함께 사용되는 디자인 패턴.

서블릿 필터: 서블릿이 제공하는 기술
스프링 인터셉터: 스프링 MVC가 제공하는 기술(더 좋아!!😸)

공통점 > 웹과 관련된 공통 관심 사항 처리.
차이점 > 적용되는 순서, 범위, 사용방법

스프링 인터셉터 흐름
HTTP 요청 → WAS → 필터 → 서블릿 → 스프링 인터셉터 → 컨트롤러

  • 📎 디스패처 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전 호출
  • 스프링 MVC가 제공하는 기능이기 때문에 결국 디스패처 서블릿 이후 등장
    (스프링 MVC의 시작점이 디스패처 서블릿)
  • 스프링 인터셉터에도 URL 패턴 적용 가능. 서블릿 URL 패턴과는 다르고 매우 정밀하게 설정 가능.

스프링 인터셉터 제한
- 로그인 사용자
HTTP 요청 → WAS → 필터 → 서블릿 → 스프링 인터셉터 → 컨트롤러
- 비로그인 사용자
HTTP 요청 → WAS → 필터 → 서블릿 → 스프링 인터셉터(적절하지 않은 요청이라 판단, 컨트롤러 호출 X, 여기서 딱 끝낼 수 있다는 것이 장점)

스프링 인터셉터 체인
HTTP 요청 → WAS → 필터 → 서블릿 → 스프링 인터셉터1 → 스프링 인터셉터2 → 컨트롤러

ex. 로그 남기는 인터셉터 적용 후 로그인 여부 체크하는 인터셉터

스프링 인터셉터를 사용하려면 HandlerIntercepter 인터페이스를 구현하기.

서블릿 필터의 경우 단순하게 doFilter() 하나만 제공됐었다. (개발자는 "요청이 왔네?" → doFilter()호출. 혹시라도 놓치면 사고)
인터셉터는 호출 전 preHandle, 호출 후 postHandle, 요청 완료 이후 afterCompletion 이렇게 단계적으로 세분화 되어있다.

또한, 서블릿 필터의 경우 request, response만 제공했었다.
인터셉터는 어떤 컨트롤러(핸들러)가 호출되는지 호출 정보도 받을 수 있다. 그리고 어떤 modelAndView가 반환되는지 응답 정보도 받을 수 있다.

스프링 인터셉터 호출 흐름

스프링 인터셉터 예외 상황

afterCompletion은 예외가 발생해도 호출된다.
이것이 가장 큰 차이점이다.

예외가 발생하면 postHandle()은 호출되지 않으므로 예외와 무관하게 공통 처리를 하려면 afterCompletion()(예외 정보(ex)를 포함해 호출됨)을 사용해야 한다.

즉,

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


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

String uuid = UUID.randomUUID().toString(): 요청 로그를 구분하기 위해 uuid 생성
request.setAttribute(LOG_ID, uuid): 서블릿 필터의 경우 지역 변수로 해결이 가능하지만, 스프링 인터셉터는 호출 시점이 완전히 분리되어 있다.
따라서 prehandle에서 지정한 값을 postHandle, afterCompletion에서도 사용하려면 어딘가 담아두어야하는데 LogInterceptor도 싱글톤처럼 사용되기 때문에 멤버 변수를 사용하려면 위험하다.
그래서 request에 담아두었다. 이 값은 afterCompletion에서 request.getAttribute(LOG_ID)로 찾아 사용한다.

HandlerMethod
핸들로 정보는 어떤 핸들러 매핑을 사용하는가에 따라 달라진다.
스프링을 사용하면 일반적으로 @Controller, @RequestMapping을 활용한 핸들러 매핑을 사용하는데 이 경우 핸들러 정보로 HandlerMethod가 넘어온다.

ResourceHttpRequestHandler
@Controller가 아니라 /resources/static과 같은 정적 리소스가 호출되는 경우 ResourceHttpRequestHandler가 핸들러 정보로 넘어오기 때문에 타입에 따라 처리가 필요하다.

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

WebMvcConfigurer가 제공하는 addInterceptors()를 사용해 인터셉터를 등록

registry.addInterceptor(new LogInterceptor()): 인터셉터 등록
order(1): 인터셉터 호출 순서를 첫번째로 지정
addPathPatterns("/**"): 인터셉터를 적용할 URL 패턴을 지정
excludePathPatterns("/css/**", "/*.ico", "/error"): 인터셉터에서 제외할 패턴 지정

(filter와 비교해보면 인터셉터는 addPathPatterns, excludePathPatterns로 매우 정밀하게 URL 패턴을 지정할 수 있음)


[6] 스프링 인터셉터 - 인증 체크

서블릿 필터와 다르게 코드가 매우 간결하며 인증컨트롤러 호출 전에만 호출하면 되므로 preHandler만 구현하면 된다.

인터셉터를 적용할 부분은 addPathPatterns, 적용하지 않을 부분은 excludePathPatterns에 작성하기.

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

서블릿 필터와 비교해 서블릿 인터셉터가 개발자 입장에서는 훨씬 편리하므로 특별한 예외 사항이 없다면 인터셉터를 사용하자


[7] ArgumentResolver 활용

@Login 애노테이션 만들기
직접 만든 ArgumentResolver를 동작시켜 자동으로 세션에 있는 로그인 회원을 찾아주고 세션에 없다면 null 반환

@Target(ElementType.PARAMETER): 파라미터에만 사용
@Retention(RetentionPolicy.RUNTIME): 리플렉션 등을 활용할 수 있도록 런타임까지 애노테이션 정보가 남아있음

supportsParameter(): @Login 애노테이션이 있으며 Member 타입이면 ArgumentResolver가 사용됨.
resolveArgument(): 컨트롤러 호출 직전 호출되어 필요한 파라미터 정보 생성해줌. 여기서는 세션에 있는 로그인 회원 정보 member 객체를 찾아 반환해줌. 이후 스프링 MVC는 컨트롤러의 메소드를 호출하며 여기서 반환된 member 객체를 파라미터에 전달해줌.

이렇게 ArgumentResolver를 활용하면 공통 작업이 필요할 때 컨트롤러를 더욱 편리하게 사용할 수 있다.


화이팅팅팅


참고

profile
개발 바보 이사 중

0개의 댓글