본문 바로가기

프로그래밍/Spring

Xplatform과 Spring Framework 연동 템플릿으로 보는 HandlerMethodArgumentResolver와 ViewResolver (3) - HttpServletRequestWrapper

이번 글에서는 이번 템플릿에서 사용하게 될 HttpServletRequestWrapper 클래스에 대해 얘길 해볼까 한다. 이것도 연재하는 내용을 알기 위한 선제지식 성격이 강하긴 한데 이전 글에서 언급하기에는 내용이 긴데다가 이 내용은 이번 템플릿에서뿐만 아니라 다른 상황에서도 쓰일 성격이 다분히 존재하기 때문에 글을 별도로 할당해서 쓰도록 하겠다.

 

Xplatform 같은 Ria Tool 뿐문 아니라 웹브라우저와 같은 웹 클라이언트는 WAS에 접속해서 데이터를전송하게 되면 HttpServletRequest 객체가 생성이 된다. 엄밀하게 말하면 통신이기 때문에 header 정보와 body가 있게 된다.  HttpServletRequest 객체에서 header 정보는 언제든 읽어올 수 있지만 body는 일단 한번 읽으면 다시 읽을수가 없다. 생각해보면 내가 보낸 통신이란 하나의 흐름을 그 상태 그대로 다시 재현할 수는 없는것이다. 같은 내용을 다시 보내면 보내는 것이지 그때의 그 상태 그대로를 재현할 수는 없는 것이다. 이러한 상황은 Spring을 이용한 Web 개발을 할 때도 만나게 되는데 우리가 Restful 프로그래밍을 할때 사용하는 어노테이션으로 @RequestBody 란 어노테이션을 사용하게 된다. 이 어노테이션이 하는 역할은 HttpServletRequest 객체에 있는 Request Body를 읽어서 이를 어노테이션이 붙은 변수에 저장하게 된다. 근데 이 어노테이션을 사용할 때 주의사항이 있다. @RequestBody 어노테이션을 두 번 이상 사용할 수 없다는 것이다. 다음의 예를 보자

 

@RequestMapping("/restfulTest")
public void restfulTest(@RequestBody String requestBody1
						, @RequestBody String requestBody2) {
...		
}

 

다음의 예는 메소드의 파라미터에 String 변수 2개를 사용하고 있는데 이 2개 모두 @RequestBody 어노테이션을 사용하고 있다. 이럴 경우에는 "Required request body is missing" 이란 메시지를 가진 HttpMessageNotReadableException 예외가 발생하게 된다. 이유는 첫번째 변수인 requestBody1 변수에서는 정상적으로 Request Body를 읽게 되지만 requestBody2 변수에서는 이미 requestBody1 변수에서 Request Body를 읽어버렸기 때문에 위에서 설명한 이유로 인해 읽어올 수 없어서 이러한 예외가 발생하게 된다. 그러면 어떻게 하면 Request의 Body를 여러번 읽어올 수 있을까? Request Body에 있는 내용을 어딘가에 저장해 놓은 뒤에 다시 읽을때는 Request Body에 있는 내용을 읽는게 아니라 저장해 놓은 곳에서 읽어오는 방법으로 이 상황을 풀어볼 수 있다. 이때 사용되는 것이 Wrapper 클래스이다.

 

Java Servlet Spec 에서는 HttpSerlvetRequest 인터페이스를 구현하고 이를 서브클래스 형태로 상속받아 만들수 있는 HttpServletRequestWrapper 클래스를 제공한다. 이 클래스를 상속받아서 지금 말했던 재사용을 구현 할 수 있다. 이 HttpServletRequestWrapper 클래스를 상속받아 만든 소스는 github에 있으니 여기에서 전체 소스를 볼 수 있으므로 이 글에서는 설명을 위해 부분부분을 보이면서 설명하도록 하겠다. 다음의 소스는 HttpRequestWrapper 클래스에서 알아두어야 할 부분만 남겨놓은 소스이다.

 

public class HttpRequestWrapper extends HttpServletRequestWrapper {

	private byte[] bodyData;
	
	public HttpRequestWrapper(HttpServletRequest request) throws IOException {
		super(request);
		// TODO Auto-generated constructor stub
		InputStream is = request.getInputStream();
		bodyData = StreamUtils.copyToByteArray(is);
	}

	@Override
	public ServletInputStream getInputStream() throws IOException {
		// TODO Auto-generated method stub
		final ByteArrayInputStream bis = new ByteArrayInputStream(bodyData);
		return new ServletImpl(bis);
	}
}

class ServletImpl extends ServletInputStream {

	private InputStream is;

	public ServletImpl(InputStream bis) {
		is = bis;
	}

	...
}

 

이 클래스에서는 2가지를 봐야 하는 것이 있는데, 하나는 HttpRequestWrapper 클래스의 멤버변수로 있는 byte 배열 변수인 bodyData이고, 또 하나는 이 클래스에서 다시 만들고 있는 ServletImpl 클래스이다. bodyData 변수가 하는 역할은 Web 클라이언트가 보낸 HttpServletRequest의 Request Body 부분을 저장해놓게 된다. HttpRequestWrapper 클래스의 생성자를 보면 Web Client가 보낸 HttpServletRequest 객체를 받아 getInputStream 메소드를 통해 body 부분을 Stream으로 읽어온뒤 이를 bodyData 부분에 복사하고 있다. 이런 식으로 저장해놓으면 언제든 해당 요청에 대한 body 부분을 이 변수를 필요한 시점마다 읽어와서 Request의 Body 부분을 여러번 읽어들여 재활용 할 수가 있게 된다. 이 bodyData를 어떻게 읽어오는가에 대한 고민을 하게 될텐데 HttpRequestWrapper 클래스의 getInputStream 메소드를 호출하는 시점에 이 bodyData를 return 해주면 된다. getInputStream 메소드가 Request의 Body 부분을 Stream 형태로 return 해주면 되는 것이기 때문에 그 형태에 맞춰서 bodyData 변수를 return 해주면 되는 것이다. 그래서 이 클래스의 getInputStream 메소드를 보면 bodyData 변수를 이용해서 Stream 형태의 객체를 만들어서 return 하게 되는데 바로 그 객체의 클래스가 ServletImpl 클래스이다. 요청할때 마다 새로 객체를 생성해서 return 하기 때문에 요청하는 스레드들간에 bodyData가 공유가 되지 않으므로 thread-safe 한 구조를 가지고 있는 형태가 된다. 그래서 이 Request Input Stream 을 읽어오는 입장에서는 bodyData가 있는 ServletImpl 클래스 객체를 통해서 작업을 진행하게 된다.

 

이렇게 만든 HttpRequestWrapper 클래스를 어떻게 사용할까? HttpRequestWrapper 클래스가 HttpServletRequest 객체를 변환하는 역할을 하는 것은 맞지만 이 글을 읽어보면 한가지 궁금증이 생길것이다. 

 

HttpRequestWrapper 클래스 객체를 어디서 생성하게 되는 거지?

 

이 클래스를 만들었다고 Spring에서 HttpServletRequest 객체가 HttpRequestWrapper 클래스 객체로 변환되는 것이 아니다. Spring MVC에서 Controller 클래스에서 HttpServletRequest 객체가 들어오기 전에 HttpServletRequest 객체가 HttpRequestWrapper 클래스 객체로 변환되어서 사용되는 것이다. 그러면 이러한 역할을 어디서 하는 걸까? web.xml의 filter에서 이러한 역할을 하게 된다. 여기서는 HttpServletRequestWrapperFilter 클래스를 filter 클래스로 만들어서 사용하고 있다. web.xml에서 filter 설정을 보면 다음과 같이 되어 있다.

 

<filter> 
	<filter-name>httpServletRequestWrapperFilter</filter-name>
	<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
	<filter-name>httpServletRequestWrapperFilter</filter-name>
	<url-pattern>*.do</url-pattern>
</filter-mapping>

 

여기서 보면 HttpServletRequestWrapperFilter 클래스를 사용하고 있지 않다. filter 이름은 HttpServletRequestWrapperFilter 라고 사용하고 있지만 이것은 어디까지나 filter 이름을 지정하는 것이기 때문에 아무 이름이나 가능하고 실질적으로는 filter-class 태그에 설정된 클래스가 filter 로 사용되는 것인데 여기서는 HttpServletRequestWrapperFilter 클래스를 사용하고 있는게 아니라 org.springframework.web.filter.DelegatingFilterProxy 클래스를 사용하고 있다. 왜 이런 설정을 하는 것일까?

 

Filter 인터페이스는 Spring과는 무관한 인터페이스이다. Filter 인터페이스는 Filter 인터페이스가 실행이 되는 WAS가 제어하는 것이지 Spring이 제어하는 것이 아니다. Spring이 제어하는 것은 Spring Bean으로 등록된 클래스이기 때문이다. 물론 Spring Bean으로 등록할 수도 있다. Spring Bean으로 등록할 경우의 이점은 Spring Bean으로 등록된 다른 Bean class 객체를 DI를 할 수 있기 때문이다. DI가 가능한 클래스는 Spring Bean으로 등록된 클래스만 가능하기 때문에 Filter 구현 클래스도 Spring Bean으로 등록해야 한다. 약간 설명이 장황했는데 정리하자면..

 

  1. Filter 구현 클래스가 Spring Bean으로 등록된다
  2. web.xml에서 Spring Bean 으로 등록된 Filter 구현 클래스를 사용한다

 

요렇게 된다. 그러면 web.xml에서 직관적으로 Spring Bean으로 등록된 Filter 구현 클래스를 사용할 수 있을까? 그렇지가 않다. 왜냐면 Spring Bean은 Context 안에서 등록되어져서 움직이기 때문에 Context에서 Filter 구현 클래스를 찾아서 등록되어져야 하기 때문이다. 이로 인해 Spring에서는 이러한 역할을 해주는 Proxy Class가 존재하게 되는데 그 역할을 하는 것이 DelegatingFilterProxy 클래스이다. 이 클래스가 Spring Context에서 Spring Bean으로 등록된 Filter 구현 클래스를 찾아 Filter로 등록해주게 된다.  이러한 과정이기 때문에 주의할 점이 하나 존재하게 되는데 그것은 Root Context와 Servlet Context로 분리해서 Spring Bean을 등록할 경우 Root Context에 Bean을 등록해야 한다는 것이다. Filter 구현 클래스이기 때문에 Servlet Context에 이를 등록하려 하겠지만 Servlet Context는 filter에 대한 설정이 마쳐진 뒤에 Servlet Context가 구현되어지기 때문에 DelegatingFilterProxy에서는 Filter 구현 클래스 Spring Bean을 찾을수 없게 된다. 때문에 비록 Filter 구현 클래스이지만 Root Context에 이를 등록하여야 제대로 된 동작이 이루어진다. 그러면 이 클래스를 Root Context에서 어떻게 등록했을까? 이 클래스의 소스를 보면 @Service 어노테이션이 붙어있다. 프로젝트의 전체적인 설정을 보면 @Service 어노테이션이 붙은 클래스는 Root Context에 등록되도록 했기 때문에 이 클래스에 @Service 어노테이션을 붙이면 Root Context에 Spring Bean이 등록되는 것이다.

 

이번에는 HttpServletRequest 클래스의 Wrapper 클래스인 HttpRequestWrapper 클래스의 제작 이유와 이를 설정하는 방법에 대해 얘기했다. 사전지식은 이정도로 마치고 다음 글부터는 본격적인 내용에 들어가도록 하겠다.

 

 1. Xplatform과 Spring Framework 연동 템플릿으로 보는 HandlerMethodArgumentResolver와 ViewResolver (1) - 개요

 2. Xplatform과 Spring Framework 연동 템플릿으로 보는 HandlerMethodArgumentResolver와 ViewResolver (2) - 사전지식

 3. Xplatform과 Spring Framework 연동 템플릿으로 보는 HandlerMethodArgumentResolver와 ViewResolver (3) - HttpServletRequestWrapper

 4. Xplatform과 Spring Framework 연동 템플릿으로 보는 HandlerMethodArgumentResolver와 ViewResolver (4) - Spring Controller에서 하려는 것

 5. Xplatform과 Spring Framework 연동 템플릿으로 보는 HandlerMethodArgumentResolver와 ViewResolver (5) - HandlerMethodArgumentResolver 분석(1)

 6. Xplatform과 Spring Framework 연동 템플릿으로 보는 HandlerMethodArgumentResolver와 ViewResolver (6) - HandlerMethodArgumentResolver 분석(2)

 7. Xplatform과 Spring Framework 연동 템플릿으로 보는 HandlerMethodArgumentResolver와 ViewResolver (7) - HandlerMethodArgumentResolver 분석(3)

 8. Xplatform과 Spring Framework 연동 템플릿으로 보는 HandlerMethodArgumentResolver와 ViewResolver (8) - HandlerMethodArgumentResolver 분석(4)

 9. Xplatform과 Spring Framework 연동 템플릿으로 보는 HandlerMethodArgumentResolver와 ViewResolver (9) - XplatformView 분석

 10. Xplatform과 Spring Framework 연동 템플릿으로 보는 HandlerMethodArgumentResolver와 ViewResolver (10) - 예외처리