[JSP] 파일 업로드 예제와 다운로드 (form 태그와 multipart/form-data)

파일 업로드

이번 포스트에서는 form 태그, multipart/form-data 내용을 확인하고 간단한 예제 코드를 통해 파일 업로드 전체 과정을 배워본다.

form 태그

form 태그는 입력 양식 전체를 감싸는 태그로 입력된 정보를 한 번에 서버로 전송한다.

form 태그는 아래와 같은 속성을 가진다.

  • name : form 을 식별하기 위한 이름 지정
  • action : 전송된 form 을 수신할 서버 쪽 스크립트 파일 지정
  • accept-charset : form 전송에 사용할 문자 인코딩 지정
  • target : action 에서 지정한 스크립트 파일을 현재 창이 아닌 다른 위치에 열도록 지정
  • method : form 을 서버에 전송할 HTTP 메소드
  • autocomplete : 자동 완성 지원
  • enctype : form 이 서버로 전송될 때, 해당 데이터가 인코딩되는 방법 명시
    • application/x-www-form-urlencoded : 모든 문자들을 서버로 보내기 전 인코딩
    • text/plain : 공백 문자는 + 기호로 변환하고, 나머지 문자는 모두 인코딩되지 않음
    • multipart/form-data : 모든 문자를 인코딩하지 않음

multipart/form-data

위에서 말한 form 태그가 submit 되면 다음과 같은 과정을 거쳐 생성된 요청이 서버에 전송된다.

  • form 태그 내부 submit 이 발생
  • 요청을 위한 HTTP Request 생성
    • HTTP Request 는 Body 에 클라이언트가 전송하는 데이터 삽입
    • Body 에 들어가는 데이터 타입을 HTTP Header (Content-type) 에 명시
  • 생성된 HTTP Request 서버에 전송

이렇게 전송된 요청에 대해 서버는 Content-type 을 확인하여 해독하게 된다. 이 때, 보통의 경우 Body 내부는 한 가지 방법으로 인코딩 된다. text/plain, text/xml, image/jpeg 등이 그 예시다.

그러나 파일 전송에는 위 요소들 중 2가지 이상이 복합적으로 사용된다. 그렇기에 이를 구분하기 위해서 multipart/form-data 을 사용해 각각이 서로 다른 인코딩을 가진다는 것을 명시해주는 것이다.

실제로도 웹 브라우저 클라이언트에서 파일 업로드를 구현할 때, 보통 <form> 태그를 통해 파일을 등록하고 전송한다. 이때, F12를 눌러 개발자 도구를 살펴보면 웹 브라우저가 보내는 HTTP 메시지의 Content-type 속성이 multipart/form-data 로 설정된 것을 볼 수 있다.

파일 업로드 예제

먼저 html 이나 jsp 파일을 아래와 같이 만든다.

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<form action="/utils/fileUpload" method="post" enctype="multipart/form-data">
        file: <input type="file" name="fileName"> <input type="submit" value="file upload">
    </form>
</body>
</html>

이 때, action="/utils/fileUpload"@WebSevlet("/utils/fileUpload") 어노테이션이 달린 어딘가에 있는 자바 소스를 동작시킨다. 이 자바 파일을 아래와 같이 작성한다.

package com.jsp_example.web;

/* import ...*/

@MultipartConfig(
	// location="/tmp",
	fileSizeThreshold=1024*1024,
	maxFileSize=1024*1024*10,
	maxRequestSize=1024*1024*10*10
)
@WebServlet("/utils/fileUpload")
public class FileUpload extends HttpServlet{
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		System.out.println("Get Request!!");
	}
	
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		System.out.println("Post Request!!");
		
		// 실제 서블릿이 동작하는 서버 경로 (Not 개발 서버)
		String realPath = req.getServletContext().getRealPath("/upload");
		
		// form 태그 중 name="fileName" 인 요청 파트
		Part filePart = req.getPart("fileName");
		
		// 요청된 파트의 전송된 파일 이름
		String fileName = filePart.getSubmittedFileName();
		
		// 입력 스트림
		InputStream fis = filePart.getInputStream();
		
		// 실제 업로드 될 경로 + 파일명
		String filePath = realPath + File.separator + fileName;
		
		// 파일 출력 스트림 (저장)
		FileOutputStream fos = new FileOutputStream(filePath);
		
		// 1024byte 씩 버퍼에 담아 읽어오는 과정
		// write(buffer, offset, length); 를 통해 읽어온 만큼만 쓰는 방법
		byte[] buf = new byte[1024];
		int size = 0;
		while((size=fis.read(buf)) != -1)
			fos.write(buf, 0, size);
		fos.close();
		fis.close();
		
		PrintWriter out = resp.getWriter();
		out.write("file uploaded to " + realPath +" successfully!");
	}
}

이렇게 작성하고 서버를 재시작 한 후 파일을 업로드하면 파일이 정상적으로 업로드되는 것을 확인할 수 있다. @MultipartConfig 어노테이션을 이용해 저장 위치와 최대 전송 용량을 지정하는 모습을 볼 수 있다.

코드 내부 주석을 참고하면 어떤 과정을 거쳐 파일을 읽어오고, 정해진 위치로 업로드하는지 자세히 알 수 있다. 궁금한 점이 생길 것 같다. 위 코드는 단일 파일만 업로드하는 예제다. 다중 파일은 어떻게 전송할 수 있을까.

간단하게 생각하면 두 가지 방법이 가능하다고 느낄 것 같다. 서로 다른 name 을 지정해 각각 받아오는 방법과 같은 name 을 지정해 배열로 받아오는 방법. 본문에서는 두 번째 예제를 살펴본다. request.getParts() 를 이용해 배열을 반환받는 예제다.

package com.jsp_example.web;

/* import ... */

@MultipartConfig(
	// location="/tmp",
	fileSizeThreshold=1024*1024,
	maxFileSize=1024*1024*10,
	maxRequestSize=1024*1024*10*10
)
@WebServlet("/utils/fileUpload")
public class FileUpload extends HttpServlet{
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		System.out.println("Get Request!!");
	}
	
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		System.out.println("Post Request!!");
		
		String realPath = req.getServletContext().getRealPath("/upload");

		// request 객체 내부 getParts() 메소드 사용
		Collection<Part> fileParts = req.getParts();
		
		for(Part filePart : fileParts) {		
			// 요청의 여러 파트(Part)들 중 name 이 filename 으로 설정된 것
			if(filePart.getName().equals("fileName")) continue;
			if(filePart.getSize() == 0) continue;

			String fileName = filePart.getSubmittedFileName();
			InputStream fis = filePart.getInputStream();
			String filePath = realPath + File.separator + fileName;
			FileOutputStream fos = new FileOutputStream(filePath);
			
			byte[] buf = new byte[1024];
			int size = 0;
			while((size=fis.read(buf)) != -1)
				fos.write(buf, 0, size);
			fos.close();
			fis.close();
		}
		
		PrintWriter out = resp.getWriter();
		out.write("file uploaded to " + realPath +" successfully!");
	}
}

위에서 얻은 경로와 파일명을 이용해 업로드 수행과 동시에 연결된 데이터베이스에 경로를 저장하고, 추후에 이를 가져와 사용할 수 있게 만들 수 있다. 이 기능까지 구현하면 간단한 게시판을 JSPJava 코드로 개발해 볼 수 있다.

개발하다보면 다양한 정책들을 세워야 함을 느끼게 된다. 저장할 때, 게시글 ID 등을 함께 삽입해 중복을 방지하는 방법 등이 예시가 될 수 있다. 이는 개발 경험을 쌓아나가며 하나씩 자기만의 방법을 터득해나가면 된다.

download

다운로드는 간단하게 <a download href="filepath"/> 과 같이 태그 안에 download 키워드를 삽입해 다운로드를 활성화 시킬 수 있다. 옛날에는 다운로드 요청에 대해 동작하는 코드를 서버에서 개발했다고 한다.

Updated:

Leave a comment