API서버 RequestBody 여러번 읽는방법

2023. 2. 7. 11:08개인노트

반응형

기본적으로 Request내에 있는 inputStream의 경우 1회만 읽을 수 있다. 로그성 데이터를 남기기 위해서 한번 읽어버리면 이후 로직에서 데이터를 읽지못하는 경우가 발생한다.

예를들어 API서버 만들었는데 요청과 응답에 대한 로그성 데이터를 남기고 싶었다.

그런경우 Interceptor에서 해당 바디에 있는 데이터를 읽어와서 String으로 변환하여 로그 데이터를 남겼고 그 이후 컨트롤러로 진입하였을때 바디에 있는 정보를 이미 앞에서 읽었기 때문에 데이터가 없는 경우가 발생했다.

 

아래와 같이 인터셉터에서 요청 바디와, 요청 파람, 응답 바디 등을 받아서 로그성 데이터를 남기고 싶다면 추가적으로 필터를 추가하고 요청에 대한 Cache처리가 필요했다.

public class TestInterceptor extends HandlerInterceptorAdapter {

	@Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
    
    		String payload = getReqBody(request);
            String requestParam = requestToParamString(request);
    }
	
	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
		/** 응답(String) */
		String responseBody = getResponseBody(response);
    }
    
    /**
	 * Response 응답정보를 저장하기위해 String으로 가공처리
	 * @param response
	 * @return String
	 * @throws IOException
	 */
    private String getResponseBody(final HttpServletResponse response) throws IOException {
        String payload = null;
        ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        if (wrapper != null) {
            wrapper.setCharacterEncoding("UTF-8");
            byte[] buf = wrapper.getContentAsByteArray();
            if (buf.length > 0) {
                payload = new String(buf, 0, buf.length, wrapper.getCharacterEncoding());
            }
        }
        return null == payload ? "" : payload;
    }
    
    public static String requestToParamString(HttpServletRequest request, String prefix) {
		String				returnString	= "";
		Enumeration<String>	en				= request.getParameterNames();
		String				param			= null;
		String				value			= null;


		while(en.hasMoreElements()) {
			param = en.nextElement();
			value = request.getParameter(param);

			if (!StringUtils.contains(param, "Pwd")) {
				if (StringUtils.isNotBlank(value)) {
					if (StringUtils.isBlank(prefix)) {
						returnString += param + "=" + value + "||";
					} else {
						if (StringUtils.startsWith(param, prefix)) {
							returnString += param + "=" + value + "||";
						}
					}
				}
			}
		}


		return returnString;
	}
}

 

 

캐싱에 사용되는 클래스 생성

CachedBodyHttpServletRequest
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {

    private byte[] cachedBody;

    private Map<String, String[]> parameterMap;

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

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

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

    @Override
    public Map<String, String[]> getParameterMap() {
        return this.parameterMap;

    }
}
 
CachedBodyServletInputStream
public class CachedBodyServletInputStream extends ServletInputStream {

    private InputStream cachedBodyInputStream;

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

    @Override
    public boolean isFinished() {
        try {
            return cachedBodyInputStream.available() == 0;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    @Override
    public boolean isReady() {
        return true;
    }

    @Override
    public void setReadListener(ReadListener listener) {

    }

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

요청에 대해 한번만 적용되는 필터 생성

/**
 * 응답시 ResData를 로그로 남기기위해 사용하는 Wrapping Filter
 */
@Slf4j
public class ServletWrapperFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {
        // reponse를 ContentCachingResponseWrapper 객체로 래핑
        ContentCachingResponseWrapper httpServletResponse = new ContentCachingResponseWrapper(response);

        log.debug("ServletWrapperFilter 필터통과확인");
        CachedBodyHttpServletRequest cachedBodyHttpServletRequest = new CachedBodyHttpServletRequest(request);
        filterChain.doFilter(cachedBodyHttpServletRequest, httpServletResponse);

        /** interceptor에서 throws 해서 응답하는 경우 body를 복사처리가 필요함 */
        ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(httpServletResponse, ContentCachingResponseWrapper.class);
        wrapper.copyBodyToResponse();

    }


}

 

마지막으로 스프링에서 사용하는 필터에 등록을 해주면된다.

@Configuration
public class MvcConfig implements WebMvcConfigurer {

	@Bean
	public FilterRegistrationBean filterBean(){
		FilterRegistrationBean registrationBean = new FilterRegistrationBean(new ServletWrapperFilter());
		registrationBean.setOrder(1); //필터 여러개 적용 시 순번
        registrationBean.addUrlPatterns("/*"); //전체 URL 포함

		return registrationBean;
	}

}
반응형