-
Spring에서 HttpServletRequest의 반복적 읽기 (feat. Filter에서는 request 교체가 가능한 이유)Spring 2023. 8. 22. 22:30
이전에 정리했던 내용을 리더님에게 피드백 받으면서 한가지 추가적인 피드백을 받게 되었다.
Interceptor와는 다르게 Filter에서는 request / response 값을 조작하는 것이 가능하다는 표를 넣었는데,
Http request를 다른 값으로 바꿔칠 수는 있지만 request의 내부 parameter값을 조작하는 것은 Filter에서라도 불가능 하다는 것을 추가로 배웠다.
또, 추가로 HttpServletRequest가 반복적으로 읽기가 불가능 하다는 피드백을 받아 추가 공부를 해보았다!
Filter에서는 request의 교체가 가능한 이유
먼저 Interceptor와 다르게 Filter에서 request 값 교체가 가능한 이유는 다음과 같다.
// Filter @Component class ExampleFilter : Filter { override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { // 다음 필터가 수행할 request를 교체해서 넣을 수 있음 chain.doFilter(request, response) } } // Interceptor class ExampleInterceptor : HandlerInterceptor { override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean { // Request/Response에 대한 변경은 불가능, boolean 값만 반환함 return true } }
Filter의 doFilter 의 경우ServletRequest request, ServletResponse response 받고 또한 필터 체이닝을 통해서 다음 필터에게 파라미터로 request, response 객체를 넘겨주는데, 이때 원하는 request/response 로 값을 교체하여 넣어줄 수 있다. 하지만 Interceptor의 경우 응답값을 boolean으로 형식으로만 돌려주고 있어 변경이 불가능하여 다른 request / response 객체를 넘겨줄 수 없다. Dispatcher Servlet이 여러 인터셉터 목록을 가지고 있고, for 문으로 순차적으로 Interceptor를 실행하고 반환되는 boolean 값이 true인 경우 다음 인터셉터나 컨트롤러를 수행하고, false인 경우 작업을 중단한다.
그렇다면 filter에서는 request로 넘어오는 파라미터(요청값)까지 수정이 가능할까?
HttpServletRequest의 파라미터 변경이 불가능한 이유
HttpServletRequest에는 getParameter()는 존재하지만 setParameter()는 존재하지 않는다. Parameter는 클라이언트에서 넘어온 값, Attribute는 요청의 처리 과정에서 필터, 서블릿, JSP 등에서 속성을 설정하고 공유하는 용도로 사용되는 값으로 서버에서 설정하거나 수정한 값을 더 다양한 범위에서 공유하기 위한 용도로 사용된다. 따라서, 이미 들어가 있는 parameter를 교체하기 위해서는 httpServletRequest를 전부 복사하여 다시 만들어 전달해야 한다.
HttpServletRequest의 반복적인 읽기가 불가능 한 이유
HttpServletRequest는 Java Servlet API의 일부로, 웹 애플리케이션에서 클라이언트로부터 받은 HTTP 요청 정보를 처리하는 데 사용되는 인터페이스로 서블릿이나 필터에서 이를 사용하여 클라이언트의 요청 정보를 처리하고 응답을 생성할 수 있다. . HttpServletRequest의 getInputStream()을 통해서 요청 본문을 읽을 수 있는데, InputStream 형식이기 때문에 데이터는 한번만 읽을 수 있다. 이로 인해 요청 데이터를 읽는 순간 이후에는 후속 핸들러 또는 필터에서 해당 데이터를 다시 읽을 수 없다.
그렇다면 어떻게 HttpServletRequest을 반복적으로 읽을 수 있을까?
HttpServletRequest의 반복적 읽기를 구현하는 방법
HTTP 요청의 본문 데이터를 반복해서 읽을 수 있도록 하는 방법은 다음과 같이 요청 데이터를 메모리나 임시 파일에 저장하는 것이다.
- 요청 데이터 캐싱
- HttpServletRequestWrapper
- 요청 데이터를 캐싱하고 여러 번 읽을 수 있는 새로운 HttpServletRequest를 생성하는 방법
- Spring에서 제공
- ex) Spring의 ContentCachingRequestWrapper
- 스트림을 메모리에 캐싱
- 요청의 본문 데이터를 메모리에 캐싱하는 방법
- ByteArrayOutputStream나 BufferedReader 등을 사용하여 요청 데이터를 읽어 메모리에 저장
- 요청 데이터가 큰 경우 메모리 부하가 발생할 수 있음
- 스트림을 임시 파일에 캐싱
- 요청 데이터를 임시 파일에 저장하여 여러 번 읽을 수 있도록 하는 방법
- java.io.File이나 java.nio.file.Path 등을 사용하여 임시 파일을 생성하고, 요청 데이터를 파일에 저장
- 메모리 부하를 줄일 수 있지만 파일 I/O 비용이 추가됨
구현 예시 (bealdung.com 참고)
먼저 HttpServletRequestWrapper를 확장하여 요청 데이터 반복 읽기를 구현할 수 있는 ReusableRequestWrapper를 구현한다.
여기서 CachedInputStream 클래스는 ServletInputStream을 확장하여 반복적으로 읽을 수 있는 스트림을 생성한다. read 메서드를 오버라이딩하여 캐시된 바이트 배열로부터 데이터를 읽고, isFinished 메서드와 isReady 메서드를 통해 반복 읽기를 위한 상태 정보를 제공한다.
class ReusableRequestWrapper(request: HttpServletRequest) : HttpServletRequestWrapper(request) { private val body: String init { // 요청 데이터를 문자열로 읽어 메모리에 캐시 body = BufferedReader(InputStreamReader(request.inputStream)) .lines() .collect(Collectors.joining(System.lineSeparator())) } @Throws(IOException::class) override fun getReader(): BufferedReader { // 캐시된 데이터로부터 Reader 생성 return BufferedReader(InputStreamReader(inputStream)) } override fun getInputStream(): ServletInputStream { // 캐시된 데이터로부터 InputStream 생성 return CachedInputStream(body.byteInputStream()) } } class CachedInputStream(private val cachedInputStream: ByteArrayInputStream) : ServletInputStream() { override fun read(): Int { return cachedInputStream.read() } override fun isFinished(): Boolean { return cachedInputStream.available() == 0 } override fun isReady(): Boolean { return true } override fun setReadListener(readListener: ReadListener) { throw UnsupportedOperationException("Not implemented") } }
이제 구현된 ReusableRequestWrapper를 이용하여 요청 데이터를 반복해서 읽을 수 있는 request로 교체할 수 있다.
class ReusableReadFilter : Filter { override fun init(filterConfig: FilterConfig) { // 필터 초기화 로직 } @Throws(IOException::class, ServletException::class) override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { if (request is HttpServletRequest) { // 요청 데이터를 반복해서 읽을 수 있는 요청으로 교체 val reusableRequest = ReusableRequestWrapper(request) chain.doFilter(reusableRequest, response) } else { chain.doFilter(request, response) } } override fun destroy() { // 필터 종료 로직 } }
💡 참고 ) Spring 에서 HttpServletRequest를 참조하는 방법
-1) RequestContextHolder를 통해 ThreadLocal로 저장된 HttpServletRequest 객체를 참조할 수 있음
Spring RequestContextHolder은 Spring 2.x부터 제공되던 기능으로 전 구간에서 HttpServletRequest에 접근할 수 있도록 도와준다. 여기서 보관하는 value는 HttpServletRequest 그 자체가 아니라 가공된 값을 의미하며 servlet 호출 시 HttpServletRequest값을 thread에 보관하고 (전 구간에서 같은 값을 나타냄) 꺼내쓰다가 servlet이 종료될 때 제거된다.
val httpServletRequest: HttpServletRequest = (RequestContextHolder.getRequestAttributes() as ServletRequestAttributes).request
- 2) @Autowired를 통해 RequestScope로 정의된 bean 참조
@Autowired private latenient var HttpServletRequest httpServletRequest
‼️ 주의 사항
HTTP session을 WAS간에 공유하기 위한 session clustering에서 정보가 많아지면 WAS에 부하가 증가하므로 RequestContextHolder을 활용할 때 session scope에 너무 많은 정보를 보관하면 안된다. thread 내에서 공통적으로 자주 사용되는 내용은 session scope이 아니라 requset scope을 사용하여 WAS에 무리를 주지 않고도 전 구간에서 접근이 가능하도록 해야한다.(참고한 사이트)
https://memo-the-day.tistory.com/177
https://www.baeldung.com/spring-reading-httpservletrequest-multiple-times
https://dveamer.github.io/backend/SpringRequestContextHolder.html
'Spring' 카테고리의 다른 글
SpringBatch) 스프링 배치 5의 변경점 (1) 2024.03.31 SpringBatch) 스프링 배치 간단 정리 (2) 2024.03.31 Transaction Propagation과 예외 전파 (0) 2023.02.12 [AWS + JENKINS + SONARQUBE] Spring 프로젝트 CI/CD 구현하기 3) CD 구현하기 ② (Sonarqube 설치 및 연동) (0) 2022.09.17 [AWS + JENKINS + SONARQUBE] Spring 프로젝트 CI/CD 구현하기 2) CD 구현하기 ① (Jenkins 배포) (0) 2022.09.17 - 요청 데이터 캐싱