λ³Έλ¬Έ λ°”λ‘œκ°€κΈ°

Server/Spring

[Spring] 둜그λ₯Ό μ–΄λ–»κ²Œ 남겨야 ν• κΉŒ?

πŸ“1단계 : HTTP Request / Response 둜그λ₯Ό μ–΄λ””μ—μ„œ μ²˜λ¦¬ν•΄μ•Ό ν• κΉŒ?

HTTP Request / Response λ‘œκ·ΈλŠ” 곡톡 μ²˜λ¦¬κ°€ ν•„μš”ν•œ μ˜μ—­μž…λ‹ˆλ‹€. 이λ₯Ό μ²˜λ¦¬ν•˜κΈ° μœ„ν•œ λ°©λ²•μœΌλ‘œ 주둜 μ‚¬μš©λ˜λŠ” 세가지가 μžˆμŠ΅λ‹ˆλ‹€.

 

1. Servlet Filter

Servlet Filter λŠ” Dispatcher Servlet 의 μ „ / 후에 λ™μž‘ν•˜λ©°, μ‚¬μš©μžμ˜ μš”μ²­μ΄λ‚˜ 응닡을 κ°€μž₯ λ¨Όμ € λ§ˆμ£Όν•©λ‹ˆλ‹€. ν•„ν„°λŠ” μŠ€ν”„λ§μ˜ 고유 κΈ°λŠ₯이 μ•„λ‹ˆλΌ μžλ°” μ„œλΈ”λ¦Ώμ—μ„œ μ œκ³΅ν•˜λŠ” κΈ°λŠ₯μž…λ‹ˆλ‹€. Filter λŠ” λ™μΌν•œ Servlet Container (e.g Tomcat) λ‚΄μ—μ„œ ν•„μš”ν•œ μžμ›λ“€μ„ ν™œμš©ν•˜μ—¬ λ™μž‘ν•©λ‹ˆλ‹€.

 

2. Handler Interceptor

Interceptor λŠ” Dispatcher Servlet 이 μ‹€ν–‰λœ ν›„ ν˜ΈμΆœλ©λ‹ˆλ‹€. Spring Context μ—μ„œ κ΄€λ¦¬λ˜κΈ° λ•Œλ¬Έμ— λͺ¨λ“  빈(Bean)에 접근이 κ°€λŠ₯ν•˜λ‹€λŠ” μž₯점이 μžˆμŠ΅λ‹ˆλ‹€.

 

3. AOP

Interceptor 와 filter λŠ” μ£Όμ†Œλ‘œ λŒ€μƒμ„ κ΅¬λΆ„ν•΄μ„œ κ±ΈλŸ¬λ‚΄μ•Ό ν•˜λŠ” 반면, AOP λŠ” μ£Όμ†Œ, νŒŒλΌλ―Έν„°, μ–΄λ…Έν…Œμ΄μ…˜ λ“± λ‹€μ–‘ν•œ λ°©λ²•μœΌλ‘œ λŒ€μƒμ„ 지정할 수 μžˆμŠ΅λ‹ˆλ‹€. AOP advice 와 Handler Interceptor 의 κ°€μž₯ 큰 차이점은 νŒŒλΌλ―Έν„°μ˜ μ°¨μ΄μ μž…λ‹ˆλ‹€. Advice 의 경우 JoinPoint λ‚˜ ProceedingJointPoint 등을 ν™œμš©ν•΄μ„œ ν˜ΈμΆœν•˜μ§€λ§Œ, HandlerInterceptor λŠ” Filter 와 μœ μ‚¬ν•˜κ²Œ HttpServletRequest, HttpServletResponse λ₯Ό νŒŒλΌλ―Έν„°λ‘œ μ‚¬μš©ν•©λ‹ˆλ‹€.

 

βœ… 결둠적으둜 μ €λŠ” Filter λ₯Ό μ„ νƒν•˜μ˜€μŠ΅λ‹ˆλ‹€.

HTTP μš”μ²­μ€ Spring 의 μ™ΈλΆ€ μ˜μ—­, 즉 μ›Ή μ„œλ²„ λ ˆμ΄μ–΄μ—μ„œ μ‹œμž‘λ©λ‹ˆλ‹€. λ§Œμ•½ Aspect μ‹€ν–‰ 전에 였λ₯˜κ°€ λ°œμƒν•˜λ©΄ HTTP μš”μ²­ 둜그λ₯Ό 남길 수 μ—†κ²Œ λ©λ‹ˆλ‹€. AOP와 Interceptor λŠ” Spring μ˜μ—­μ—μ„œ λ™μž‘ν•˜λ―€λ‘œ HTTP μš”μ²­/응닡 둜그λ₯Ό 남기기 μœ„ν•΄μ„œλŠ” Filter κ°€ μ ν•©ν•˜λ‹€κ³  νŒλ‹¨ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

 

πŸ“ 2단계 : μš”μ²­λœ HTTP 정보에 μ–΄λ–»κ²Œ μ ‘κ·Όν•΄μ•Ό ν• κΉŒ?

InputStream 은 1번만 읽을 수 μžˆμŠ΅λ‹ˆλ‹€. λ”°λΌμ„œ 이미 캐싱 κΈ°λŠ₯ κ΅¬ν˜„λœ ContentCachingRequestWrapper λ₯Ό μ‚¬μš©ν•˜κ±°λ‚˜ HttpServletRequestWrapper λ₯Ό μƒμ†ν•΄μ„œ 직접 캐싱 κΈ°λŠ₯을 κ΅¬ν˜„ν•˜λŠ” 방법을 선택할 수 μžˆμŠ΅λ‹ˆλ‹€.

 

1. ContentCachingRequestWrapper

이 ν΄λž˜μŠ€λŠ” getContentAsByteArray() λ©”μ„œλ“œλ₯Ό μ œκ³΅ν•˜μ—¬, body λ₯Ό μ—¬λŸ¬λ²ˆ 읽을 수 있게 ν•΄μ€λ‹ˆλ‹€. 이 λ©”μ„œλ“œλŠ” 슀트림이 μ†ŒλΉ„λœ μ‹œμ μ— μΊμ‹±λ˜κΈ° λ•Œλ¬Έμ—, ν•œ 번 read κ°€ λ˜μ–΄μ•Όλ§Œ 캐싱이 μ΄λ£¨μ–΄μ§‘λ‹ˆλ‹€.

 

문제 상황

Servlet Filter μ—μ„œ μš”μ²­μ„ 좜λ ₯ν•˜λ € ν–ˆμœΌλ‚˜, request body κ°€ 빈 κ°’μœΌλ‘œ λ‚˜μ˜€λŠ” λ¬Έμ œκ°€ λ°œμƒν•˜μ˜€μŠ΅λ‹ˆλ‹€.

val cachingRequest = ContentCachingRequestWrapper(request)
val cachingResponse = ContentCachingResponseWrapper(response)

try {
	logRequest(cachingRequest)
	filterChain.doFilter(cachingRequest, cachingResponse)
} finally {
	logResponse(cachingResponse)
	cachingResponse.copyBodyToResponse()
}

원인은 InputStream 이 read 된 μ‹œμ μ—μ„œ 캐싱이 μ΄λ£¨μ–΄μ§€λŠ”λ°, 이 μ½”λ“œμ—μ„œλŠ” InputStream 이 아직 μ½νžˆμ§€ μ•Šμ•˜κΈ° λ•Œλ¬Έμ— request body κ°€ λΉ„μ–΄ μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 이λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ doFilter() 이후에 request λ₯Ό 좜λ ₯ν•˜λ € ν–ˆμœΌλ‚˜, μ˜ˆμ™Έκ°€ λ°œμƒν•˜λ©΄ 둜그λ₯Ό λ³Ό 수 μ—†λŠ” λ¬Έμ œκ°€ λ°œμƒν•˜μ˜€μŠ΅λ‹ˆλ‹€. 

 

이 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ HttpServletRequestWrapper λ₯Ό 상속받아 RequestWrapper λ₯Ό μ»€μŠ€ν…€ν•˜μ—¬ μ‚¬μš©ν•˜μ˜€μŠ΅λ‹ˆλ‹€. 

 

2. HttpServletRequestWrapper

class RequestWrapper(request: HttpServletRequest) : HttpServletRequestWrapper(request) {
    private val cachedInputStream: ByteArray

    init {
        val requestInputStream = request.inputStream
        cachedInputStream = StreamUtils.copyToByteArray(requestInputStream)
    }

    override fun getInputStream(): ServletInputStream {
        return object : ServletInputStream() {
            private val cachedBodyInputStream = ByteArrayInputStream(cachedInputStream)

            override fun isFinished(): Boolean {
                return try {
                    cachedBodyInputStream.available() == 0
                } catch (e: IOException) {
                    e.printStackTrace()
                    false
                }
            }

            override fun isReady(): Boolean {
                return true
            }

            override fun setReadListener(readListener: ReadListener) {
                throw UnsupportedOperationException()
            }

            override fun read(): Int {
                return cachedBodyInputStream.read()
            }
        }
    }

}
 val cachingRequest = RequestWrapper(request)
 val cachingResponse = ContentCachingResponseWrapper(response)

 try {
	logRequest(cachingRequest)
	filterChain.doFilter(cachingRequest, cachingResponse)
} finally {
	logResponse(cachingResponse)
	cachingResponse.copyBodyToResponse()
	MDC.clear()
}