Day 82. Spring Framwork 5 : 파일 관련 기능

ho_c·2022년 6월 26일
0

국비교육

목록 보기
64/71
post-thumbnail

Spring File upload, download

이번 시간엔 스프링을 이용해서 파일 업로드-다운로드 기능을 다뤄봤다.
( 너무 진행이 느려서 이틀치라는 건 안 비밀)

먼저 중점은 파일 업로드을 위해 multi-part/form-data라는 전달 타입을 사용하면, request를 서버 측에서 분석할 수 없다는 것이다. 그래서 세미 때는 Cos.jar를 라이브러리를 사용해서 처리했다.

이 Cos.jar 라이브러리는 편의성이 높지만, 문제는 동일 name값이 들어가면 업로드는 되어도, 파일 정보를 뽑아낼 수 없는 한계가 있다. 그래서 스프링으로 넘어와서는 Apache-fileupload를 사용해서 업로드 기능을 구현하고자 한다.


1. Maven dependency 추가

1.4 ver : https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload/1.4

<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version>
</dependency>

그러면 Maven Dependencies에 commons-fileupload-1.4.jarcommos-io-2.2.jar 가 추가된다. 이렇게 추가된 라이브러리는 스프링에서 지원해서 별다른 설정 없이 bean으로 스프링 컨테이너 등록해주면 된다.

2. Servlet-context에 multipartResolver 추가

앞서 말한 것처럼 몇 가지 특정한 라이브러리들은 @Autowired나 @Component처럼 우리가 DI 세팅을 따로 해서 사용할 필요 없이, 스프링이 알아서 인식해 사용한다. 그래서 컨테이너 안에만 넣어주면 되고, 이를 다루기 위해 multipartResolver가 필요하다.

<beans:bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
	<beans:property name="maxUploadSize" value="10485760"></beans:property>
</beans:bean>

3. upload

이제 우리가 사용하는 request는 자동으로 스프링컨테이너에 라이브러리 빈에 의해 multi-part로 업그레이드 된다. 때문에 컨트롤러 매개변수로 쉽게 받을 수 있다.

하지만 실제 파일은 업로드 과정에서 임시 저장소에 들어간다. 그래서 따로 우리가 저장소에 로직이 끝나기 전에 저장해줘야 한다.


0) RealPath

업로드 되는 파일은 프로젝트 파일이 아닌, 서버가 가동되는 RealPath에서 저장되고 다운로드 된다. 따라서 조작을 위해 RealPath 경로를 가져와야 한다.

1) Session에서 가져오기

@Autowired
private HttpSession session; 

2) 경로 잡기(생성)

String realPath = session.getServletContext().getRealPath("upload"); // 파일 업로드 경로 설정
		
File realPathFile = new File(realPath); // 업로드 경로를 파일 객체로 생성
		
if(!realPathFile.exists()) {realPathFile.mkdir();}; // 경로가 없으면 생성 / 자동완성 안되면 mkdir() 쓰셈

3) 파일명 설정

파일명은 DB에 저장해서 데이터를 분류하는 용도로 사용하거나, 다운로드 시 특정 파일을 선택할 수 있는 기능을 한다. 그래서 Cos.jar에선 파일명이 겹치지 않는 메서드를 지원했지만,
Apache에는 중복되는 이름을 변경하는 기능이 없어 애초에 겹치지 않게 만들어줘야 한다.

String oriName = file.getOriginalFilename(); // 클라이언트에서 보여질 이름
String sysName = UUID.randomUUID()+"_"+oriName; // 해당 메서드는 중복되지 않는 랜덤한 String값을 만들어 반환한다.

4) 파일 저장 (저장소로 보관)

file.transferTo(new File(realPath+"/"+sysName));

5) DB 작업

이렇게 들어온 파일들은 데이터 관리를 위해 DB에 파일 정보를 저장한다. 이를 위해 root-context.xml에서 DBCP, Spring JDBC의 빈을 생성하고 DI작업, DAO의 @Repository설정, JdbcTemplate @Autowired 설정, DTO 생성을 처리하자.

라이브러리는 Maven Repository에서 가져온다.

[ pom.xml ]

<dependency>
    <groupId>com.oracle.database.jdbc</groupId>
    <artifactId>ojdbc8</artifactId>
    <version>21.1.0.0</version>
</dependency>
		
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-dbcp2</artifactId>
    <version>2.9.0</version>
</dependency>
		
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>${org.springframework-version}</version>
</dependency>

[ FileDAO ]

@Repository
public class FilesDAO {
	
	@Autowired
	private	JdbcTemplate jdbc;
	
	public int insert(FilesDTO dto) throws Exception{
		String sql = "insert into files values(files_seq.nextval, ?, ?, ?)";
		return jdbc.update(sql, dto.getOri_name(), dto.getSys_name(),dto.getParent_seq());
	}
}

[ FileDTO ]

public class FilesDTO {
	
	private int seq;
	private String ori_name;
	private String sys_name;
	private int parent_seq;
	
	public FilesDTO() { }
}
// 명시 생성자랑 Setter, Getter는 알아서...ㅎ

다시 FileController로 돌아와서, DB에 정보입력 작업을 마무리한다. 만약 실행 후에 에러가 난다면 Servlet-context에서 component-scan을 제대로 설정했는지 확인해보자.


6) 여러 개의 옵션 추가 multiple

아파치 라이브러리와 스프링으로 인해 이제 프론트에서 여러 개의 파일을 올릴 수 있다.
먼저 파일을 여러 개 업로드 하기 위해선 multiple 조건을 걸어주면 된다.

<input type=file name=file multiple>

이제 업로드는 여러 개가 된다. 이를 컨트롤러에서 받아줘야 하는데, 기존처럼 MultipartFile 인스턴스 하나로는 다수의 파일을 담을 수 없다. 따라서 배열을 이용하여 담아준다.

@RequestMapping("upload")
public String upload(String writer, String message, MultipartFile[] file) throws Exception{
		
	String realPath = session.getServletContext().getRealPath("upload"); 
		
	File realPathFile = new File(realPath); 
		
	if(!realPathFile.exists()) {realPathFile.mkdir();}; 
	
	// forEach로 반복 작업을 해주면 된다.	
	for(MultipartFile mf : file) {
			
		String oriName = mf.getOriginalFilename();
		String sysName  = UUID.randomUUID()+"_"+oriName;
			
		mf.transferTo(new File(realPath+"/"+sysName));

		dao.insert(new FilesDTO(0, oriName, sysName, 0)); // DB작업
	}
		
	return "redirect:/";
}

4. download

먼저 프론트에 저장된 파일들의 이름을 꺼내올 수 있도록 UI를 만들어준다.

<fieldset>
	<legend>File List</legend>
	<button id=fileList>파일목록</button>	
</fieldset>

디자인은 잼병이라...대충 만들어 준다.

이제 ajax를 이용해서 저 위에 ‘파일목록’버튼을 누를 때마다, DB에서 정보를 꺼내오게 로직을 짜줄 것이다. 그리 어렵진 않으니 간단히 해보자.

[ ajax ]

$("#fileList").on("click", function(){
	$.ajax({
		url: "/file/fileList",
		dataType : "json"
	}).done(function(resp){
			
		for(let i=0; i<resp.length; i++){
				
			let fileLine = $("<div>");
			fileLine.addClass  = "fileItem";
				
			let fileAnker = $("<a>");
			fileAnker.attr("href", "/file/download?ori_name="+resp[i].ori_name+"&sys_name="+resp[i].sys_name);
			fileAnker.text(resp[i].ori_name);
				
			fileLine.append(fileAnker);
			$("#fileList").after(fileLine);
				
		}	
	});
});

해당 요청은 fileList라는 URL로 요청을 하면 컨트롤러에서 JSON 형태로 응답하도록 만들었다. 그리고 그 응답 내용을 동적 바인딩으로 프론트의 id=fileList 버튼 뒤로 요소가 들어가게 설정하고 <a> 태그의 속성도 추가해서 다운로드 요청도 설정해놨다.

[ Controller ]

@ResponseBody
@RequestMapping("fileList")
public String fileList() throws Exception{
		
	Gson g = new Gson();
		
	List<FilesDTO> list = dao.getList();
		
	String result = g.toJson(list);
		
	return result;
}

응답은 JSON으로 보내기 위해서 Maven Repository에서 GSON 2.9.0버전을 가져왔다. 음..그리고 미리 bean으로 생성해서 사용할까 했지만, ajax 쓰는 메서드가 한 개 뿐이라 그냥 new해버렸다.

이렇게 GSON을 활용하면, 객체 배열도 쉽게 JSON의 형태로 직렬화해서 프론트로 보내줄 수 있다.


1) Jackson

하지만 Spring은 GSON보드는 jackSon을 더 권장한다. 위 과정을 Jackson으로 한번 바꿔서 진행해보자. 먼저 기존의 GSON dependency는 지워주고, Maven Repository에 들어가 jackson-databind 최신 버전을 받아준다.

그리고 컨트롤러의 내용을 다음과 바꿔준다.

@ResponseBody
@RequestMapping("fileList")
public List<FilesDTO> fileList() throws Exception{
		
	List<FilesDTO> list = dao.getList();
		
	return list;
}

Jackson은 Spring에서 공식지원하기 때문에 별다른 과정이 필요 없다. List와 같은 형태로 메서드 반환을 해주면, 자동으로 Jackson에서 직렬화 처리해준다. 그래서 굳이 새로운 객체를 사용하지 않는 이상 Jackson이 편하고, 임의의 객체를 만들어 처리하면 GSON이나, ObjectMapper를 사용하기를 추천한다.


2) 다운로드 컨트롤러

Spring에서 파일 다운로드는 DS를 거치지 않고, 파일 스트림을 그냥 열어서 돌려주기 때문에 반환값은 void로 해도 괜찮다. 그리고 들어오는 값을 받을 name과 스트림을 돌려줄 reponse 객체도 미리 준비해둔다.

(1) 경로 가져오기

파일을 저장한 경로를 서버의 리얼패스에서 끌어온다.

String realPath = session.getServletContext().getRealPath("upload");

(2) 파일 타겟팅
클라이언트에서 요청한 파일의 정보를 파일 객체에 담아 준다. 한글 파일의 경우엔 깨지지 않도록 인코딩 처리를 해준다.

File targetFile = new File(realPath+"/"+sys_name); 
ori_name = new String(ori_name.getBytes("utf8"), "ISO-8859-1");

(3) response 객체 설정

  • 먼저 객체의 용도가 바뀌었기 때문에 설정을 초기화 한다.
  • 클라이언트에서 다운 받을 때, 해당 파일의 이름을 세팅해준다.
response.reset();
response.setHeader("content-disposition", "attachment;filename=\""+ori_name+"\";");

(4) Stream을 열고 보내기

먼저 try-with-resource로 스트림이 자동으로 닫히도록 틀을 세워 준다. 그리고 input, output 스트림을 다루기 편하도록 업그레이드 시켜준 뒤, 메모리에 쓰고 전송한다.

		try(
				DataInputStream dis = new DataInputStream((new FileInputStream(targetFile))); // 객체를 메모리로 올리는 길
				DataOutputStream dos = new DataOutputStream(response.getOutputStream()); // Servlet outputStream을 불러와 업그레이드해서 내보낼 준비
				){
			byte[] fileContents = dis.readAllBytes(); // 파일을 담을 배열을 만들주고
			dos.write(fileContents); // // 메모리에 쓴 뒤
			dos.flush(); // 메모리에 있는 내용을 전송하고 버퍼를 비움
		}
profile
기록을 쌓아갑니다.

0개의 댓글