현재 오픈 예정인 Admin 서비스의 경우 사용자 활동에 대한 로그를 S3에 남겨야 한다.

처음에 욕심으로 RequestBody에 담기 내용도 올려야지~ 하고는 Interceptor에서 HttpServletRequest에 담긴

Body 내용을 기록했는데 Body 데이터가 Controller단에서 사용이 불가한 것 아니겠는가.

 

HttpServletRequest 인터페이스는 본문을 읽기 위해 getInputStream() 메소드를 갖고 있는데 기본적으로

이 InputStream의 데이터는 한 번만 읽을 수 있도록 설계가 되어 있다.

해당 데이터를 재활용하려면 Wrapper를 이용해야 했는데 개인적인 생각으로 이는 Too much라 생각되었고,

그냥 URL에 담겨있는 ip, method, url 같은 간단한 정보만 기록하는 것으로 일보 후퇴하였다.

이게 데이터를 어쨌든 복사하는 개념이니 Body에 데이터가 많은 경우

매 Request마다 이를 수행하는 것은 성능에도 좋을 수 없겠다는 판단이 들어서이기도 하다.

(never ever 귀찮아서가 아니었음)

 

그로부터 일년이 지났을까..ㅋㅋ 팀장님께서 다른 서버는 몰라도

Admin 서버의 경우에는 Body 데이터를 기록해야 된다는 의견을 주셨고, 결국 이를 수행하기로......

(그래, Admin은 성능보다는 정확성과 기록이 중요한거 아니겠어?!)

 

나는 아래와 같이 설정하였고, 잘 수행되는 것까지 확인하였다.

파일과 같은 멀티파트 타입의 데이터를 보내는 경우에는 

StandardMultipartHttpServletRequest가 Cast 불가하다는 에러를 발생시키기 때문에 

"application/json" 콘텐트 타입만 처리하도록 만들었다. 

 

 

Filter단

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;


@Slf4j
@Component
public class InitSettingFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        CachedBodyHttpServletWrapper cachedBodyHttpServletWrapper = new CachedBodyHttpServletWrapper(httpRequest);
        chain.doFilter(cachedBodyHttpServletWrapper, response);
    }

    @Override
    public void destroy() {
    }
}

 

HttpServletRequestWrapper구현

import lombok.SneakyThrows;
import org.springframework.util.StreamUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class CachedBodyHttpServletWrapper extends HttpServletRequestWrapper {

    private byte[] cachedBody;

    public CachedBodyHttpServletWrapper(HttpServletRequest request) throws IOException {
        super(request);
        InputStream requestInputStream = request.getInputStream();
        this.cachedBody = StreamUtils.copyToByteArray(requestInputStream);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        return new CachedBodyServletInputStream(this.cachedBody);
    }

    @Override
    public BufferedReader getReader() throws IOException {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
        return new BufferedReader(new InputStreamReader(byteArrayInputStream, "UTF-8"));
    }

    /*
    Inner Class
     */
    public class CachedBodyServletInputStream extends ServletInputStream {

        private InputStream cachedBodyInputStream;

        public CachedBodyServletInputStream(byte[] cachedBody) {
            this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
        }

        @SneakyThrows
        @Override
        public boolean isFinished() {
            return cachedBodyInputStream.available() == 0;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setReadListener(ReadListener readListener) {
        }

        @Override
        public int read() throws IOException {
            return cachedBodyInputStream.read();
        }
    }
}

 

Interceptor단

preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 메소드에 아래의 로직을 추가하였다. 

if (request.getContentType() != null && request.getContentType().contains("application/json")) {
    final CachedBodyHttpServletWrapper cachingRequest = (CachedBodyHttpServletWrapper) request;
    String requestBody;
    if ((requestBody = cachingRequest.getReader().readLine()) != null) {
        // S3로 업로드!
    }
} else {
	// Body 제외한 상태로 S3에 업로드!
}

 

끝!

 

참고

https://www.baeldung.com/spring-reading-httpservletrequest-multiple-times

+ Recent posts