서론
신규파일과 기존파일을 구분하여 스케줄러가 신규파일인지 확인 한 후 파일 타입을 찾는 업무가 있었다.
근데, 신규 파일인지 확인을 하려면 데이터베이스가 필요했다.
뿐만 아니라 특정 디렉토리에서 반복문을 사용하여 기존 파일인지, 신규파일인지 확인하는 로직도 필요했다.
파일의 개수가 적을 때는 상관없겠지만 만약 천개, 만개라면....? 반복문을 천번, 만번을 돌면서 확인을 해야하는데 구현 전부터 성능문제가 생길 것 같았다..
그래서 찾아보기 시작하다가 Java 7에서부터 도입된 WatchService를 발견했다!!!
WatchService란?!
- Java 7부터 도입되었으며 Java NIO 패키지의 일부
- 파일 시스템에서 디렉토리의 변경 사항을 모니터링 하기 위해 사용되는 서비스
- 파일 시스템의 이벤트를 감지하고 응답하는 기능 제공
- 예를 들어, 지정한 디렉토리에 새로운 파일이 생성되거나, 기존 파일이 수정되는경우, 파일이 삭제될 때 이러한 이벤트를 감지
따라서!!!
나는 스케줄러와 반복문 없이 그냥 내가 지정해놓은 디렉토리를 계속 모니터링하고,
신규 파일이 들어왔을 때 바로 파일 타입을 구해서 반환하는 로직을 WatchService를 사용하여 작성하기로 했다.
주요 구성 요소
1. WatchService
- java.nio.file 패키지에 포함
- 파일 시스템의 특정 디렉토리를 감시하기 위해 등록(register)된 후에 변경 사항이 발생하면 이를 감지하고 이벤트를 큐에 추가
2. Watchable
- 파일 시스템에서 감시할 수 있는 객체
- 주로 'Path' 인터페이스가 이를 구현하며, Path 객체를 통해 특정 디렉토리를 감시 대상으로 설정할 수 있음
3. WatchKey
- WatchService에 등록된 감시 대상 디렉토리와 관련된 key
- 이벤트가 발생하면 WatchKey가 신호를 받게 되며, 이 키를 통해 이벤트를 검색하고 확인할 수 있음
4. WatchEvent
- 파일 시스템의 변경 사항에 대한 이벤트 객체
- 이벤트 종류에는 'ENTRY_CREATE(파일 생성), ENTRY_DELETE(파일 삭제), ENTRY_MODIFY(파일 수정) 등이 있음
완성 코드
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.nio.file.*;
@Service
@RequiredArgsConstructor
public class FileWatchService {
private WatchKey watchKey;
@PostConstruct
public void fileMonitoring() throws IOException {
String dirPath = "/Users/chaesuhyeon/Documents/test"; // 모니터링 디렉토리
// watchService 생성
try {
WatchService watchService = FileSystems.getDefault().newWatchService();
log.info("watch service started!!!");
// 경로 생성
Path path = Paths.get(dirPath);
// 해당 디렉토리 경로에 watchService와 이벤트 등록
path.register(watchService,
StandardWatchEventKinds.ENTRY_CREATE, // 파일 생성
StandardWatchEventKinds.ENTRY_MODIFY, // 파일 수정
StandardWatchEventKinds.ENTRY_DELETE); // 파일 삭제
Thread thread = new Thread(() -> {
try {
while (true) {
try {
watchKey = watchService.take(); // 이벤트가 발생하면 watchKey 객체 반환 (이벤트 발생 전까지 블로킹 상태)
} catch (InterruptedException e) {
log.error("WatchService interrupted", e);
return; // 스레드를 종료시킴
}
List<WatchEvent<?>> watchEvents = watchKey.pollEvents(); // 이벤트 목록 가져옴
for (WatchEvent<?> watchEvent : watchEvents) { // 감지된 모든 이벤트 순회
// 이벤트 종류
WatchEvent.Kind<?> kind = watchEvent.kind();
// 경로
Path paths = (Path) watchEvent.context();
System.out.println(paths.getFileName());
if (kind.equals(StandardWatchEventKinds.ENTRY_CREATE)) {
System.out.println("created something in directory");
} else if (kind.equals(StandardWatchEventKinds.ENTRY_DELETE)) {
System.out.println("delete something in directory");
} else if (kind.equals(StandardWatchEventKinds.ENTRY_MODIFY)) {
System.out.println("modified something in directory");
} else if (kind.equals(StandardWatchEventKinds.OVERFLOW)) {
System.out.println("overflow");
}
}
if (!watchKey.reset()) {
log.warn("WatchKey could not be reset. Exiting...");
watchService.close();
break; // WatchService를 닫고 루프 종료
}
}
} catch (IOException e) {
log.error("Error closing WatchService", e);
}
});
thread.setDaemon(true); // 메인 스레드가 종료되면 이 스레드도 종료됨
thread.start();
} catch (IOException e) {
log.error("Failed to initialize WatchService", e);
}
}
}
복잡해보이지만, 모니터링할 디렉토리 경로만 수정해주면 문제없이 잘 돌아갈 것이다!!
코드 설명
1. @PostConstruct 사용 이유
- 서버가 켜짐(돌아감) 과 동시에 해당 로직이 실행되어야 모니터링이 가능하므로 사용
2. 모니터링할 디렉토리 경로 지정
String dirPath = "/Users/chaesuhyeon/sweetk/doc/IITP/테스트파일";
.
.
.
Path path = Paths.get(dirPath); // 경로 생성
3. 모니터링할 이벤트 지정 (생성, 수정, 삭제 )
// 해당 디렉토리 경로에 이벤트 등록
path.register(watchService,
StandardWatchEventKinds.ENTRY_CREATE
,StandardWatchEventKinds.ENTRY_MODIFY
,StandardWatchEventKinds.ENTRY_DELETE
);
코드 실행 결과
1.
@PostConstruct를 사용했기 때문에 서버를 돌리면서 FileWatchService가 빈으로 등록되고 바로 fileMonitoring()이 실행
2.
모니터링 디렉토리를 아래 경로로 지정해뒀기 때문에 해당 디렉토리에 파일을 생성, 수정, 삭제를 해본다.
"/Users/chaesuhyeon/Documents/test";
3.
해당 디렉토리에 sample.txt 라는 파일을 생성한 후 서버 로그 확인
생성된 파일 이름과 , 파일을 생성했으므로 ENTRY_CREATE 이벤트와 일치하여 그에 해당하는 로그가 잘 출력됨을 확인
(아래는 로그 출력 코드)
System.out.println(paths.getFileName());
if (kind.equals(StandardWatchEventKinds.ENTRY_CREATE)) {
System.out.println("created something in directory");
} else if (kind.equals(StandardWatchEventKinds.ENTRY_DELETE)) {
System.out.println("delete something in directory");
} else if (kind.equals(StandardWatchEventKinds.ENTRY_MODIFY)) {
System.out.println("modified something in directory");
} else if (kind.equals(StandardWatchEventKinds.OVERFLOW)) {
System.out.println("overflow");
} else {
System.out.println("hello world");
}
파일 모니터링이 매우 잘된다 ㅎㅎㅎ
코드가 길어서 그렇지 매우 잘 작동이 되니 뿌듯하다!!!!
'BE > Spring-Boot' 카테고리의 다른 글
[SpringBoot] 동시성 처리하기 1 [재고 시스템으로 보는 동시성 처리] (0) | 2024.12.04 |
---|---|
[SpringBoot] 스케줄러(@Scheduled)가 간헐적으로 동작 안하는 문제 (0) | 2024.05.07 |
[SpringBoot] Spring Security Config에서 permitAll()에 대한 진실과 오해 (4) | 2024.01.06 |
[SpringBoot] Controller Unit Test에서 발생한 401, 403 에러를 해결해보자! (+ Spring Security) (1) | 2023.12.27 |
[JPA Auditing] 생성자/수정자 자동화 (+생성 일시/수정 일시) (0) | 2023.11.03 |