- Today
- Total
개발하는 고라니
[JSP/Servlet] 파일 업로드 및 저장 본문
순수 Servlet을 이용해 파일을 업로드하고 로컬 저장소에 저장하는 것을 알아보자.
먼저 서버사이드에서 파일을 업로드하는 방법은 가장 간단하게 <form> 태그를 이용한 POST 방식이 있다. (form태그 속성 중 enctype을 반드시"multipart/form-data"로 변경) 자바스크립트와 Ajax를 이용한 방법도 있지만 이는 다음에 알아보고자 한다.
먼저 form 태그에 파일과 내용을 달아보자.
<form action="reg" method="post" enctype="multipart/form-data">
<table border="1">
<tr>
<th>제목</th>
<td colspan="3"><input type="text" name="title" value=""></td>
</tr>
<tr>
<th>첨부파일</th>
<td colspan="3">
<input type="file" name="f" multiple>
</td>
</tr>
<tr>
<td colspan="4">
<textarea cols="80" rows="20" name="content"></textarea>
</td>
</tr>
</table>
<div>
<button>저장</button>
<a href="list.jsp">취소</a>
</div>
</form>
reg라는 URL로 제목(title), 내용(content) 그리고 파일(f)가 전송될 것이다.
이를 Controller에서 받는 방법을 소개한다.
Controller에서 파일 정보 가져오기
우리는 그동안 body에 담긴 데이터든, 쿼리스트링에 붙어있는 값이든 request.getParameter()를 이용해 가져왔다. 하지만 파일은 다르다. 파일은 part로 받아와야한다.
Part part = request.getPart("f");
part.getSubmittedName();
part.getName();
...
파일을 단일 업로드 할 수도 있지만 지금은 다중 파일 업로드를 하고자 한다. 그러므로 getPart()가 아닌 getParts()를 사용할 것인데, getParts()의 반환 타입은 Collections이다. 이것을 List로 캐스팅 하도록 한다.
그리고 파일을 업로드할 때 서블릿에 대한 설정이 필요한데, 이를 web.xml에 설정할 수도 있지만, 어노테이션으로도 가능하다. 이는 꼭 필요한 설정이므로 반드시 해준다.
@WebServlet("/admin/notice/reg")
@MultipartConfig(
fileSizeThreshold = 1024*1024,
maxFileSize = 1024*1024*50, //50메가
maxRequestSize = 1024*1024*50*5 // 50메가 5개까지
)
public class RegController extends HttpServlet{
...
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
UploadUtil uploadUtil = UploadUtil.create(request.getServletContext());
List<Part> parts = (List<Part>) request.getParts();
for(Part part : parts) {
if(!part.getName().equals("f")) continue; //f로 들어온 Part가 아니면 스킵
if(part.getSubmittedFileName().equals("")) continue; //업로드 된 파일 이름이 없으면 스킵
String fileName = part.getSubmittedFileName();
uploadUtil.saveFiles(part, "", uploadUtil.createFilePath());
}
위 코드에서 UploadUtil이라는 객체를 생성했는데, 이는 파일을 로컬 저장소에 저장하기 위해 따로 만든 클래스인데, 이에 대해서는 조금 있다가 설명하도록 하겠다.
본론으로 와서 parts를 받고 이를 for 루프를 돌렸을 때 아래에 if문이 2개가 있다. 이는 반드시 이유가 있다.
- Parts는 모든 Part를 가져온다. 따라서 title에 대한 Part, content에 대한 Part, f에 대한 Part가 모두 나오기 때문에 일부를 거른다.
- 글을 쓸 때 파일을 반드시 업로드해야하는 것은 아니다. 파일을 올리지 않고 업로드하게 되면 f에 대한 Part가 올라오게 되는데, 이 때 파일이 존재하지 않음에도 파일명이 ""로 가져온다. 따라서 파일 명이 공백이면 거른다.
class UploadUtil
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import javax.servlet.ServletContext;
import javax.servlet.http.Part;
public class UploadUtil {
private String uploadPath;
private ServletContext app;
/* 생성 메서드 */
public static UploadUtil create(ServletContext app) {
UploadUtil uploadUtil = new UploadUtil();
uploadUtil.setApp(app);
uploadUtil.setUploadPath(app.getRealPath("/upload"));
return uploadUtil;
}
private void setApp(ServletContext app) {
this.app = app;
}
private void setUploadPath(String realPath) {
this.uploadPath = realPath;
}
/* 파일 저장 */
public void saveFiles(Part filePart, String folderPath) {
String realPath = this.uploadPath + File.separator + folderPath;
String filePath = realPath + filePart.getSubmittedFileName();
try(
InputStream fis = filePart.getInputStream();
OutputStream fos = new FileOutputStream(filePath);)
{
byte[] buf = new byte[1024];
int len = 0;
while((len = fis.read(buf, 0, 1024)) != -1)
fos.write(buf, 0, len);
} catch (IOException e) {
e.printStackTrace();
}
}
/*/upload 하위 폴더 경로 생성 */
public String createFilePath() {
LocalDateTime date = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
String[] paths = formatter.format(date).split("/");
String result = paths[0] + File.separator + paths[1] + File.separator + paths[2];
createFolders(result);
return result;
}
private void createFolders(String paths) {
File folders = new File(uploadPath, paths);
if(!folders.exists())
folders.mkdirs();
}
}
장황하게 클래스의 코드를 쭉 소개했는데, 사실 별 것 없다. 생성자를 만들지 않고(기본 생성자만 두고) 생성 메서드를 만들어 보았다. 이는 최근에 알게된 정적 팩토리 메서드를 나름 연습해보고자 한 것이므로 저것을 쓰지 않고 그냥 new UploadUtil 해서 객체를 생성해도 무관하다.
다만 생성메서드를 이용하면, 인자로 Application 레이어에 해당하는 객체만 주어지면 알아서 객체 속성이 설정되므로 개발자 입장에서 굉장히 편리하다. 기존 같으면 개발자가 setter를 이용해 직접 설정해주어야 하니 말이다.
createFilePath() 메서드는 파일을 저장할 때 날짜를 계층 폴더 구조로 만들어주는 메서드이다. 예를 들어 2021년 4월 10일이라면 2021/04/10이라는 폴더 계층이 만들어진다.
핵심 메서드인 saveFile() 메서드는 자바로 파일 스트림을 다뤄보았다면 낯설지 않은 코드일 것 이다. 그냥 파일 정보를 읽어 컴퓨터에 저장하는 것 이다.
Application
서블릿의 데이터 저장소는 4가지가 있다. pageContext / request / session / application이 있는데, 마지막인 application은 해당 자바 어플리케이션 전역에서 사용될 수 있는 데이터를 저장하거나 가지고 있다.
파일을 업로드할 때 어디에 저장해야하는지 망설이게 될 수 있다. 이번엔 프로젝트의 upload 폴더 내부에 저장한다고 했을 때, 이클립스의 workspace 에 있는 프로젝트 폴더에서 찾아보는게 아니라 실제 배포되는 폴더로 가야한다.
즉, 이클립스 워크 스페이스 안에 .metadata안에서 찾아야한다.
D:\Users\82102\eclipse-workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\wtpwebapps\JSPPrj\upload
이 경로를 application이 갖고있고, 이 application을 가져오기 위해서는 request.getServletContext()를 사용하면 된다.
그렇게 가져온 ServletContext의 getRealPath()는 반환값이
D:\Users\82102\eclipse-workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\wtpwebapps\JSPPrj 가 된다.
'Web > JSP-Servlet' 카테고리의 다른 글
[JSP] Ajax로 JSON 전송 (0) | 2021.05.06 |
---|---|
[JSP] JDBC (0) | 2021.04.21 |
[JSP] JSP (0) | 2021.04.14 |