잡동사니를 모아두는 서랍장
Request Body를 인터셉터에서 읽고 싶을 때 본문
구직과 퇴사와 인수인계 때문에 현생이 혐생이었어서 이제야 새로운 글을 작성한다. 한달만 놀고 다시 출근하면 좋겠다...
한창 전 직장에서 일할 때, 요청에 들어온 데이터로 인증 여부를 판별해야 하는 일이 있었다. 하지만 한 가지 문제가 있었는데...판별 데이터가 Request Body에 있고 나는 이것을 필터가 아니라 인터셉터에서 읽어내야 했다는 것이다.
모두 알다시피 Body 는 한번만 읽을 수 있다. 이유는 HttpServletRequest의 InputStream 은 한번 읽으면 다시 못읽기 때문에. 다시 읽으려고 하면 Exception 파티 일어난다. 고통스럽다...
해결방안을 모색하려고 하던 중 이전에 팀장님께서 팀 슬랙에 Request Body를 로깅하는 포스트를 공유하신적이 있다. 거기에서 인터셉터로 처리했던거 같은데...?
Interceptor 에서 Request Body를 읽을 수 있을까?
일단, 인터셉터 자체에서는 방법이 없다고 한다. 찾아보니 나말고도 많은 사람들이 인터셉터로만 처리해보려고 했지만...방법이 없는 듯 하다(만약 있으면 제보 부탁드려요ㅎㅎ).
여기서는 필터에서 래퍼클래스를 이용하여 임의로 요청을 래핑하는 작업을 통해 인터셉터에서 읽을 수 있게 하였다.
HttpServletRequestWrapper 를 상속한 Wrapper 클래스를 만들고 필터에서 이 wrapper 를 사용하여 attribute를 추가하게 래핑하였다.
HttpServletRequestWrapper
HttpServletRequestWrapper는 ServletRequestWrapper를 상속한 래퍼클래스. ServletRequestWrapper는 필터가 요청을 변경한 결과를 저장하는 래퍼클래스다.
이걸로 HttpServletRequest를 래핑해보자.
내용 자체는 거창하지 않다. 어차피 한번 읽으면 다시 못읽는거 일단 읽어서 byte array로 따로 빼두고, InputStream을 요청하는 상황에서는 빼둔 데이터로 새로운 InputStream을 만들어 반환시킨다.
새로 만든 래퍼클래스 ReadableRequestBodyWrapper는 byte array와 String 필드를 가지고 있다. byte array는 byte로 변환된 HttpServletRequest 의 InputStream의 contents를 저장하기 위한 용도. 이건 나중에 InputStream을 새로 만들기 위해 필요하다.
String 필드는 위의 변환된 contents를 나중에 인터셉터에서 읽을 목적으로 추가했다. 즉 body의 내용을 담게 된다.
이제 어디선가 getInputStream 메소드를 호출한다면 아까 구해놓은 byte을 이용해 InputStream을 새로 만들어 반환해준다.
public class ReadableRequestBodyWrapper extends HttpServletRequestWrapper {
class ServletInputStreamImpl extends ServletInputStream {
private InputStream inputStream;
public ServletInputStreamImpl(final InputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public boolean isFinished() {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean isReady() {
// TODO Auto-generated method stub
return false;
}
@Override
public int read() throws IOException {
return this.inputStream.read();
}
@Override
public int read(final byte[] b) throws IOException {
return this.inputStream.read(b);
}
@Override
public void setReadListener(final ReadListener listener) {
// TODO Auto-generated method stub
}
}
private byte[] bytes;
private String requestBody;
public ReadableRequestBodyWrapper(final HttpServletRequest request) throws IOException {
super(request);
InputStream in = super.getInputStream();
// request의 InputStream의 content를 byte array로 가져오고
this.bytes = IOUtils.toByteArray(in);
// 그 데이터는 따로 저장한다
this.requestBody = new String(this.bytes);
}
@Override
public ServletInputStream getInputStream() throws IOException {
// InputStream을 반환해야하면 미리 구해둔 byte array 로
// 새 InputStream을 만들고 이걸로 ServletInputStream을 새로 만들어 반환
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.bytes);
return new ServletInputStreamImpl(byteArrayInputStream);
}
public String getRequestBody() {
return this.requestBody;
}
}
귯귯. 이제 필터에서 사용해보자.
위에서 만든 래퍼클래스를 이용해서 request에 body 내용을 담은 attribute를 추가한다.
@Component
public class CopyBodyFilter implements Filter {
@Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
throws IOException, ServletException {
try {
ReadableRequestBodyWrapper wrapper = new ReadableRequestBodyWrapper((HttpServletRequest) request);
wrapper.setAttribute("requestBody", wrapper.getRequestBody());
chain.doFilter(wrapper, response);
} catch (Exception e) {
chain.doFilter(request, response);
}
}
}
이제 인터셉터에서 이 attribute를 꺼내서 body 내용을 사용해본다.
@Component
public class TestInterceptor extends HandlerInterceptorAdapter {
private void readBody(final HttpServletRequest request, final HttpServletResponse response) {
String reqBody = (String) request.getAttribute("requestBody");
/*
reqBody 값을 읽어 임의 처리.
*/
}
@Override
public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler)
throws Exception {
this.readBody(request, response);
return super.preHandle(request, response, handler);
}
}
이렇게 하면 Body의 내용을 인터셉터에서 확인할 수 있다. 여기서는 String으로 가정하였지만 여러가지 자료형이 될 수도 있을거다.
팀장님은 이 방법이 좋은 방법은 아니라고 하셨다. 필터에서 계속 래핑을 하는게 요청이 많아지면 부하가 있다고. 나중에 더 좋은 방법을 찾게 되면 추가로 포스팅하겠다.
참고
'Spring' 카테고리의 다른 글
@Transactional 이 안먹힐 때 참고할 기본적인것들 (0) | 2021.01.29 |
---|---|
Request Body를 따로 읽고 싶을 때 (0) | 2020.04.21 |
스프링의 간단한 Async 처리 (0) | 2020.02.27 |
인터셉터와 필터 차이 (0) | 2020.02.23 |
PropertySourcesPlaceholderConfigurer로 프로퍼티로 지정하기 (0) | 2020.02.22 |