Ajax를 이용해서 첨부파일을 사용하면 사용자가 게시물을 등록하거나 수정하기 전에 미리 업로드 시킨 파일들을 볼 수 있다는 장점이 있지만, 다음과 같은 문제를 남긴다.
- 첨부파일만을 등록하고 게시물을 등록하지 않았을 때의 문제 - 파일은 이미 서버에 업로드되었지만, 게시물을 등록하지 않았으므로 의미 없이 파일들만 서버에 업로드된 상황
- 게시물을 수정할 때 파일을 삭제했지만 실제로 폴더에서 기존 파일은 삭제되지 않은 문제 - 데이터 베이스에는 기존 파일이 삭제되었지만, 실제 폴더에는 남는 문제
위 상황의 공통적인 문제는 사용자가 Ajax로 어떤 작업을 한 후에 비정상적으로 브라우저를 종료하거나 페이지를 빠져나가는 문제이다. 이 문제를 해결하는 핵심은 정상적으로 사용자의 게시물에 첨부된 파일인지 아니면 사용자가 게시물을 수정할 때 업로드했지만 최종적으로 사용되는 파일인지 아닌지를 파악하는 것이 핵심이다.
1. 잘못 업로드된 파일의 정리
게시물에 필요한 모든 파일에 대한 정보는 최종적으로는 데이터베이스에 기록되어 있다. 만일 사용자가 게시물을 등록하거나 수정하기 위해서 첨부파일을 등록했지만, 최종적으로 submit을 하지 않은 경우에는 폴더에 파일들은 업로드되지만, 데이터베이스에는 아무 변화가 없게 된다.
만일 정상적으로 해당 첨부파일이 게시물에 사용되었다면 데이터베이스의 tbl_attach 테이블에 기록되어 있을 것이므로 데이터베이스와 비교하는 작업을 거쳐서 업로드만 된 파일의 목록을 찾아야 한다.
파일의 목록을 찾을 때에는 반드시 오늘 날짜가 아닌 파일들을 대상으로 해야만 한다. 만일 오늘 날짜를 대상으로 하는 경우 지금 현재 게시물을 작성하거나 수정하기 위해서 업로드하고 있는 파일들을 삭제할 가능성이 있기 때문!
어제 날짜로 등록된 첨부파일의 목록을 구함 -> 어제 업로드 되었지만, 데이터베이스에는 존재하지 않는 파일들을 찾음 -> 데이터 베이스와 비교해서 필요 없는 파일들을 삭제함!
이 작업은 주기적으로 동작해야 하므로 스케줄링을 할 수 있는 spring-batch나 Quartz 라이브러리를 이용한다.
2. Quartz 라이브러리 설정
Quartz 라이브러리는 일반적으로 스케줄러를 구성하기 위해서 사용한다. 서버를 운영하기 위해서는 간혹 주기적으로 매일, 매주, 매월 등 주기적으로 특정한 프로그램을 실행할 필요가 있다. 이 작업은 운영체제의 기능을 이용해서 작업할 수 도 있지만, 스프링과 Quartz 라이브러리를 이용하면 간단히 처리할 수 있다. (Spring Batch의 경우 많은 양의 데이터를 주기적으로 읽고 쓰는 작업에는 유용하지만 설정이 더 복잡하다.)
Quartz 기능을 활용하기 위해서는 pom.xml에 아래와 같이 라이브러리를 추가한다.
- pom.xml의 일부
Quartz에 대한 설정은 XML과 어노테이션을 활용할 수 있다. 어노테이션을 이용하기 위해서 namespace에 task에 체크하고 root-context.xml의 일부를 수정한다.
- root-context.xml
2-2. Task 작업의 처리
실제 작업의 로직은 별도로 org.zerock.task라는 패키지 내에 FileCheckTask라는 클래스를 작성해서 처리한다.
package org.zerock.task;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import lombok.extern.log4j.Log4j;
@Log4j
@Component
public class FileCheckTask {
@Scheduled(cron="0 * * * * *")
public void checkFiles()throws Exception{
log.warn("File Check Task run. . . . . . . . . . . . . .");
log.warn("==============================================");
}
}
FileCheckTask에는 @Component 어노테이션과 @Scheduled 어노테이션이 사용되었다. @Scheduled 어노테이션 내에는 cron이라는 속성을 부여해서 주기를 제어한다. 로그가 정상적으로 기록되는지 확인하기 위해서 log.warn() 레벨을 이용해서 실행중에 확인할 수 있도록 한다.
우선 FileCheckTask가 정상적으로 동작하는지 확인하기 위해서 root-context.xml에 FileCheckTask를 스프링의 빈으로 설정한다.
- root-context.xml의 일부
예제의 cron 설정은 위의 경우 매분 0초마다 한 번씩 실행되도록 지정되었으므로, 서버를 실행해 두고 1분에 한 번씩 로그가 기록되는지를 확인한다 ( 아래의 화면은 몇 분간 별도의 작업 없이 서버를 실행해둔 상태에서 기록되는 로그이다.)
3. BoardAttachMapper 수정
FileCheckTask가 정상적으로 동작하는지를 확인했다면 데이터베이스에서 어제 등록된 모든 파일의 목록이 필요하므로, BoardAttachMapper에 첨부파일 목록을 가져오는 메서드를 추가한다.
- BoardAttachMapper.java
BoardAttachMapper.xml 파일에는 아래와 같이 쿼리를 추가한다. sysdate에서 1을 빼서 어제 날짜를 처리한다.
4. cron 설정과 삭제 처리
Cron은 원래 유닉스 계열에서 사용되는 스케줄러 프로그램의 이름이지만, 워낙 많이 사용되다 보니 각종 언어나 기술에 맞는 라이브러리 형태로 많이 사용된다. 작성된 FileCheckTask 내에서는 @Scheduled(cron="0 * * * * *") 과 같이 표현식이 사용되었는데, 이때의 의미는 '매분 0초가 될때 마다 실행한다'로 알 수 있다. (601p)
4-1 파일의 목록 처리
작업의 순서는 1) 데이터베이스에서 어제 사용도니 파일의 목록을 얻어오고, 2) 해당 폴더의 파일 목록에서 데이터베이스에서 없는 파일을 찾아낸다. 3) 이후 데이터베이스에 없는 파일들을 삭제하는 순서로 구성된다.
- FileCheckTask.java
package org.zerock.task;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.zerock.domain.BoardAttachVO;
import org.zerock.mapper.BoardAttachMapper;
import lombok.Setter;
import lombok.extern.log4j.Log4j;
@Log4j
@Component
public class FileCheckTask {
@Setter(onMethod_ = {@Autowired })
private BoardAttachMapper attachMapper;
private String getFolderYesterDay() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DATE, -1);
String str = sdf.format(cal.getTime());
return str.replace("-", File.separator);
}
@Scheduled(cron = "0 0 2 * * *")
public void checkFiles() throws Exception {
log.warn("File Check Task run.............................");
log.warn(new Date());
//file list in database
List<BoardAttachVO> fileList = attachMapper.getOldFiles();
//ready for check file in directory with database file list
List<Path> fileListPaths = fileList.stream()
.map(vo-> Paths.get("C:\\upload", vo.getUploadPath(),vo.getUuid() + "_" + vo.getFileName()))
.collect(Collectors.toList());
//image file has thumnail file
fileList.stream().filter(vo -> vo.isFileType() == true)
.map(vo -> Paths.get("C:\\upload", vo.getUploadPath(),
vo.getUuid() + "_" + vo.getFileName()))
.forEach(p -> fileListPaths.add(p));
log.warn("=================================================");
fileListPaths.forEach(p -> log.warn(p));
//files in yesterday directory
File targetDir = Paths.get("C:\\upload", getFolderYesterDay()).toFile();
File[] removeFiles = targetDir.listFiles(file -> fileListPaths.contains(file.toPath()) == false);
log.warn("----------------------------------------------------");
for(File file : removeFiles) {
log.warn(file.getAbsolutePath());
file.delete();
}
}
}
FileCheckTask의 checkFiles()는 매일 새벽 2시에 동작한다. 먼저 attachMapper를 이용해서 어제 날짜로 보관되는 모든 첨부파일의 목록을 가져온다. 데이터베이스에서 가져온 파일 목록은 BoardAttachVO 타입의 객체이므로, 나중에 비교를 위해서 java.nio.Paths 목록으로 변환한다. 이때 이미지 파일의 경우에는 섬네일 파일도 목록에 필요하기 때문에 별도로 처리해서 해당 일의 예상 파일 목록을 완성한다. 코드에서는 fileListPaths라는 이름의 변수로 처리한다.
데이터베이스에 있는 파일들의 준비가 끝나면 실제 폴더에 있는 파일들의 목록에서 데이터베이스에 없는 파일들을 찾아서 목록으로 준비한다. 이 결과는 removeFiles 변수에 담아서 처리한다. 최종적으로는 삭제 대상이 되는 파일들을 삭제한다.
'Spring' 카테고리의 다른 글
[40] spring web security를 이용한 로그인 처리 - 로그인과 로그아웃 처리 (0) | 2020.01.28 |
---|---|
[39] spring web security를 이용한 로그인 처리 - spring web security 란? (1) | 2020.01.28 |
[37] 파일 업로드 처리 - 게시물의 수정과 첨부파일 (0) | 2020.01.16 |
org.springframework.jdbc.UncategorizedSQLException: ### Error updating database. Cause: java.sql.SQLException: 해당 위치에 지원되지 않는 SQL92 토큰: 31 (0) | 2020.01.10 |
[35] 파일 업로드 처리 - 게시물의 조회와 첨부파일 (0) | 2020.01.10 |