Developer Sang Guy

HttpServletRequest Body 여러 번 읽기 본문

Spring

HttpServletRequest Body 여러 번 읽기

은크 2022. 12. 28. 13:07

HttpServletRequest는 본문을 읽기 위해 getInputStream() 메서드를 노출하는 인터페이스입니다.

기본적으로 InputStream의 데이터는 한 번만 읽을 수 있습니다 .

https://www.baeldung.com/spring-reading-httpservletrequest-multiple-times

 

Spring을 사용하여 여러 Handler를 개발하였을 경우 보통 Interceptor나 Filter를 사용하여 공통적으로 수행하는 요청 로그 출력 같은 처리를 진행한다.

그런데 막상 Interceptor 또는 Filter로 요청 본문을 읽어들여 로깅 처리를 한 다음 Controller에서 다시 한번 요청 본문을 읽으려고 하면 request.getInputstream 안에 데이터가 없어 본문은 읽을수 없는 상황이 발생한다.

 

해당 이슈는 HttpServletRequest 객체의 getInputStream()의 데이터는 한번 밖에 읽을 수 없기 때문이다.

(Content-Type : application/x-www-form-urlencoded 일 경우 사용하는 getParameter... 관련 메소드는 여러번 읽을 수 있다.)

 

HttpServletRequest의 getInputStream() 메소드를 여러 번 사용할 수 있는 방법을 공유하려한다.

 

Spring의 ContentCachingRequestWrapper

Spring은 ContentCachingRequestWrapper 클래스를 제공한다.

이 클래스에는 본문을 여러 번 읽을 수 있는 getContentAsByteArray() 메서드가 구현되어있다.
하지만 이 클래스에는 제한이  있어 getInputStream() 및 getReader() 메서드를 사용하여 본문을 여러 번 읽을 수 없다.

이 클래스는 InputStream을 사용하여 요청 본문을 캐시한다.

필터 중 하나에서 InputStream 을 읽으면 필터 체인의 다른 후속 필터에서 더 이상 본문을 읽을 수 없다.

이러한 제한으로 이 클래스를 사용하는 것만으로는 이슈를 해소할 수 없다.

이 제한을 해소하기 위하여 해당 클래스의 몇몇 메소드를 아래와 같이 재구현을 해야 한다.

public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {

	private byte[] cachedBody;

	public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {

		super(request);
		InputStream is = request.getInputStream();
		this.cachedBody = StreamUtils.copyToByteArray(is);
	}

	@Override
	public ServletInputStream getInputStream() throws IOException {

		return new CachedBodyServletInputStream(this.cachedBody);
	}

	@Override
	public BufferedReader getReader() throws IOException {

		ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
		return new BufferedReader(new InputStreamReader(byteArrayInputStream));
	}
}

- 생성자 : 사용하여 HttpServletRequest의 본문을 읽고 byte[] cachedBody에 저장

- getInputStream() : cachedBody를 담은 CacheBodyServletInputStream을 반환

- getReader() : byteArrayInputStream(cachedBody)을 담은 BufferedReader를 반환

 

ServletInputStream 구현

ServletInputStream을 구현한 CachedBodyServletInputStream를 생성하여 생성자를 만들고 isFinished() , isReady()  read() 메서드를 재정의한다.

public class CachedBodyServletInputStream extends ServletInputStream {

	private InputStream cachedBodyInputStream;

	public CachedBodyServletInputStream(byte[] cachedBody) {

		this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
	}

	@Override
	public boolean isFinished() {

		try {

			return cachedBodyInputStream.available() == 0;

		} catch (IOException e) {
			e.printStackTrace();
		}

		return false;
	}

	@Override
	public boolean isReady() {

		return true;
	}

	@Override
	public int read() throws IOException {

		return cachedBodyInputStream.read();
	}

	@Override
	public void setReadListener(ReadListener listener) {

	}

- 생성자 : 매개 변수로 받은 cachedBody를 담은 ByteArrayInputStream의 인스턴스를 생성하여 전역 변수 cachedBodyInputStream에 할당한다.

 

- isFinished() : InputStream에 더이상 읽을 데이터가 남았는지 여부를 확인한다.

읽은 데이터가 0 일 경우 true를 반환한다.

 

- isReady() : InputStream을 읽은 준비가 되었는지 여부를 확인한다.

이미 생성자를 통하여 ByteArrayInputStream을 할당하여 항상 사용 가능 상태를 알리는 true로 반환한다.

 

- read() : 전역 변수 cachedBodyInputStream을 읽어들인다.

 

필터

CachedBodyHttpServletRequest 클래스를 사용하기 위한 필터를 생성해야 한다.

여기서 우리는 Spring의 OncePerRequestFilter 클래스를 확장할 CachedBodyHttpServletRequest  클래스를 생성할 것이다.

OncePerRequestFilter에서는 추상 메소드 doFilterInternal()를 제공하며 해당 메소드를 통하여 1회 1요청에 1회 호출을 보장하는 필터를 구현할 수 있다.

public class ContentCachingFilter extends OncePerRequestFilter {

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		System.out.println("ContentCachingFilter doFilterInternal Method");
		CachedBodyHttpServletRequest cachedBodyHttpServletRequest = new CachedBodyHttpServletRequest(request);
		filterChain.doFilter(cachedBodyHttpServletRequest, response);
	}
}

- doFilterInternal() : CachedBodyHttpServletRequest의 인스턴스를 생성하여 필터 체인에 등록한다.

 

필터 빈 생성

@Configuration
public class HttpRequestDemoConfig implements WebMvcConfigurer {

	@Bean
	public FilterRegistrationBean<ContentCachingFilter> logFilter() {
		FilterRegistrationBean<ContentCachingFilter> registrationBean = new FilterRegistrationBean<>();
		registrationBean.setFilter(new ContentCachingFilter());
		registrationBean.addUrlPatterns("/*");
		registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
		return registrationBean;
	}

	@Override
	public void addInterceptors(InterceptorRegistry registry) {

		registry.addInterceptor(new RequestInterceptor());
	}
}

- logFilter() : ContentCachingFilter를 빈으로 생성한다.

필터 체인 내 앞선 필터에 영향을 받거나 주지 않기 위해 setOrder을 사용하여 가장 마지막에 호출되는 필터로 세팅한다.

 

- addInterceptors() : 테스트 용 인터셉터 등록

 

테스트

public class RequestInterceptor implements HandlerInterceptor {

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		System.out.println("Interceptor Request Body : " + StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8));
		return true;
	}
}
@RestController
public class HomeController {
	
	@PostMapping("/home")
	public String getHome(HttpServletRequest request) throws Exception {
		
        System.out.println("Controller Request Body 1 : " + StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8));
        System.out.println("Controller Request Body 2 : " + StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8));
		return "This is Home";
	}
}

 

필터 적용

ContentCachingFilter doFilterInternal Method
Interceptor Request Body : {"a" : "123"}
Controller Request Body 1 : {"a" : "123"}
Controller Request Body 2 : {"a" : "123"}

 

필터 미적용

Interceptor Request Body : {"a" : "123"}
Controller Request Body 1 : 
Controller Request Body 2 :
Comments