파일 업로드

파일 업로드는 평소에 많이 이용 해보셨을테니 파일 업로드가 무엇인지는 생략하고 바로 구현해보도록 하겠습니다.

이 글에서는 여러 개의 파일을 업로드 할 수 있는 방법까지 다루려고 합니다.




환경 설정

pom.xml

<!-- common fileupload -->

<dependency>

         <groupId>commons-fileupload</groupId>

         <artifactId>commons-fileupload</artifactId>

         <version>1.2.1</version>

</dependency>


<dependency>

         <groupId>commons-io</groupId>

         <artifactId>commons-io</artifactId>

         <version>1.4</version>

</dependency>



spring-servlet.xml

<!-- 멀티파트 리졸버 -->

<bean id="multipartResolver"  class="org.springframework.web.multipart.commons.CommonsMultipartResolver">

         <!-- 최대업로드 가능한 바이트크기 -->

         <property name="maxUploadSize" value="52428800" />


         <!-- 디스크에 임시 파일을 생성하기 전에 메모리에 보관할수있는 최대 바이트 크기 -->

         <!-- property name="maxInMemorySize" value="52428800" / -->


         <!-- defaultEncoding -->

         <property name="defaultEncoding" value="utf-8" />

</bean>

MultipartResolver bean 객체를 등록했습니다.

MultipartResolver는 Muiltpart 객체를 컨트롤러에 전달하는 역할을 합니다.





컨트롤러 / 뷰 작성

이어서 컨트롤러 및 뷰 페이지의 코드를 작성하겠습니다.


먼저 파일을 전송할 수 있는 form.jsp를 생성합니다.

WEB-INF/views/form.jsp

<body>

<h1>파일 업로드 예제</h1>

<form method="post" action="upload" enctype="multipart/form-data">

         <label>email:</label>

         <input type="text" name="email">


         <br><br>

         <label>파일:</label>

         <input type="file" name="file1">


         <br><br>


         <input type="submit" value="upload">

</form>

</body>

파일 업로드를 할 때는 form의 enctype = multipart/form-data로 작성해야하고, method = post여야 합니다.

그래야 MultipartResolver가 multipartFile 객체를 컨트롤러에 전달할 수 있습니다.




FileUploadController

@Controller
public class FileUploadController {
	@Autowired
	FileUploadService fileUploadService;
	
	@RequestMapping( "/form" )
	public String form() {
		return "form";
	}
	
	@RequestMapping( "/upload" )
	public String upload(
			Model model,
			@RequestParam("email") String email,
			@RequestParam("file1") MultipartFile file) {
		
		String url = fileUploadService.restore(file);
		model.addAttribute("url", url);
		return "result";
	}
}

컨트롤러에서는 MultipartFile 객체를 통해 파일을 받습니다.

컨트롤러에서는 컨트롤 역할만 하는 것이 좋으므로 실제 파일을 저장하는 부분은 service 계층에서 하려고 합니다.




FileUploadService

@Service
public class FileUploadService {
	// 리눅스 기준으로 파일 경로를 작성 ( 루트 경로인 /으로 시작한다. )
	// 윈도우라면 workspace의 드라이브를 파악하여 JVM이 알아서 처리해준다.
	// 따라서 workspace가 C드라이브에 있다면 C드라이브에 upload 폴더를 생성해 놓아야 한다.
	private static final String SAVE_PATH = "/upload";
	private static final String PREFIX_URL = "/upload/";
	
	public String restore(MultipartFile multipartFile) {
		String url = null;
		
		try {
			// 파일 정보
			String originFilename = multipartFile.getOriginalFilename();
			String extName
				= originFilename.substring(originFilename.lastIndexOf("."), originFilename.length());
			Long size = multipartFile.getSize();
			
			// 서버에서 저장 할 파일 이름
			String saveFileName = genSaveFileName(extName);
			
			System.out.println("originFilename : " + originFilename);
			System.out.println("extensionName : " + extName);
			System.out.println("size : " + size);
			System.out.println("saveFileName : " + saveFileName);
			
			writeFile(multipartFile, saveFileName);
			url = PREFIX_URL + saveFileName;
		}
		catch (IOException e) {
			// 원래라면 RuntimeException 을 상속받은 예외가 처리되어야 하지만
			// 편의상 RuntimeException을 던진다.
			// throw new FileUploadException();	
			throw new RuntimeException(e);
		}
		return url;
	}
	
	
	// 현재 시간을 기준으로 파일 이름 생성
	private String genSaveFileName(String extName) {
		String fileName = "";
		
		Calendar calendar = Calendar.getInstance();
		fileName += calendar.get(Calendar.YEAR);
		fileName += calendar.get(Calendar.MONTH);
		fileName += calendar.get(Calendar.DATE);
		fileName += calendar.get(Calendar.HOUR);
		fileName += calendar.get(Calendar.MINUTE);
		fileName += calendar.get(Calendar.SECOND);
		fileName += calendar.get(Calendar.MILLISECOND);
		fileName += extName;
		
		return fileName;
	}
	
	
	// 파일을 실제로 write 하는 메서드
	private boolean writeFile(MultipartFile multipartFile, String saveFileName)
								throws IOException{
		boolean result = false;

		byte[] data = multipartFile.getBytes();
		FileOutputStream fos = new FileOutputStream(SAVE_PATH + "/" + saveFileName);
		fos.write(data);
		fos.close();
		
		return result;
	}
}

조금 복잡해 보일 수 있습니다.

부가적인 부분을 말씀드리고 싶어서 길어진 것 뿐이니, 복잡하게 생각하실 필요 없습니다.


1. SAVE_PATH는 파일을 저장할 위치를 의미합니다.

일반적으로 서버는 리눅스 기반이므로 리눅스 경로명을 사용하는 것이 좋습니다.


즉 파일을 루트 경로인 / 아래의 upload 폴더에 저장하겠다는 의미인데, 윈도우에서는 JVM이 알아서 workspace가 존재하는 드라이브의 위치를 찾아서 드라이브를 루트 경로로 하여 upload 폴더에 저장합니다.

예를들어 이클립스 workspace가 C드라이브에 있다면 C드라이브의 upload 폴더에 파일이 저장될 것입니다.



2. PREFIX_URL은 저장된 파일을 JSP에서 불러오기 위한 경로를 의미합니다.

특별한 의미는 없습니다.


3. 22~25 line : MultipartFile 객체는 파일의 정보를 담고 있습니다.


4. 28 line : url을 반환하는 이유는 뷰 페이지에서 바로 이미지 파일을 보기 위함입니다.

만약 DB에서 이미지 경로를 저장 해야 한다면, 이와 같이 url을 반환하면 좋을 것입니다.


5. 41 ~ 55 line : 현재 시간을 기준으로 파일 이름을 바꿉니다.

이렇게 하는 이유는, 여러 사용자가 올린 파일의 이름이 같을 경우 덮어 씌어지는 문제가 발생하기 때문입니다.

따라서 파일 이름이 중복될 수 있는 문제를 해결하기 위해 ms단위의 시스템 시간을 이용하여 파일 이름을 변경합니다.


6. 59 ~ 69 line : FileOutputStream 객체를 이용하여 파일을 저장합니다.

( File IO에 대해서는 여기를 참고 해주세요 ! )




마지막으로 결과 페이지를 보여주는 페이지입니다.

result.jsp

<h1>Upload completed</h1>

<div class="result-images">

         <img src="${pageContext.request.contextPath }${url }" style="width:150px">

</div>


<p> <a href='/fileupload/form'> 다시 업로드 하기 </a> /p>

${ url }은 컨트롤러에서 넘겨준 파일이 저장된 경로명입니다.


이제 workspace가 위치한 드라이브에 가서 upload 폴더를 생성하신 후에,  브라우저에서 이미지 파일을 업로드하는 테스트를 해보세요 !





문제점 및 해결

upload폴더를 보시면 파일이 업로드 된 것을 확인할 수 있지만, 결과 화면을 보시면 이미지가 제대로 출력 되지 않을 것입니다.


엑박이 뜬 이미지 파일을 우클릭하여 경로를 보시면 아마 다음과 같을 것입니다.

http://localhost:8080/fileupload/upload/201822632335607.PNG


파일을 저장할 때 upload라는 폴더에 저장을 했는데, 파일을 저장할 때의 upload는 C드라이브 내의 upload 폴더이고,

위 URL에서 upload는 애플리케이션 상 경로에 있는 upload이므로 WEB-INF 폴더의 하위 폴더로서의 upload를 의미합니다.

즉 실제 파일이 저장된 서버 상의 위치( 물리 주소 )와 애플리케이션에서 보여주고자 하는 파일 경로( 가상 주소 )가 일치하지 않은 것입니다.


따라서 실제 파일이 저장되어 있는 위치와 애플리케이션 상의 위치를 일치시키는 작업이 필요합니다.

spring-servlet.xml에 물리 주소와 가상 주소를 매핑 해주는 코드를 추가하도록 하겠습니다.

<!-- resource mapping -->

<!-- location : 물리적 주소 / mapping : 가상 주소 -->

<mvc:resources location="file:/upload/" mapping="/upload/*"/>

이제 정상적으로 result.jsp 페이지에서 이미지가 출력될 것입니다.





멀티파일 업로드

이번에는 여러 개의 파일을 업로드 할 수 있는 멀티 파일 업로드를 알아보겠습니다.

수정할 부분은 <input> 태그랑 Controller에서 MultipartFile 객체를 받는 파라미터 부분 두 곳인데, 필요한 부분만 간단하게 언급하겠습니다.


WEB-INF/views/form.jsp

<input type="file" name="files" multiple>

<input> 태그에서는 multiple 속성만 추가하면 됩니다.

"파일선택"을 클릭하면 ctrl 키를 눌러서 여러 개의 파일을 선택할 수 있습니다.



FileUploadController

@RequestMapping( "/upload" )
public String upload(
		Model model,
		@RequestParam String email,
		@RequestParam(required=false) List<MultipartFile> files) {

	...

}

컨트롤러에서는 여러 개의 파일을 받기 때문에 MultipartFile을 List로 받아야 합니다.






추가적인 고려 사항

파입 업로드를 구현하는 작업은 끝났지만 한 가지 고려해야 할 부분이 있습니다.


한 디렉터리에 담을 수 있는 파일 수는 제한적( 65535개 ? )이라는 것인데, 

만약 위의 예제와 같이 한 디렉터리에 모든 파일을 저장한다면 용량의 문제보다 저장할 수 있는 파일 개수의 부족으로 파일을 저장하지 못할 수 있습니다.

이럴 경우를 대비하여 subdirectory 구조로 파일을 저장할 수 있습니다.


파일의 이름이 같을 경우 서버 디렉터리에서 파일이 덮어 씌어 질 수 있다고 했었습니다.

그래서 파일 이름을 현재 시간을 기준으로 바꿔서 저장을 했었죠.


파일이름의 시간을 기준으로 계층형 디렉터리에 파일이 골고루 분포하도록 할 수 있는 알고리즘을 생각할 수 있습니다.

따라서 파일을 저장할 폴더를 결정할 때 현재 시간의 시(Hour)를 기준으로 파일이 저장될 폴더를 1차적으로 나누고,

분(Minute)을 기준으로 파일이 저장될 폴더를 2차적으로 나눌 수 있습니다.

따라서 한 폴더에만 파일이 집중적으로 저장되는 문제를 해결 할 수 있습니다.


이 내용은 부가적인 부분인데, 알아두시면 좋을 것 같아서 언급을 했습니다.





이상으로 파일 업로드를 하는 방법에 대해 알아보았습니다.

구현하는 것 자체는 그리 어렵지 않은데, 서버를 관리할 때 생길 수 있는 여러 이슈를 언급하다 보니 코드랑 글이 길어졌네요.