HttpContentCache적용하여 body 재활용

BlackBean99·2023년 7월 21일
2

SpringBoot

목록 보기
19/20
post-thumbnail
post-custom-banner

내가 개발한 Whatnow 서버에서는 비동기적으로 슬랙으로 예외를 전송하고 있습니다.

ContentCachingRequest, ContentResponseWrapper 를 적용하여 body 를 래핑하여 다음 객체로 건내주면 된다고 생각했어요.

자 어떻게 적용하는지는 Document에 나와있습니다.

간단하게 적용해보았습니다. ContentCacheFilter를 만들어보겠습니다

@Component
class HttpContentCacheFilter : OncePerRequestFilter() {
    @Throws(ServletException::class, IOException::class)
    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        chain: FilterChain,
    ) {
        val wrappingRequest = ContentCachingRequestWrapper(request)
        val wrappingResponse = ContentCachingResponseWrapper(response)
        chain.doFilter(wrappingRequest, wrappingResponse)
        wrappingResponse.copyBodyToResponse()
    }
}

HttpServletRequest, Response 를 ContentCachingResponse로 래핑해서 사용하게 해줍니다.

이렇게 Wrapping 된 request, response 를 GlobalExceptionHandler 에서 사용해볼 수 있겠습니다.

    @ExceptionHandler(Exception::class)
    private fun handleException(
        e: Exception,
        request: HttpServletRequest?,
    ): ResponseEntity<ErrorResponse?> {
        logger.error("Exception", e)
        val cachingRequest = request as ContentCachingRequestWrapper
        val url = UriComponentsBuilder.fromHttpRequest(ServletServerHttpRequest(request))
            .build()
            .toUriString()

        val internalServerError = INTERNAL_SERVER_ERROR
        val errorResponse = ErrorResponse(
            internalServerError.status,
            internalServerError.code,
            internalServerError.reason,
            url,
        )
        slackProvider.execute(cachingRequest, e, userId)
        return ResponseEntity.status(HttpStatus.valueOf(internalServerError.status))
            .body<ErrorResponse>(errorResponse)
    }

override 해야하기 때문에 HttpServletRequest를 ContentCachingRequestWrapper 로 다시 캐스팅 해야합니다.
최종적으로 에러 핸들러에서 넘어오는 HttpRequest는 ContentCachingRequestWrapper 타입입니다.

그런데...

java.lang.ClassCastException: 
class org.springframework.web.filter.ForwardedHeaderFilter$ForwardedHeaderExtractingRequest 
cannot be cast to class org.springframework.web.util.ContentCachingRequestWrapper

왜 슬랙 알림이 안나갈까... 고민하다가..

이런 에러가 발생하는데 잘 보면 ForwardHeaderFilter 가 앞에서 먼저 동작하게 되면서 캐스팅 오류가 발생하면서 슬랙 에러 알림이 안날라가더라구요.. 이것도 모르고 계속 그냥 문제가 없네.. 생각하고 있었어요.

디버그를 계속 찍어보니까 Filter 순서가 안맞는 것이???

실제로 등록된 걸 보면 ServletFilter 가 먼저 originalChain 이 먼저 실행되고 그 뒤에 additionalFilters 가 실행이 되는데
img : FilterChainProxy$VirtualFilterChain

내부 additionalFilters에 등록된 필터에서는 ForwardedHeaderFilter 필터가 HttpContentCacheFilter 보다 앞에오게하면 된다고 생각했는데 당최 내가 선언적으로 filterConfig에 내가 작성한 순서대로 순서가 지정되지 않습니다.

그래서 이 스택오버 플로우의 의견를 보면 따로

https://stackoverflow.com/questions/25957879/filter-order-in-spring-boot

그래서 ServletFilterConfig 를 따로 설정을 해주는 작업을 해봤습니다.

@Configuration
@Profile("prod", "staging", "dev")
class ServletFilterConfig(
    val forwardedHeaderFilter: ForwardedHeaderFilter,
    val httpContentCacheFilter: HttpContentCacheFilter,
) : WebMvcConfigurer {
    @Bean
    fun securityFilterChain(
        @Qualifier(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
        securityFilter: Filter?,
    ): FilterRegistrationBean<Filter> {
        val registration: FilterRegistrationBean<Filter> = FilterRegistrationBean(securityFilter)
        registration.order = Int.MAX_VALUE - 3
        registration.setName(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
        return registration
    }

    @Bean
    fun setResourceUrlEncodingFilter(): FilterRegistrationBean<ResourceUrlEncodingFilter> {
        val registrationBean: FilterRegistrationBean<ResourceUrlEncodingFilter> = FilterRegistrationBean(ResourceUrlEncodingFilter())
        registrationBean.order = Int.MAX_VALUE - 2
        return registrationBean
    }

    @Bean
    fun setForwardedHeaderFilterOrder(): FilterRegistrationBean<ForwardedHeaderFilter> {
        val registrationBean: FilterRegistrationBean<ForwardedHeaderFilter> = FilterRegistrationBean(forwardedHeaderFilter)
        registrationBean.order = Int.MAX_VALUE - 1
        return registrationBean
    }

    @Bean
    fun setHttpContentCacheFilterOrder(): FilterRegistrationBean<HttpContentCacheFilter> {
        val registrationBean: FilterRegistrationBean<HttpContentCacheFilter> = FilterRegistrationBean(httpContentCacheFilter)
        registrationBean.order = Int.MAX_VALUE
        return registrationBean
    }
}

ForwardedHeaderFilter를 HttpContentCacheFilte의 앞으로 설정해주었습니다.
그랬더니 성공!!

근데 Security에 FilterConfig 로 설정해주면 좋을텐데 굳이 별도의 클래스를 나눠서 등록해야하나? 싶긴 한데 개선해볼 수 있을 것 같다.

프록시 환경에서는 필터 등록 순서가 달라질 수도 있다는 점을 알게 되었다.

reference
spring 프록시 환경에서 HttpContentCache적용

profile
like_learning
post-custom-banner

0개의 댓글