본문 바로가기

프로그래밍/Spring

Spring Framework의 메시지 컨버터를 이용한 FileDownload 구현(2)

일단 메시지 컨버터가 무엇인지에 대한 설명이 필요할것 같다. 일반적으로 Spring MVC를 이용해서 컨트롤러에 값을 주고 받을때는 HTTP 요청 프로퍼티를 분석하여 그에 따라 특정 클래스로 바인딩이 되게끔 하고 컨트롤러에서 특정 객체를 Model Object에 집어 넣어 View를 리턴하는 식으로 주고받게 된다. 그러나 메시지 컨버터는 그런 개념이 아니라 HTTP 요청 메시지 본문과 HTTP 응답 메시지 본문을 통째로 하나의 메시지로 보고 이를 처리한다. Ajax를 통해 XML이나 JSON 문자열을 주고 받을때를 보면 짐작할수 있을 것이다. Spring MVC에서 이러한 작업을 하는데 사용되는 어노테이션이 바로 @RequestBody와 @ResponseBody이다. @RequestBody를 이용하려 파라미터를 받으면 HTTP 요청 메시지 본문을 분석하는 것이 아닌 그냥 하나의 통으로 받아서 이를 적절한 클래스 객체로 변환하게 되고 @ResponseBody를 사용하여 특정 값을 리턴하면 View 개념이 아닌 HTTP 응답 메시지에 그 객체를 바로 쏴주는 개념으로 리턴하게 되는 것이다.

Spring MVC에서 이러한 메시지 컨버터를 통해 다룰수 있는 데이터 형태는 바이너리 데이터를 다루기 위한 byte [], String 형태, Form 데이터를 처리하기 위한 MultiValueMap<String, String>, XML, JSON이다. XML의 경우는 세부적으로 DOM 또는 SAX 방식의 XML 처리 방법, JaxB2 방식의 XML 처리 방법, Marshaller 또는 Unmarshaller 스타일의 OXM 기술을 활용하는 XML 처리 방법으로 세분화 되어 있다. 그래서 내가 의도하는 File을 처리할려면 Spring에서 제공하는 메시지 컨버터가 아닌 별도의 메시지 컨버터를 추가로 만들어서 이를 Spring MVC에서 사용하게끔 환경을 셋팅해줘야 한다.

이런 환경을 셋팅할려면 우리가 흔히 사용하는 <mvc:annotation-driven /> 태그를 사용하면 안된다. 왜냐면 이것을 사용하면 Spring이 사용하는 메시지 컨버터를 디폴트로 셋팅해버리기 때문에 개발자가 별도로 만든 메시지 컨버터를 추가할 수 없기 때문이다.(그러나 Spring 3.1에서는 사용자 정의 메시지 컨버터를 추가할수 있는 기능을 가진 태그가 추가되었기 때문에 <mvc:annotation-driven />을 써도 된다)  하지만  <mvc:annotation-driven /> 태그를 빼버리면 과연 Spring 설정파일에 어떤 내용을 넣어야 하는지를 모르게 되는데 그건 여기를 클릭해서 확인해보길 바란다.

위의 링크를 클릭해서 XML을 보게 되면 AnnotationMethodHandlerAdapter 클래스 빈을 정의한 부분을 유심히 보길 바란다. 거기에 보면  messageConverters 프로퍼티에 여러개의 bean이 list 안에 들어가 있는 것을 알수 있다. 바로 이 여러개의 bean이 <mvc:annotation-driven /> 을 설정했을때 Spring이 기본으로 설정해주는 메시지 컨버터들이다. 여기에 File을 처리하는 메시지 컨버터를 추가해 주면 된다.

컨트롤러에서 File을 처리해주는 내가 만든 컨버터의 소스는 다음과 같다.

public class FileHttpMessageConverter implements HttpMessageConverter<File> {
	
	private final Log logger = LogFactory.getLog(getClass());
	
	private List<MediaType> supportedMediaTypes = new ArrayList<MediaType>();

	public FileHttpMessageConverter(){
		this.supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM);
	}
	
	public boolean canRead(Class<?> clazz, MediaType mediaType) {
		// TODO Auto-generated method stub
		return false;
	}

	public boolean canWrite(Class<?> clazz, MediaType mediaType) {
		// TODO Auto-generated method stub
		
		return File.class.equals(clazz);
	}

	public List<MediaType> getSupportedMediaTypes() {
		// TODO Auto-generated method stub
		return supportedMediaTypes;
	}

	public File read(Class<? extends File> clazz, HttpInputMessage inputMessage)
			throws IOException, HttpMessageNotReadableException {
		// TODO Auto-generated method stub
		return null;
	}

	public void write(File t, MediaType contentType, HttpOutputMessage outputMessage) throws IOException,
			HttpMessageNotWritableException {
		// TODO Auto-generated method stub
		
		if (contentType == null) {
			contentType = MediaType.APPLICATION_OCTET_STREAM;
		}
		
		outputMessage.getHeaders().setContentType(contentType);
		FileInputStream fis = new FileInputStream(t);
		
		FileCopyUtils.copy(fis, outputMessage.getBody());
		fis.close();
	}
	
}


이 클래스에 대해 부가적인 설명을 하면..

1. 사용자 정의 메시지 컨버터를 만들려면 HttpMessageConverter<T> 인터페이스를 구현하면 된다. 여기서 T는 현재 만드는 컨버터가 처리하게 될 클래스를 지정한다. 그래서 여기에서는 File을 썼다.
 
2. MediaType 클래스는 HTTP 헤더에 정의하는 컨텐트 타입이라고 보면 된다. 클래스의 생성자를 통해 이 메시지 컨버터가 어떤 컨텐트 타입을 지원할 것인지를 미리 셋팅한다.

3. canRead 함수는 컨트롤러에서 @RequestBody로 셋팅한 파라미터의 클래스와 현재 넘어온 HTTP 컨텐트 타입이 이 메시지 컨버터에서 처리가 가능한지를 확인하여 true, false를 넘겨주도록 구현한다. FileHttpMessageConverter는 현재로선 @RequestBody로 받은 HTTP 요청 메시지 본문을 File 클래스로 변환할 일이 없어서 무조건 false를 리턴하게끔 했다. 하지만 그럴 상황이 있다면 이 부분도 적절히 구현해주면 된다.

4. 마찬가지로 canWrite 함수는 컨트롤렁서 @ResponseBody로 객체를 리턴할때 객체의 클래스와 HTTP 컨텐트 타입이 이 메시지 컨버터에서 처리가 가능한지를 확인하여 true, false 를 넘겨주도록 구현한다. 여기서는 @ResponseBody로 File 클래스를 넘길때 이 메시지 컨버터가 처리를 해야 하기때문에 File 클래스인지를 확인하는 절차를 구현했다

5.  read 함수는 HTTP 요청 메시지 본문을 @RequestBody로 받은 파라미터 객체로 변환해주는 작업을 진행한다. 즉 HTTP 요청 메시지 본문 자체를 File 클래스 객체로 만들어주는 작업을 하면 된다. 현재로썬 그럴 일은 없어서 단순히 null이 리턴되도록 했지만 만약 그런 부분을 구현해야 한다면 여기 read 함수에다가 구현해주면 된다

6. write 함수는 @ResponseBody를 써서 리턴하는  객체를 HTTP 응답 메시지 본문으로 만들어주는 작업을 진행한다. 즉 내가 원하는 File 객체를 리턴하면 이를 HTTP 응답 메시지 본문으로 만들어주는 작업을 여기서 해주면 된다. MediaType 객체가 null일 경우 binary로 처리를 하도록 셋팅을 했다(이 부분은 생성자 부분과 동일하게 처리했다) 그리고 파라미터로 받은 File 객체를 FileInputStream 객체로 한번 wrapping 해준 후에 HttpOutputMessage 객체의 getBody 함수를 통하여 HTTP 응답 메시지 스트림을 구한뒤 이를 FileCopyUtils 클래스의 copy 함수를 통해 복사하는 과정을 통하여 HTTP 응답 메시지에 쏴주는 식으로 구현했다

그럼 이렇게 만든 메시지 컨버터를 이제 Spring이 쓸수 있게끔 등록해줘야 한다. 이것은 다음글에서 언급하겠다