본문 바로가기

프로그래밍/Spring

SpringFramework의 Converter를 이용하여 업로드된 파일을 객체로 변환해보자 (3)

지난 글에서는 MultipartFile 클래스 객체를 우리가 원하는 VO 클래스(FileVO 클래스)로 변환하는 Converter 인터페이스를 구현한 클래스에 대해 살펴보았다. 이번은 마지막 글로 이렇게 만든 Converter 인터페이스를 구현한 클래스를 어떻게 사용하는지에 대해 알아보자.

 

MultipartFile 클래스 객체를 FileVO 객체로 변환하는 작업은 웹페이지에서 데이터를 전송해서 이를 객체로 Binding 하는 과정에서 일어나게 된다. 즉 데이터 전송시 파일을 업로드 했으면 이를 FileVO 객체로 변환한뒤 이를 Controller의 메소드에서 사용되어지는 것이다. 그렇기 때문에 이러한 역할을 하는 Converter 인터페이스를 구현한 클래스는 Spring의 Servlet Context에 등록되어져야 한다. Spring의 Servlet Context에 이러한 역할을 하는 클래스를 등록하는 방법은 2가지가 있다. 우리가 Spring 환경 설정을 XML로 했다면 XML을 이용해서 이를 등록시킬수가 있다. 또는 최근 많이 사용되는 방법은 Java Code를 이용하는 Spring 환경 설정을 등록한다면 Java Code를 이용해서도 등록시킬수 있다. 여기서는 Java Code를 이용해서 등록시키는 방법에 대해 설명하겠다(XML의 경우는 구글링을 하면 검색이 되는데다가 Spring Reference 문서의 MVC 섹션을 보면 나오는지라 별도의 설명은 하지 않겠다)

 

위에서 설명했던 작업 내용을 요약하자면 Servlet Context에 Converter 인터페이스를 구현한 클래스를 Spring Bean으로 등록하고 이렇게 등록된 Bean을 Converter로 등록시켜주는 작업으로 요약할 수 있다. 그래서 Java Code 로 Spring 환경 설정을 할때 Spring MVC를 사용하는 환경설정 작업에서 이러한 작업을 해야 한다. 그래서 이를 Java Code로 작성하게 되면 다음과 같이 된다.

 

@Configuration
@EnableWebMvc
@ComponentScan(
		basePackages="com.terry.springconfig",
		useDefaultFilters = false,
		includeFilters={
			@ComponentScan.Filter(type=FilterType.ANNOTATION, value=Controller.class),
			@ComponentScan.Filter(type=FilterType.ANNOTATION, value=ControllerAdvice.class)
		}
)
public class ServletContextMain extends WebMvcConfigurerAdapter{
	
	@Bean
	public CommonsMultipartResolver multipartResolver() throws IOException{
		CommonsMultipartResolver commonsMultipartResolver = new CommonsMultipartResolver();
		commonsMultipartResolver.setMaxInMemorySize(2048000);  // 메모리에 쓰여지는 크기(2메가)
		commonsMultipartResolver.setMaxUploadSize(10240000);	// 최대 업로드 크기 설정(10메가)
		// commonsMultipartResolver.setUploadTempDir(new FileSystemResource("D:/FileUploadTempDir"));	// 업로드시 임시로 쓰여지는 위치를 지정
		commonsMultipartResolver.setUploadTempDir(new FileSystemResource(uploadTempDir));	// 업로드시 임시로 쓰여지는 위치를 지정
		return commonsMultipartResolver;
	}
	@Bean
	public MultipartFileToFileVOConverter multipartFileToFileVOConverter() {
		return new MultipartFileToFileVOConverter();
	}
	
	@Bean
	public StringToFileVOConverter stringToFileVOConverter() {
		return new StringToFileVOConverter();
	}
	
	@Bean
	public StringToMultipartFileConverter stringToMultipartFileConverter() {
		return new StringToMultipartFileConverter();
	}
	
	@Override
	public void addFormatters(FormatterRegistry registry) {
		// TODO Auto-generated method stub
		registry.addConverter(multipartFileToFileVOConverter());
		registry.addConverter(stringToFileVOConverter());
		// registry.addConverter(stringToMultipartFileConverter());
		super.addFormatters(registry);
	}
}

 

Java Code로 Spring MVC 환경설정을 할때 org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter 클래스를 상속받아 구현하게 된다. 이 클래스에서 제공하는 메소드 중 addFormatters 메소드가 있는데 우리가 하려고 하는 Converter 인터페이스를 구현하는 클래스를 등록하는 작업을 이 메소드에서 하게 된다. 우리가 만든 Converter 인터페이스를 구현한 클래스를 @Bean 어노테이션을 붙인 메소드를 통해 Spring Bean으로 등록한 뒤 addFormatters 메소드에서 파라미터로 넘겨받는 FormatterRegistry 인터페이스에서 제공되는 addConverter 메소드의 파라미터에서 @Bean 어노테이션이 붙은 메소드를 호출하는 방법으로 우리가 만든 Converter들을 등록하게 된다.

 

그럼 이제 여기서 눈여겨보아야 할 코드가 있다. MultipartFile 클래스를 FileVO 클래스로 변환하는 MutipartFileToFileVOConverter 클래스를 Spring Bean으로 등록하는 메소드인 multipartFileToFileVOConverter() 메소드에 대해서는 이해가 가는데 나머지 2개의 메소드, 즉 StringToFileVOConverter 클래스 객체를 return 하는 stringToFileVOConverter() 메소드와 StringToMultipartFileConverter 클래스 객체를 return 하는 stringToMultipartFileConverter() 메소드가 그것이다. 이 메소드가 왜 있어야 하는가에 대한 설명을 지금부터 하고자 한다.

 

이 글을 보는 사람들 중 기억하는 사람들이 있을라나 모르겠는데 이전 글에서 MultipartFileToFileVOConverter 클래스에 대한 소스를 설명할때 파일 업로드가 가능한 게시판 형태에서 파일을 지정하지 않고 submit을 할 경우 파일을 지정했는지 확인하기 위해 MultipartFile 인터페이스의 isEmpty() 메소드를 이용해서 업로드하고자 하는 파일을 설정했는지의 여부를 확인하는 부분이 있었다. 이때 결과론적으론 이 부분은 필요가 없게 되었다고 설명하면서 다음 글에서 왜 그런지를 설명하겠다고 했는데 바로 이 부분에 대한 설명을 하고자 한다. Converter를 이용하지 않고 바로 MultipartFile을 이용해서 업로드 한 파일을 받고자 할때는 isEmpty() 메소드가 유효했다. 즉 파일을 지정하지 않고 게시판 등록작업을 하는 경우 파일을 지정했는지의 여부를 isEmpty() 메소드를 이용해서 확인을 할 수 있었다. 근데 Converter를 등록하게 되면서 이 부분이 약간 이상하게 동작하게 되었다. 위의 코드에서와 같이 MultipartFileToFileVOConverter 클래스를 등록한 뒤에 파일을 지정하지 않고 게시판 등록 작업을 하게 되면 String 객체인 빈 문자열("")을 FileVO 클래스로 변환할 수 없다는 의미의 에러메시지가 나오게 된다. Converter 를 등록하면서 이런 증상이 생기게 되었는데 이런 증상이 생기는 원인은 다음과 같이 추측해보았다. 우리가 form의 특정 필드에 값을 설정한뒤 이를 특정 타입으로 변환하게 되는 바인딩 과정을 살펴보면 바로 특정 타입으로 변환하는 것이 아니라 먼저 String 타입으로 한번 바꾼뒤에 다시 이 String 타입을 특정 타입으로 바꾸는 작업을 하게 된다. 실제로 Spring에 기본적으로 등록되는 Converter들을 보면 충분히 이런 작업을 위해 사용될꺼라는 생각들이 들 정도로 String을 기반으로 하는 많은 Converter 들이 존재하며 이것들이 실제로 등록되고 있다. 그래서 파일 또한 이런 원리가 아닐까 싶다. 파일은 애초에 바이너리이기 때문에 파일 관련 필드에 무언가 값이 있으면 바로 MultipartFile로 변환하게 되지만 그렇지 않을 경우 먼저 String 타입으로 변환한뒤 다시 이를 변환하는 적절한 Converter를 찾게 되는 것이다. 우리는 FileVO 클래스로 변환되게끔 했기 때문에 String 객체를 FileVO로 변환하는 Converter를 찾을려고 시도할텐데 우리는 이런 Converter를 등록하질 않았기 때문에 이와 같은 에러를 내는 것이라고 추측해보았다. 그래서 이와 같은 에러로 인해 String을 FileVO 클래스로 변환하는 작업을 하는 Converter 인터페이스를 구현한 클래스인 StringToFileVO 클래스를 만든뒤 이를 Spring Bean으로 등록했다. StringToFileVO 클래스 소스는 다음과 같다.

 

public class StringToFileVOConverter implements Converter<String, FileVO> {

	@Override
	public FileVO convert(String source) {
		// TODO Auto-generated method stub
		return null;
	}

}

위의 클래스 소스를 보면 단순하게 되어 있다. convert 메소드가 null을 return 하는 것으로 아주 단순하다. 그럴수 밖에 없다. 이전 글에서도 얘기를 했지만 Converter를 만들때는 의미가 있는가를 따지고서 만들어야 한다고 했었다. String 클래스를 이용해서 FileVO 클래스로 바꿀때 무슨 의미가 있을까? 현재는 파일을 업로드하지 않을 경우 1차적으로 이 부분을 String 객체로 변환해서 여기에 빈 문자열("")을 값으로 설정하게 되는데 이것을 가지고 FileVO로 변환을 하는 것이 의미가 있을까? 아무 의미가 없기 때문에 그냥 null을 return 하도록 했다. 그리고 이 Converter 작업을 하면서 FileVO가 아니라 MultipartFile로 받을때도 String 객체인 빈 문자열("")을 MultipartFile 클래스로 변환할 수 없다는 의미의 에러메시지가 나온다. 그래서 String 객체를 MultipartFile 클래스로 변환하는 Converter인 StringToMultipartFile 클래스도 만들어서 Converter로 등록했다. 물론 String을 MultipartFile로 변환하는 것이 의미가 없는 관계로 이 클래스의 convert 메소드 또한 null을 return 하도록 했다(StringToFileVOConverter 클래스 소스와 클래스 이름만 다를 뿐이지 나머지는 동일한 소스라 따로 올리진 않았다)

 

그럼 이렇게 등록한 Converter를 이제 Spring에서 어떻게 사용하는지를 설명하겠다. 예를 들어 다음과 같은 VO를 하나 만들어서 사용자가 게시판에 글을 쓸때 이 VO로 바인딩한다고 생각해보자.

 

public class Member1BoardVO  {

	@NotBlank
	@Size(min=5, max=13)
	private String title;					// 제목

	@NotBlank
	private String content;					// 내용
	
	private FileVO [] file;					// 업로드한 파일 정보
	
}

 

위의 클래스 소스에서 각 멤버변수의 getter, setter 메소드는 생략했다. 소스의 주석을 보면 알겠지만 제목과 내용, 그리고 업로드한 파일 정보의 배열을 저장하는 변수들로 이루어져 있다. html form의 text 태그에서 name 속성을 각각 title, content, file로 주면 해당 변수에 알아서 매핑이 된다. 파일의 경우 배열로 선언함으로써 file 태그를 같은 이름으로 여러개를 사용할 경우도 배열로 저장이 되게끔 구성했다. 이렇게 만들어진 Member1BoardVO 클래스를 Controller에서 어떻게 사용하는지 보자. 

 

@RequestMapping(value="/member1/insertUpdateMember1Board", method=RequestMethod.POST)
@ResponseBody
public String insertMember1Board2(@Valid Member1BoardVO member1BoardVO){
	
	return "redirect:/member1/selectMember1List.do";
}

 

이 소스는 html  form 태그에서 post 방식으로 전달된 사용자 입력값을 받는 Controller의 메소드 소스이다. 이 코드는 설명하기 위해 단순하게 만든 코드이다. 즉 서비스 로직을 타는 것도 없이 그냥 리스트 페이지인 selectMember1List.do로 redirect 해주는 것 뿐이 없다. 그러나 이 시점에서 이미 사용자가 업로드한 파일 정보가 Member1BoardVO 클래스 객체인 member1BoardVO에 들어가 있다. 즉 file 태그의 name 속성을 file이라고 한뒤 거기에 파일을 지정하고 submit 하면 Member1BoardVO 클래스의 FileVO 배열 멤버변수인 file에 FileVO 객체가 들어와 있고 이 객체 내부를 살펴보면 FileVO가 저장하는 것들(MultipartFile 객체, Original File Name, 파일 크기 등)이 이미 셋팅이 되어 있는 상태로 있게 된다. 즉 사용자가 submit 한 것을 관련 변수(여기서는 Member1BoardVO의 member1BoardVO 변수)에 바인딩 되는 시점에 이미 그런 값들을 다 파악해서 셋팅해두는 것이다. 그래서 Controller 메소드에서나 또는 이 변수를 사용하는 Service 메소드에서 사용자가 업로드 한 파일의 정보를 알기 위해 부가적인 작업을 할 필요가 없게 된다. 이 부분은 이클립스에서 return 부분에 중단점을 걸은뒤 이클립스에 등록된 WAS를 디버그 모드로 실행하여 member1BoardVO 변수의 내부값을 이클립스로 확인해 보면 알 수 있다.

 

이상으로 File 관련 Converter 설명을 마치도록 하겠다. 비단 파일 정보뿐만 아니라 사용자가 submit 하는 시점에서 별도의 서비스 로직을 통해 미리 구할 수 있는 정보가 있다면 Converter를 통해 이러한 값들을 미리 구한뒤 바인딩을 시켜버리면 Controller나 Service 쪽에서 부가적인 작업을 할 필요없이 바로 이용할 수 있다.