본문 바로가기

프로그래밍/Spring

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

최근에 XML을 이용하여 Spring 운영 환경을 설정하는 방식에서 Java Configuration 방식으로 운영 환경 설정을 바꾸어가고 있다. 실행 착오도 몇번 있었지만 역시 코드로 환경을 설정한다는건 여러모로 유용한 점이 많아서 일단 계속 파보고 있다. 기존 MVC 환경 설정은 이제 다 끝내서 일종의 템플릿화 작업까지 마쳐진 상태인데 그 과정에서 Spring Converter Interface를 이용한 파일 업로드 방식을 구현한 것이 있어서 이번에 이와 관련된 글을 남기고자 한다.

 

우리가 Spring MVC에서 파일 업로드 기능을 넣을 경우 이 2가지 중 하나는 했을 것이다.

 

1. Spring Controller에서 @RequestParam 어노테이션을 사용하여 사용자가 업로드 한 파일을 MultipartFile 객체로 받기

2. Spring Controller에서 Model로 사용되는 Bean 클래스에 MultipartFile 멤버변수를 선언해서 이 변수에서 사용자가 업로드한 파일을 매핑해서 받기

 

즉 Spring에서 제공하는 org.springframework.web.multipart.MultipartFile 클래스 객체로 사용자가 업로드한 파일을 받지만 그 방법은 위에서 언급한 것과 같이 크게 2가지 중 하나를 사용할 것이라고 생각한다. 근데 한번 생각해보자. MultipartFile 클래스 객체를 받으면 이것을 바로 사용하는가? 절대..절대로 아닐것이다. 아마 다음의 부가 작업을 할 것이다.

 

1. 파일이 저장될 경로를 구한다(프로퍼티에서 구할수도 있고 또는 내부적으로 정의란 룰에 의해서도 구할 것이다)

2. 업로드 한 파일의 확장자를 구한다

3. 사용자가 업로드한 파일의 파일 이름과 이를 실제 저장할때 파일 이름을 구한다(우리가 흔히 얘기하는 Original File Name과 Real File Name을 의미한다)

4. 3번에서 구한 파일 이름들을 DB에 저장한다

5. 1번에서 구한 경로에 3번에서 구한 실제 저장할 파일 이름으로 사용자가 업로드한 파일을 서버에 저장한다

 

개발자에 따라 4번과 5번의 순서를 바꿔서 하는 사람도 있을 것이다. 그러나 나는 이 순서로 진행한다. 이렇게 진행하는 이유에 대해서는 나중에 설명하겠다. 일단 지금 말하고자 하는 것의 핵심은 우리가 처음 받게 되는 MultipartFile 객체를 바로 사용하지는 않는다는데 있다. 이렇게 무언가 몇가지 작업을 한다는 것이다.

 

그러면 이런 작업을 개발자들은 어디서 수행하는가? Spring을 기준으로 얘기한다면 Controller 계층에서 이런 작업을 처리할수도 있고 Service 계층에서 처리할 수도 있다. 나는 이런 작업을 Service 계층에서 처리한다. 이 작업을 Service 계층에서 처리하는 이유에 대해서도 나중에 설명하겠다. 암튼 이런 작업을 하기 위해 Controller 계층이든 Service 계층이든 적어도 10여라인의 코드가 들어갈 것이다. 그리고 이런 작업이 빈번하다면 이 작업 또한 공통 코드로 빼서 작업하는 리팩토링 과정을 할 것이다.

 

그럼 이제 이 시점에서 이 글을 읽어보는 분들에게 한가지 제안을 해보고 싶다. 이거..사실 은근히 귀찮은 작업이다. 난이도는 거의 없다시피 한 작업이긴 하지만 반복성 노가다 작업 성격이 있다. 어느 사이트를 개발하든 간에 파일 업로드 하는 과정은 다 있다 보니 습관마냥 만들게 되고 그렇게 자주 만들다보면 나만의 코드..도 생길것이다. 그러나 그렇다 해도 파일 업로드가 들어가는 부분마다 일일이 코딩을 하든 공통코드를 호출하든 그렇게 해야 한다. 이런 반복성 작업에서 이젠 벗어나고 싶지 않은가? 벗어나고 싶을것이다(아니라고? 그럼 그렇게 생각하는 사람은 이 다음의 내용은 읽지 말고 뒤로 가기를 누르길 바란다. 시간 낭비다) 이걸 벗어날려면 어디서 하면 좋을까? 아무래도 Controller 계층에 들어오는 시점에 미리 위에서 언급된 작업들이 전부 또는 일부 되어 있는 상태에서 서비스 코드 작업을 하는 것이 가장 바람직할 것이다. 즉 Model에 Binding 되는 시점에 미리 다 되어 있으면 우리는 해당 멤버변수만 가져와서 필요한 작업만 해주면 되기 때문이다. 다음의 클래스 코드는 이런 작업을 생각하고 만든 사용자가 업로드 한 파일이 Binding 되는 클래스 코드이다.

 

package com.terry.springconfig.common.vo;

import java.io.File;
import java.io.IOException;

import org.springframework.web.multipart.MultipartFile;

public class FileVO {

	private String orgFileName;				// 원래 파일명(업로드 될 당시의 파일명)
	private String realFileName;			// 실제 파일명(실제로 저장된 파일명)
	private String ext; 					// 업로드 된 파일의 확장자
	private File file;						// MultipartFile이 실제 Java File로 저장된 파일
	private MultipartFile multipartFile;	// 사용자가 업로드한 MultipartFile
	private long fileSize;					// 업로드된 파일 크기
	
	public String getOrgFileName() {
		return orgFileName;
	}
	
	public void setOrgFileName(String orgFileName) {
		this.orgFileName = orgFileName;
	}
	
	public String getRealFileName() {
		return realFileName;
	}
	
	public void setRealFileName(String realFileName) {
		this.realFileName = realFileName;
	}
	
	public String getExt() {
		return ext;
	}

	public void setExt(String ext) {
		this.ext = ext;
	}

	public File getFile() {
		return file;
	}
	
	public void setFile(File file) {
		this.file = file;
	}
	
	public MultipartFile getMultipartFile() {
		return multipartFile;
	}
	
	public void setMultipartFile(MultipartFile multipartFile) {
		this.multipartFile = multipartFile;
	}

	public long getFileSize() {
		return fileSize;
	}

	public void setFileSize(long fileSize) {
		this.fileSize = fileSize;
	}
	
	/**
	 * 저장될 파일 경로를 입력받아 사용자가 업로드한 파일을 해당 경로에 저장한다
	 * @param filePath					저장될 파일 경로
	 * @throws IllegalStateException
	 * @throws IOException
	 */
	public void transferFile(String filePath) throws IllegalStateException, IOException{
		file = new File(filePath);
		multipartFile.transferTo(file);
	}
}

 

위의 VO 클래스 코드를 보자. 멤버변수쪽에 주석을 달아두었기 때문에 무엇을 저장하는 클래스인지는 감을 잡을수 있을 것이다. 정리하자면 사용자가 파일을 업로드 하게 되면 이 FileVO 객체에 Binding 되어서 받을수 있게끔 할려는 것이다. 멤버변수에 저장되는 것이 무엇인지 보면 위에서 나열했던 우리가 파일을 업로드 하게 되면 부가적인 작업들을 하는 그런 내용들이 저장되기 때문에 Controller에서 이 객체를 가져오는 시점에서 우리는 관련작업을 다 마치거나 또는 일부가 마쳐진 상태에서 받을수 있게 된다.

 

그러면 이 클래스 객체에 관련 내용을 채워넣어야 할텐데 이것을 누가 하게 되는가? 즉 사용자가 업로드 한 Spring의 MultipartFile 객체를 FileVO 객체로 Binding 해야 하는데 이것을 누가하는가..에 대한 고민을 해야 하는 부분이 생긴다. Spring은 특정 타입으로의 매핑(여기서는 MultipartFile 타입을 FileVO 타입으로 매핑함을 의미한다)하는 역할을 하는 것이 2가지가 존재한다.

 

1. PropertyEditor(java.beans.PropertyEditor)

2. Converter(org.springframework.core.convert.converter.Converter)

 

원래 PropertyEditor는 JavaBeans에서 Property에 대한 표현을 하기 위해 생긴 Java Interface이고 이를 구현한 java.beans.PropertyEditorSupport 클래스를 상속받아서 사용하게 된다. 그리고 Converter는 Java에서 제공하는게 아닌 Spring에서 제공하는 Interface이다.

 

PropertyEditor나 Converter의 경우 타입 변환을 하는 작업을 하는데 있어서는 변함이 없다. 그러나 이 둘의 경우 2가지의 차이점이 존재하게 되는데..

 

1. PropertyEditor의 경우 양방향을 모두 지원한다. 예를 들면 A 타입과 B 타입간의 타입 변환을 해야 하는 경우 PropertyEditor는 A<->B 간의 상호변환이 가능하다. 그러나 Converter의 경우는 단방향이다. 즉 A->B로 변환하는 Converter를 하나 만들고 다시 B->A로 변환하는 Converter를 만들어야 양방향 구현이 가능하다

 

2. PropertyEditor의 경우는 MultiThread에 안전하지가 않다. 그래서 PropertyEditor를 Spring에서 사용하려 할 경우 new를 사용해서 일일이 객체를 생성해서 사용해야 한다. 그러나 Converter의 경우 MultiThread에서도 안전하기 때문에 Spring의 Bean으로 선언해서 작업해도 문제가 되지 않는다.

(물론 PropertyEditor도 Bean 으로 등록해서 사용해도 된다. 단 이렇게 하려고 할 경우 Bean의 Scope를 ProtoType으로 주어서 Singleton 스타일이 아닌 일일이 객체를 만들어서 Injection이 되도록 해줘야 한다)

 

PropertyEditor가 왜 MultiThread에 안전하지 않은가 하면 변환의 대상이 되는 객체를 PropertyEditorSupport 클래스 객체에 멤버변수로 저장을 하고 있기 때문에 그렇다. 이 부분은 PropertyEditorSupport의 Source 코드를 보면 알 수 있다. 즉 내부에서 객체 정보를 가지는 형식으로 변환하고 있기 때문에 이를 Singleton으로 할 경우 문제가 생기는 것이다.

 

그래서 이 글에서는 Converter를 구현하는 내용으로 설명을 하고자 한다. Converter를 구현할 경우 위에서 얘기했다시피 양방향 변환을 해야 할 경우 Converter 클래스를 2개를만들어야 한다고 했다. 그러나 매번 양방향이 되는 상황은 아니다. 지금 하고자 하는 File 작업을 보자. Spring에서 MultipartFile로 받은 파일 객체를 위에서 언급한 FileVO로 변환하기 위해서 Converter를 하나 만들어야 하는데는 이견이 없을 것이다. 그러나 FileVO 객체를 Spring의 MultipartFile로 변환해서 사용할 일이 있을까? Spring의 MultipartFile 객체는 그거만으로 완전체적인 역할을 수행할 수 없다. 파일 업로드 처리 하는 과정을 좀만 살펴보면 MultipartFile을 File로 변환해서 하는 작업은 있어도 File로 만든것을 MultipartFile로 변환해서 할 일은 없다는 것을 알 수 있다. 또한 MultipartFile 객체는 파일 업로드시에 만들어지는 객체의 성격이 강하다. 즉 Http 프로토콜로 파일이 올라갈 경우 이를 받아서 MultipartFile 객체로 만들어주는 것이지 물리적인 File을 MultipartFile로 만들수 있는 것이 아니다. 이 부분에 대해 좀더 설명하자면 MultipartFile은 Interface이기 때문에 Spring에서는 MultipartFile을 구현한 클래스인 org.springframework.web.multipart.commons.CommonsMultipartFile 클래스와 org.springframework.mock.web.MockMultipartFile 이렇게 2개의 클래스를 제공하고 있다. MockMultipartFile은 Spring MVC Test를 할때 파일 업로드와 관련된 Test시 사용하기 위한 일종의 Mock 성격의 클래스이고, 실제적으로 사용되는 클래스는 CommonsMultipartFile 클래스이다. 이 클래스의 API 문서를 보면 생성자에서 Apache Common File Upload 라이브러리에서 제공하는 Interface인 org.apache.commons.fileupload.FileItem Interface를 받도록 되어있다. Java의 File 객체를 기반으로 MultipartFile Interface를 구현한 객체를 만들 방법이 없는 것이다.  그래서 Converter를 만들때 생각해야 하는 것이 양방향을 모두 만들 필요가 있는가를 생각해야 하는 것이다. 물론 양방향을 모두 만드는데 문제가 없을 경우 지금 당장은 이용하지 않는다해도 나중을 생각해서 만들어두는 것은 바람직하다.

 

이번 글에서는 간략하게나마  File Upload시 우리가 최종 결과물을 어떤 형태로 받아볼 수 있을지, 그리고 이런 작업을 하는 PropertyEditor와 Converter의 비교, 그래서 Conveter로 가게 되는 이유를 설명했다. 다음 글에서는 Converter의 구체적인 설명과 구현 방법에 대해 설명하도록 하겠다