프로그래밍/Spring

[Spring] Spring Interceptor (인터셉터)와 WebTestClient Session (세션) 테스트 적용기

Jay22 2019. 7. 18. 23:07
반응형

Spring의 인터셉터 사용과 그것을 테스트하는 WebTestClient에 대해서 다뤄보려고 한다. 특히나 로그인 같은 기능을 구현하는 경우 서버측에서 세션을 생성하게 되는데 그것을 어떻게 하면 간단히 테스트할 수 있는지 알아보자.

MVC life cycle

 

먼저 인터셉터가 어떻게 작동하는지 그림을 보자. 인터셉터는 요청을 가로채는 역할을 한다. DispatcherServlet 이전에 가로챌것 처럼 보였지만 사실은 그 이후에 요청을 가로챈다. DispatcherServlet 이전의 요청을 거치는 것은 Filter의 역할로 볼 수 있다. 그리고 중요한 점은 인터셉터는 핸들러 이전을 가로채지만 그 이후도 처리할 수 있다는 점이다. (그림을 보면 핸들러 작업 이후 다시 인터셉터로 들어가는 모습을 볼 수 있다. 잠시 후 코드로 내용을 확인해보자)

 

다음은 스프링 공식 문서에서 번역해본 Interception에 대한 내용이다.

Interception

모든 HandlerMapping 구현은 특정 요청기능을 적용할때(어떤 규칙들) 매우 중요하게 쓰이는 핸들러 인터셉터를 지원한다. 인터셉터는 반드시 org.springframework.web.servlet 패키지에 있는 전처리와 후처리에 매우 유연한 3개의 메서드를 가지고 있는 HandlerInterceptor를 구현해야한다.

 

preHandle: 실제 핸들러가 실행되기 이전

postHandle: 핸들러가 실행되고 난 후

afterCompletion: 요청이 끝난 후

 

preHandle() 메서드는 boolean 값을 리턴한다. 이것을 이용해서 실행 과정을 멈출지 진행할지 결정할 수 있다. 만일 이 메서드에서 true를 반환하게 되면, 핸들러 실행은 지속된다. 만약 false가 반환되게 되면 DispatcherServlet은 인터셉터가 자체적으로 요청을 처리하겠거니 생각을 하고 (예를들어 알아서 적절한 뷰를 렌더링 한다든지) 실행 체인에 있는 다른 인터셉터들과 핸들러를 실행하지 않는다.

인터셉터의 구현

HandlerInterceptor를 구현한 클래스로 인터셉터를 만들어야 한다고 문서에 나와있기 때문에 그렇게 개발을 시작해보자.

AuthInterceltor.class

 

위와 같이 HandlerInterceptor를 구현한 AuthInterceptor를 만들었다. 내가 원하는 가로채기 기능은 현재 사용자의 세션이 있는지를 확인하는 것이다. 있다면 통과를 시켜주고, 없다면 login 페이지로 리다이렉트를 시키고 싶다. 그렇기 때문에 그 내용을 preHandle() 메서드를 구현하여 정의한다. postHandler은 하는일이 없기 때문에 비워둔다. 없어도 상관없다.

자 여기서 눈여겨볼 점은 이대로 AuthInterceptor 클래스를 정의했지만 스프링 컨테이너는 이 클래스의 유무를 모른다. 즉 컨테이너가 이 클래스를 집어갈 수 가 없다. 그렇다면 등록을 시켜주자.

 

WebConfig.class

 

WebMvcConfig라는 파일을 만들고 WebMvcConfiguerer를 구현했다. 저 인터페이스는 여러가지 설정이 있는데 지금은 인터셉터 설정만 할 것이기 때문에 저 addInterceptors 메서드만 구현한다. 사실은 WebMVC의 여러 기능들을 추가할 수 있다. 그리고 registry에 아까 정의한 AuthInterceptor를 new를 해주어 등록을 시켜준다. 그리고 인터셉터가 적용될 url 패턴을 정의한다. 그리고 @Configuration 애노테이션으로 스프링 컨테이너가 존재를 알아차리게 해준다.

이렇게되면 인터셉터를 등록할 수 있다. 사실 저렇게 WebMvcConfigurer를 구현하여 클래스를 만들 수 있지만 다른 방법도 있다.

 

TestConfig.class

이처럼 클래스에서 인터페이스를 구현하지 않고 아예 그 타입으로 리턴을 시킨다. 당연히 @Configuration을 선언해주고 우리가 만든 인터셉터 설정 메서드를 @Bean으로 등록을 시켜준다. 그럼 위와 같은 동작을 하게 된다. (비교를 위해 지금 막 만든 파일이라 이름이 TestConfig 클래스임...) 이처럼 어떤 종합적인 설정파일 (config)를 만들 때 첫 번째 위에서 보여준 코드는 WebMvcConfigurer에 종속적인 config 파일임에 반해 이 config 파일은 그것 뿐만 아니라 다른 인터페이스를 구현하는 설정까지 각각 @Bean으로 등록하여 만들 수 있는 장점이 있어보인다. 애플리케이션 특성에 맞게 선택하면 좋을 것 같다.

이렇게 함으로써 인터페이스 구현은 끝났다. 매우 간단하지 않은가? 스프링은 이처럼 매우 간결한 설정을 통해 강력한 기능들을 제공해준다.

우리가 지금 만든 인터셉터는 로그인에 기반한 세션을 기준으로 판단한다. 그렇다면 이 세션은 어떻게 테스트할 수 있을까?

Spring5에 추가된 WebTestClient라는 테스트용 클래스가 있다. 이것을 이용하여 우리가 세션이 있을 때 유저의 상세페이지에 접근할 수 있고 없을 때는 접근할 수 없는 테스트를 작성해보자.

 

@Autowired
WebTestClient webTestClient;

WebTestClient를 테스트 코드내에 주입시킨다.

 

테스트 케이스들

아까 우리는 세 장의 page에 인터셉터로 제약을 걸었다. 저 3개의 페이지에 접근할 때 세션이 없다면 들어가지 못하므로 요청이 들어가게 되면 login 페이지로 리다이렉트 될 것이다. 테스트는 모두 통과한다.

 

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

@LocalServerPort
private int localServerPort;

참고로 테스트 환경이 RANDOM_PORT 환경이라면 다음과 같이 @LocalServerPort 로 현재 실행중인 랜덤 포트를 가져올 수 있어서 테스트할 때 그 랜덤값을 이용할 수 있다.

 

그렇다면 세션을 직접 주입시켜서 테스트를 진행해보자. 사실 처음엔 막막했지만 코드를 하나씩 열어보았다.

WebTestClient interface

 

WebTestClient를 직접 열어보자. 따끈따끈한 5.0이 보이는가. 뭔가 세션에 대한 단서가 잡힐만한 메서드들을 쭉 찾아보았다.

WebTestClient의 환경을 설정할 수 있는 대표적인 static 메서드들의 목록을 찾았다.

bindToController, bindToRouterFunction, bindToApplicationContext, bindToWebHandler 등등... 그중 가장 눈에 띄는 메서드를 찾았다.

bindToWebHandler 메서드

바로 이 메서드인데 이름은 bindToWebHandler()이다. 주석의 내용을 해석하면 주어진 WebHandler를 이용해 Mock 서버를 통합 테스트할 수 있는 메서드였다. 아 그렇다면 이것이 바로 서버의 환경을 설정해주는 부분인듯 했다. 왠지 이 설명이 내가 지금 원하는 세션의 내용을 적용시킬 수 있지 않을까? 라는 생각이 스쳤다. WebHandler를 들어가보았다.

 

WebHandler interface

 

다행히 아주 간단한(?) 인터페이스였다. Mono 형식을 반환하는 handle이라는 메서드를 구현하면 되어보였다. 주석 설명은 웹 서버 실행을 관리하는 메서드였다. 그렇다면 저 인자인 ServerWebExchange를 들어가보자.

 

ServerWebExchange interface

 

주석의 설명대로라면 HTTP 요청-응답을 관리하는 인터페이스였다. 게다가 추가적인 서버사이드 프로세스를 관리한다라고 적혀있다. 왠지 정말 거의 다온듯 하다!!

 

잡았다 요놈

드디어 찾았다. getSession() 이름만 봐도 세션을 가져오게 생겼다. 자, 그럼 이제 다시 하나씩 위로 올라가면서 내가 원하는 코드를 만들어보자.

 

처음에 발견한 WebTestClient에 bindToWebHandler를 통해 WebHandler라는 것을 등록을 하면 될 것이고 그 WebHandler는 session을 관리하는 ServerWebExchange라는 것을 구현하여 넣어주면 될것이다. 아 좀 복잡하네...ㅠ

그럼 일단 저것 대로 코드의 틀을 만들어보자.

 

먼저 만들어본 틀

 

코드를 따라가면서 찾은 내용의 틀을 먼저 이렇게 만들었다. 그 다음 동작은 메서드에 쩜을 찍으며 추천 메서드들을 쭉 확인한다 (IDE의 강력함) 옆에 자동완성으로 뜨는 메서드들의 이름을 유츄하여 exchange 객체를 통해 URI path를 가져오는 것을 찾았다.

 

exchange.getRequest().getURI().getPath()

 

이 path가 맞다면 우리는 세션을 넣으면 될 것이다.

일단 현재 나는 WebFlux를 다뤄보지 않았기 때문에 Mono라는 녀석을 사실상 핸들할 수 없었다. 구글링으로 어떻게 핸들하면 되는지 찾아보았다.

 

결국에 완성한 코드

Session을 테스트하는 코드

exchange객체를 통해 세션을 가져오고 그리고 가져온 세션에 put 메서드를 통해 우리가 원하는 user라는 이름의 인자를 넣어 등록을 시켜주었다. 즉 저 메서드는 3가지의 url 패턴이 매칭이 된다면 webSession에 세션을 넣는 작업을 할 것이다. 실제로 테스트는 모두 통과했다.

 

WebTestClient의 적용이 쉽지많은 않았다. 하지만 앞으로 스프링을 이용하면서 아주 자주쓰게 될 최신 테스트용 클래스임에는 확실하다. 다음에는 WebTestClient의 더 많은 내용들을 다뤄보려고 한다.

 

참고!) 인터셉터의 매핑시 같은 url에 대한 다른 httpmethod 형식 처리

회원가입을 하는 경우에는 /user 의 post 요청, 회원의 목록을 조회하는데에는 /user의 get 요청 (이 부분은 세션이 있어야 조회가능) 이라면 인터셉터에서 단순 url 매핑으로는 거를수 없었다. 그래서 preHandle의 메서드 상단에 HttpServletRequest 객체를 통해서 URI path와 HttpMethod를 가져와 검증하는 작업을 추가로 넣었다.

if (request.getRequestURI().equals("/users") && request.getMethod().equals("POST")) {
	return true;
}
반응형