일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- aspect
- network
- 트랜잭션
- 스프링부트
- spring boot
- SQL
- mybatis
- 객체지향프로그래밍
- request
- 자바
- response
- proxy pattern
- 스프링
- git
- Redis
- 스프링 시큐리티
- MYSQL
- java
- Spring
- 관점지향프로그래밍
- aop
- http
- 인터셉터
- RestControllerAdvice
- 디자인패턴
- Interceptor
- exception
- Spring Security
- OOP
- Filter
- Today
- Total
장쫄깃 기술블로그
[Spring Boot] request, response 한 번만 읽어올 수 있는 제약 해결 본문
[Spring Boot] request, response 한 번만 읽어올 수 있는 제약 해결
장쫄깃 2022. 4. 19. 13:05
들어가며
프로젝트 진행 중 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
서버 소스 링크 : https://github.com/JangDaeHyeok/spring_api_server
'Spring Framework > Spring Boot' 카테고리의 다른 글
[Spring Boot] 등록되는 빈의 순서를 정하자 (@DependsOn) (0) | 2022.06.26 |
---|---|
[Spring Boot] 의존성 주입 시 Bean이 여러개라면? (@Primary, @Qualifier) (0) | 2022.06.26 |
[Spring Boot] AOP 용어 (2) | 2022.04.15 |
[Spring Boot] AOP(Aspect Oriented Programming) 란? (0) | 2022.04.15 |
[Spring Boot] AOP 설정 (0) | 2022.04.15 |