HttpServletRequest 와 HttpServletResponse 를 통해서 여러가지 작업을 처리할 수 있다는 것을 알고는 있지만 실제로 간단한 필터처리를 제외하고는 사용해 본적이 없었다. 그러던 중 모든 사용자의 request와 response 를 분석해 기록을 해달라는 요구사항이 들어왔고 이 기회를 통해서 내가 헷갈렸던 부분과 필터의 처리 과정에 대해서 다시한 번 정리 해보려고 한다. (Filter 처리 후 Spring Context 영역 내에서 일어나는 것은 다루지 않을 예정)
우선 Client 의 Request 가 들어오면 어떤 방식으로 Response 를 만들어 전달하는지에 대한 부분부터 살펴봐야 한다. Client 가 Server 로 Request 를 보내면 Spring 영역으로 들어가기 전 Filter를 통과한 후 Spring 에서 처리가 되고 처리 된 Response는 Filter를 통과 후 Client 로 보내진다. Filter 를 한 번이라도 사용해본 적이 있다면 이 동작 과정을 누구나 알고 있을 것이다.
하지만 Filter 가 chain 으로 여러 개 걸려 있고 그 과정속에서 Reuest 와 Response 를 이용해 특정 로직을 수행 하기 위해선 필터의 전반적인 동작 과정과 Client 의 Request 와 Response 에 대입되는 HttpServletRequest 와 HttpServletResponse를 이해할 필요가 있다. (OncePerRequestFilter 의 예제 사용)
HttpServletRequest, HttpServletResponse 객체 이해
Client 의 Request, Response 는 HttpServletRequest, HttpServletResponse 와 완전히 같은 객체 일 까?
아니다.
서블릿 컨테이너에서 생성되는 HttpServlet 요청/응답 객체는 Client의 HTTP 요청을 표현하는 객체이기 때문에 이 객체를 통해서 요청/응답에 대한 접근을 진행하는 것 일 뿐 완전히 동일 한 객체라고 할 수는 없다. 따라서 Fliter에서 처리하는 여러가지 작업들이 실제 Client의 요청/응답에 영향을 미치지 않기 때문에 Filter 단에서도 여러가지 작업이 가능하다고 할 수 있는 것이다. 하지만 여러가지 작업을 하기 전에 Wrapper로 캐싱 작업을 진행함은 물론 요청/응답의 처리 순서를 정확히 알고 진행해야 하기 때문에 동작과정에 대한 정확한 이해없이 객체에 접근한다면 원하는 값 혹은 예상치 못한 에러를 발생시킬 수 있다.
ContentCachingRequestWrapper, ContentCachingResponseWrapper 객체
HttpServlet 응답/요청 객체를 읽지 않고 Wrapping을 한 후에 사용해야 하는 이유는 서블릿 객체가 HTTP의 Stateless 의 특성까지 그대로 물려 받았기 때문이다. Stateless 의 이유와는 상관 없이 그 특성을 그대로 물려받아서 사용 되기 때문에 객체를 직접 읽는 다면 객체의 값이 날라가기 때문에 주의 해야 한다. 따라서 서블릿 객체를 그대로 복사해 읽고 수정할 수 있도록 하는 새로운 객체가 필요했고 그 역할을 ContentCaching(Request/Response)Wrapper 를 통해 수행하게 된다.
public class TestFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
// <-- filter 전 처리 영역 -->
filterChain.doFilter(request, responseWrapper);
// <-- filter 후 처리 영역 -->
// RESPONSE 사용자 처리 진행.
responseWrapper.copyBodyToResponse();
}
}
filterChain.doFilter() 를 전 후로 다음 필터 혹은 서블릿으로 넘어가기 때문에 필터 처리전에 수행해야 할 부분은 doFilter 메소드를 호출하기 전에 선언하고 필터 처리후에 수행해야 할 부분은 doFilter 메소드 호출 후에 선언하도록 해야 한다. 또한 responseWrapper 를 읽고 나서 다음 필터 혹은 client로 return 되기 전 copyBodyToResponse() 메소드를 호출해 줘야지 전달되는 response에 제대로 된 값을 보낼 수 있다.
여기서 다른 점은 Request에 대한 부분인데 ContentCachingRequestWrapper는 ContentCachingResponseWrapper 와 달리 다음 필터 혹은 서블릿에서 사용을 해야 하기 때문에 값을 읽는다 하더라도 날라가지 않는다. 따라서 requestWrapper에 값을 읽는 과정이 이후의 단계에 있어서 전혀 영향을 주지 않는다는 의미이다. 따라서 캐싱된 requestWrapper에 다시 값을 넣는 메소드가 존재하지 않는다.
public class TestFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingResponseWrapper(request);
// <-- filter 전 처리 영역 -->
//requestWrapper 를 읽거나 특정 속성 값을 추가 해도 다음 필터, 서블릿에 영향을 주지 않음.
filterChain.doFilter(requestWrapper, response);
// <-- filter 후 처리 영역 -->
}
}
FilterChain 과정

Client의 Request는 위에서 설명 한대로 Spring 영역에 도달하기 전 Filter 를 거치고 Spring영역에서 Dispacher Servlet이 Client의 Request, Response 에 대한 부분을 처리 후 다시 Filter를 통해 Client에 Response 를 전달하는 구조지만 필터가 chain 으로 여러개 걸려있고 그 사이에서 특정 작업을 처리해야 할 경우 요청과 응답의 객체에 대한 처리를 신중하게 진행해야 한다.

실제 여러개의 필터를 체인으로 묶어서 로그를 찍어 확인해 보면 각 필터에 filterChain.doFilter() 메소드의 호출을 기준으로 전 처리 영영과 후 처리 영역이 나눠지고 호출의 순서는 위와 같은 방식으로 묶여있는 필터 순서대로 전처리 영역이 먼저 처리 된 후 후처리 영역이 호출 된다.
실제 사용 예제
실무에서 적용된 Filter는 총 3개로 위의 이미지에 해당하는 필터 처리가 적용 되어 있었다. 중간 필터가 Session을 관리하는 필터였고 내가 처리할 작업에는 Request의 Header에 있는 Session 정보를 통해 고객의 정보를 알아내는 과정이 필요했다. 고객의 정보만 필요했다면 sessionFilter 부분에서 모든 처리가 가능하겠지만 다음 필터에서도 또 다른 수행 과정이 필요 했기 때문에 request에 속성을 추가하여 해결하기로 했다.
위에서 말 했듯이 HttpServletRequest는 Client 에서 보내는 requet 객체와 동일한 객체를 넘어서 필터 처리 과정에서 새로운 속성값을 추가 할 수 있는 메소드 setAttribute() 를 제공해 준다. 그리고 getAttribute() 메소드를 통해서 그 값을 읽을 수 있다.
public class FilterB extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// <-- filter 전 처리 영역 -->
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
// 사용자 정보 받아옴.
requestWrapper.setAttribute("userName", userName);
filterChain.doFilter(requestWrapper, response);
// <-- filter 후 처리 영역 -->
}
}
public class FilterC extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// <-- filter 전 처리 영역 -->
// filterB 에서 저장한 사용자 정보를 받아올 수 있음
String userName = request.getAttribute("userName").toString();
filterChain.doFilter(request, response);
// <-- filter 후 처리 영역 -->
}
}
이 값은 Request의 header나 body에 추가되는 값이 아니다. 왜나면 Filter에서 처리되는 Request는 실제 Client 에서 보내는 Request를 캐싱한 다른 클래스 이기 때문이다.
정리
Request와 Response 를 필터에서 직접 다뤄본 적이 없어서 호출 순서 라던지 혹은 HTTP 의 Stateless 에 대해서 깊게 생각해 보지 않아 계속적으로 Null 포인트 에러를 터뜨렸는데 이렇게 정리를 해서 보니 당연한 부분에서 많이 놓칠 수 있다는 생각이 들었다. 확실한 필터 처리를 위해서 Filter가 chain 으로 묶여 호출되는 전반적인 과정과 그 속에서 request와 response 가 실제로 유효하게 처리될 수 있는 영역, 그리고 휘발성을 가진 객체의 특성에 대해 아는 것이 중요하단 생각이 들었다.
HttpServletRequest 와 HttpServletResponse 를 통해서 여러가지 작업을 처리할 수 있다는 것을 알고는 있지만 실제로 간단한 필터처리를 제외하고는 사용해 본적이 없었다. 그러던 중 모든 사용자의 request와 response 를 분석해 기록을 해달라는 요구사항이 들어왔고 이 기회를 통해서 내가 헷갈렸던 부분과 필터의 처리 과정에 대해서 다시한 번 정리 해보려고 한다. (Filter 처리 후 Spring Context 영역 내에서 일어나는 것은 다루지 않을 예정)
우선 Client 의 Request 가 들어오면 어떤 방식으로 Response 를 만들어 전달하는지에 대한 부분부터 살펴봐야 한다. Client 가 Server 로 Request 를 보내면 Spring 영역으로 들어가기 전 Filter를 통과한 후 Spring 에서 처리가 되고 처리 된 Response는 Filter를 통과 후 Client 로 보내진다. Filter 를 한 번이라도 사용해본 적이 있다면 이 동작 과정을 누구나 알고 있을 것이다.
하지만 Filter 가 chain 으로 여러 개 걸려 있고 그 과정속에서 Reuest 와 Response 를 이용해 특정 로직을 수행 하기 위해선 필터의 전반적인 동작 과정과 Client 의 Request 와 Response 에 대입되는 HttpServletRequest 와 HttpServletResponse를 이해할 필요가 있다. (OncePerRequestFilter 의 예제 사용)
HttpServletRequest, HttpServletResponse 객체 이해
Client 의 Request, Response 는 HttpServletRequest, HttpServletResponse 와 완전히 같은 객체 일 까?
아니다.
서블릿 컨테이너에서 생성되는 HttpServlet 요청/응답 객체는 Client의 HTTP 요청을 표현하는 객체이기 때문에 이 객체를 통해서 요청/응답에 대한 접근을 진행하는 것 일 뿐 완전히 동일 한 객체라고 할 수는 없다. 따라서 Fliter에서 처리하는 여러가지 작업들이 실제 Client의 요청/응답에 영향을 미치지 않기 때문에 Filter 단에서도 여러가지 작업이 가능하다고 할 수 있는 것이다. 하지만 여러가지 작업을 하기 전에 Wrapper로 캐싱 작업을 진행함은 물론 요청/응답의 처리 순서를 정확히 알고 진행해야 하기 때문에 동작과정에 대한 정확한 이해없이 객체에 접근한다면 원하는 값 혹은 예상치 못한 에러를 발생시킬 수 있다.
ContentCachingRequestWrapper, ContentCachingResponseWrapper 객체
HttpServlet 응답/요청 객체를 읽지 않고 Wrapping을 한 후에 사용해야 하는 이유는 서블릿 객체가 HTTP의 Stateless 의 특성까지 그대로 물려 받았기 때문이다. Stateless 의 이유와는 상관 없이 그 특성을 그대로 물려받아서 사용 되기 때문에 객체를 직접 읽는 다면 객체의 값이 날라가기 때문에 주의 해야 한다. 따라서 서블릿 객체를 그대로 복사해 읽고 수정할 수 있도록 하는 새로운 객체가 필요했고 그 역할을 ContentCaching(Request/Response)Wrapper 를 통해 수행하게 된다.
public class TestFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
// <-- filter 전 처리 영역 -->
filterChain.doFilter(request, responseWrapper);
// <-- filter 후 처리 영역 -->
// RESPONSE 사용자 처리 진행.
responseWrapper.copyBodyToResponse();
}
}
filterChain.doFilter() 를 전 후로 다음 필터 혹은 서블릿으로 넘어가기 때문에 필터 처리전에 수행해야 할 부분은 doFilter 메소드를 호출하기 전에 선언하고 필터 처리후에 수행해야 할 부분은 doFilter 메소드 호출 후에 선언하도록 해야 한다. 또한 responseWrapper 를 읽고 나서 다음 필터 혹은 client로 return 되기 전 copyBodyToResponse() 메소드를 호출해 줘야지 전달되는 response에 제대로 된 값을 보낼 수 있다.
여기서 다른 점은 Request에 대한 부분인데 ContentCachingRequestWrapper는 ContentCachingResponseWrapper 와 달리 다음 필터 혹은 서블릿에서 사용을 해야 하기 때문에 값을 읽는다 하더라도 날라가지 않는다. 따라서 requestWrapper에 값을 읽는 과정이 이후의 단계에 있어서 전혀 영향을 주지 않는다는 의미이다. 따라서 캐싱된 requestWrapper에 다시 값을 넣는 메소드가 존재하지 않는다.
public class TestFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingResponseWrapper(request);
// <-- filter 전 처리 영역 -->
//requestWrapper 를 읽거나 특정 속성 값을 추가 해도 다음 필터, 서블릿에 영향을 주지 않음.
filterChain.doFilter(requestWrapper, response);
// <-- filter 후 처리 영역 -->
}
}
FilterChain 과정

Client의 Request는 위에서 설명 한대로 Spring 영역에 도달하기 전 Filter 를 거치고 Spring영역에서 Dispacher Servlet이 Client의 Request, Response 에 대한 부분을 처리 후 다시 Filter를 통해 Client에 Response 를 전달하는 구조지만 필터가 chain 으로 여러개 걸려있고 그 사이에서 특정 작업을 처리해야 할 경우 요청과 응답의 객체에 대한 처리를 신중하게 진행해야 한다.

실제 여러개의 필터를 체인으로 묶어서 로그를 찍어 확인해 보면 각 필터에 filterChain.doFilter() 메소드의 호출을 기준으로 전 처리 영영과 후 처리 영역이 나눠지고 호출의 순서는 위와 같은 방식으로 묶여있는 필터 순서대로 전처리 영역이 먼저 처리 된 후 후처리 영역이 호출 된다.
실제 사용 예제
실무에서 적용된 Filter는 총 3개로 위의 이미지에 해당하는 필터 처리가 적용 되어 있었다. 중간 필터가 Session을 관리하는 필터였고 내가 처리할 작업에는 Request의 Header에 있는 Session 정보를 통해 고객의 정보를 알아내는 과정이 필요했다. 고객의 정보만 필요했다면 sessionFilter 부분에서 모든 처리가 가능하겠지만 다음 필터에서도 또 다른 수행 과정이 필요 했기 때문에 request에 속성을 추가하여 해결하기로 했다.
위에서 말 했듯이 HttpServletRequest는 Client 에서 보내는 requet 객체와 동일한 객체를 넘어서 필터 처리 과정에서 새로운 속성값을 추가 할 수 있는 메소드 setAttribute() 를 제공해 준다. 그리고 getAttribute() 메소드를 통해서 그 값을 읽을 수 있다.
public class FilterB extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// <-- filter 전 처리 영역 -->
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
// 사용자 정보 받아옴.
requestWrapper.setAttribute("userName", userName);
filterChain.doFilter(requestWrapper, response);
// <-- filter 후 처리 영역 -->
}
}
public class FilterC extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// <-- filter 전 처리 영역 -->
// filterB 에서 저장한 사용자 정보를 받아올 수 있음
String userName = request.getAttribute("userName").toString();
filterChain.doFilter(request, response);
// <-- filter 후 처리 영역 -->
}
}
이 값은 Request의 header나 body에 추가되는 값이 아니다. 왜나면 Filter에서 처리되는 Request는 실제 Client 에서 보내는 Request를 캐싱한 다른 클래스 이기 때문이다.
정리
Request와 Response 를 필터에서 직접 다뤄본 적이 없어서 호출 순서 라던지 혹은 HTTP 의 Stateless 에 대해서 깊게 생각해 보지 않아 계속적으로 Null 포인트 에러를 터뜨렸는데 이렇게 정리를 해서 보니 당연한 부분에서 많이 놓칠 수 있다는 생각이 들었다. 확실한 필터 처리를 위해서 Filter가 chain 으로 묶여 호출되는 전반적인 과정과 그 속에서 request와 response 가 실제로 유효하게 처리될 수 있는 영역, 그리고 휘발성을 가진 객체의 특성에 대해 아는 것이 중요하단 생각이 들었다.