Spring Framework/Spring Boot

[Spring Boot] request, response 한 번만 읽어올 수 있는 제약 해결

장쫄깃 2022. 4. 19. 13:05
728x90


들어가며


프로젝트 진행 중 Client, Server Filter에서 Request, Response을 자동으로 암/복호화해주는 로직 개발을 맡았다. 자세한 내용은 아래 흐름도를 참고하면 된다.

 

그런데 개발 진행 중 문제가 발생했다. Request, Response 값을 한번 읽으면 다시 사용할 수 없는 문제였다. Request 값을 암호화 후 전송할 경우 빈 값이 전송되고, Response 값을 복호화할 경우 최종적으로 빈 값이 수신되었다. 해당 문제에 대한 트러블슈팅 과정에서 배운 점과 해결 방법에 대해서 설명해보려고 한다.

 

 

HttpServletRequest, HttpServletResponse의 InputStream


문서를 보면 해당 현상에 대한 설명이 있다.

If the parameter data was sent in the request body, such as occurs with an HTTP POST request, then reading the body directly via getInputStream() or getReader() can interfere with the execution of this method.

참조 : https://docs.oracle.com/javaee/6/api/javax/servlet/ServletRequest.html#getParameter(java.lang.String)

 

해당 내용은 getParameter(), getInputStream(), getReader() 메소드에 각각 적혀있다. 내용대로 한번 값을 요청한 후에 재요청을 할 경우 IllegalStateException을 발생시킨다. 이유가 무엇일까?

 

Request, Response 값을 확인하는 과정에서 getParameter(), getInputStream(), getReader() 등의 메소드를 사용할 경우 내부 값을 stream 방식으로 가져오게 되고, 이 단계가 종료되면 stream close 가 되기 때문에 더 이상 값을 가져올 수 없게 된다.

java.lang.IllegalStateException: getReader() has already been called for this request org.springframework.http.converter.HttpMessageNotReadableException: Could not read JSON: Stream closed; nested exception is java.io.IOException: Stream closed

때문에 Request, Response의 InputStream을 한 번 읽으면 다시 읽을 수 없다.

 

해당 링크에서 소스코드를 보며 더 상세한 정보를 확인할 수 있다.

// Request#getReader() 중..
if (usingInputStream) {
	throw new IllegalStateException(sm.getString("coyoteRequest.getReader.ise"));
}

 

 

해결방법


문제를 해결하는 방법은 2가지가 있다. 첫째, Wrapper 클래스로 한번 감싸준 후 반환하는 방법이다. 둘째, ContentCachingRequestWrapper/ContentCachingResponseWrapper 를 사용하는 방법이다.

 

1. Wrapper

Wrapper 클래스를 만들어서 요청했던 데이터를 감싸준 후 캐싱하여 여러번 읽을 수 있도록 한다.

public class CachingRequestWrapper extends HttpServletRequestWrapper {
	private final Charset encoding;
	private byte[] rawData;

	public CachingRequestWrapper(HttpServletRequest request) throws IOException {
		super(request);

		String characterEncoding = request.getCharacterEncoding();
		if (StringUtils.isEmpty(characterEncoding)) {
			characterEncoding = StandardCharsets.UTF_8.name();
		}
		this.encoding = Charset.forName(characterEncoding);

		try (InputStream inputStream = request.getInputStream()) {
			this.rawData = IOUtils.toByteArray(inputStream);
		}
	}

	@Override
	public ServletInputStream getInputStream() {
		return new CachedServletInputStream(this.rawData);
	}

	@Override
	public BufferedReader getReader() {
		return new BufferedReader(new InputStreamReader(this.getInputStream(), this.encoding));
	}


	private static class CachedServletInputStream extends ServletInputStream {
		private final ByteArrayInputStream buffer;

		public CachedServletInputStream(byte[] contents) {
			this.buffer = new ByteArrayInputStream(contents);
		}

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

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

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

		@Override
		public void setReadListener(ReadListener listener) {
			throw new UnsupportedOperationException("not support");
		}
	}
}

 

2. ContentCachingRequestWrapper/ContentCachingResponseWrapper

이미 스프링에서 구현해놓은 Wrapper 클래스를 Filter 혹은 Interceptor에서 사용하는 방법이다.

@WebFilter(urlPatterns = "/some/*")
public class SomeCustomFilter implements Filter {
	public FilterConfig filterConfig;

	public void init(FilterConfig filterConfig) throws ServletException {
		this.filterConfig = filterConfig;
	}
    
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    	throws IOException, ServletException {
		// ... before filter
		
		ContentCachingResponseWrapper responseCacheWrapperObject = new ContentCachingResponseWrapper((HttpServletResponse) response);
		chain.doFilter(request, responseCacheWrapperObject);
		
		// ... after filter
	}
    
	public void destroy() {
		this.filterConfig = null;
	}
}

필자는 1번과 2번 방법을 각각 Request, Response에 적용하여 개발했다.

 

API 암/복호화 관련 소스코드는 깃허브를 참고하면 된다.

클라이언트 소스 링크 : https://github.com/JangDaeHyeok/spring_api_client

 

GitHub - JangDaeHyeok/spring_api_client: Spring API 암/복호화 통신 - 클라이언트

Spring API 암/복호화 통신 - 클라이언트. Contribute to JangDaeHyeok/spring_api_client development by creating an account on GitHub.

github.com

서버 소스 링크 : https://github.com/JangDaeHyeok/spring_api_server

 

GitHub - JangDaeHyeok/spring_api_server: Spring API 암/복호화 통신 - 서버

Spring API 암/복호화 통신 - 서버. Contribute to JangDaeHyeok/spring_api_server development by creating an account on GitHub.

github.com

 

728x90