[Java] 특정 디렉토리 및 파일 모니터링
by 배부른코딩로그💡 생각보다 자주 사용하게 돼서, 사용했던 경험을 바탕으로 기록을 해보자.
목표
- 자바를 통해 파일을 컨트롤할 수 있다.
- WatcherService에 대해 설명할 수 있다.
- WatcherService를 통해 파일의 변화를 감지하고 이에 따른 액션을 구현할 수 있다.
- 유용한 예제를 기록하는 공간으로 활용한다.
간단한 배치 프로그램이나 서버 설정 관련 정보들은 보통 DB가 아닌 파일(File)에 저장된다.
그리고, 특별한 경우에는 특정 파일을 주기적으로 모니터링하여, 변경 이벤트가 발생되면 Reload하는 작업이 필요하다는 요구사항이 생각보다 빈번하게 발생한다.
이를 어떻게 구현해야 할까?
JAVA 1.7부터 제공하는 java.nio.file의 WatchService 인터페이스를 이용하자!!
본론으로 들어가기 전에 File과 Directory 등의 리스트업하는 기본적인 방법부터 살펴보자.
final String DIRECTORY = "C:\\work\\blog\\";
// ROOT
// │ file1.txt
// │ file2.txt
// │
// ├─dir1
// │ file3.txt
// │ file4.txt
// │
// └─dir2
// │ file5.txt
// │ file6.txt
// │
// └─dir3
// file7.txt
// file8.txt
File 클래스
import java.io.File
File.class 다양한 메서드를 지원하지만,
이번에는 디렉토리와 파일 관련된 메서드들을 활용하는 예제만 기록하려 한다.
File.listFiles()
파일 객체에 선언된 경로의 파일 목록을 가져오는 메서드이다.
public void recursiveFileNavigation(String dirPath) {
File dir = new File(dirPath);
if (!dir.isDirectory()) {
return;
}
File files[] = dir.listFiles();
for (int i = 0; i < files.length; i++) {
File file = files[i];
if (file.isDirectory()) {
recursiveFileNavigation(file.getPath());
} else {
System.out.println("file: " + file);
}
}
}
recursiveFileNavigation(DIRECTORY);
file: \ROOT\dir1\file3.txt
file: \ROOT\dir1\file4.txt
file: \ROOT\dir2\dir3\file7.txt
file: \ROOT\dir2\dir3\file8.txt
file: \ROOT\dir2\file5.txt
file: \ROOT\dir2\file6.txt
file: \ROOT\file1.txt
file: \ROOT\file2.txt
위와 같이 재귀적인 함수를 이용해서 해당 디렉토리의 디렉토리와 파일 목록을 가져올 수 있다.
추가적으로 특정 파일명 규칙을 가진 파일만을 가져오고 싶다면, FilenameFilter를 이용하자.
public static void recursiveFileNavigation(String dirPath, FilenameFilter filter) {
File dir = new File(dirPath);
if (!dir.isDirectory()) {
return;
}
File files[] = dir.listFiles(filter);
for (int i = 0; i < files.length; i++) {
File file = files[i];
if (file.isDirectory()) {
recursiveFileNavigation(file.getPath(), filter);
} else {
System.out.println("file: " + file);
}
}
}
FilenameFilter filter = new FilenameFilter() {
public boolean accept(File f, String name) {
return name.startsWith("file");
}
};
recursiveFileNavigation(DIRECTORY, filter);
// file: /ROOT/file1.txt
파일명 기준으로 Filter를 적용 가능하며, accept를 재정의하여 입맛대로 특정 파일들을 리스트업 할 수 있다.
Stream을 통해 걸러도 되지만, listFiles는 내부적으로 ArrayList에 담았다가 이를 또 toArray로 준다.
Stream을 쓰려면 이걸 또 Stream화하고, 필터링하고, 다시 toCollector 하면 뭔가 비효율적이다.
굳이 쓸 필요없는 곳에 Stream을 할 필요는 없다.
Files.walk()
기가 막히게 Java 8 부터는 Stream을 적극적으로 사용할 수 있도록 개선한 듯 하다.
Files 클래스의 walk() 메서드를 통해 쉽게 리스트업이 가능하다. (browe~ walk~ 이름 잘 짖네~)
try (Stream<Path> walk = Files.walk(Paths.get(DIRECTORY))) {
walk
.filter(p -> !Files.isDirectory(p)) // not a directory
.map(p -> p.toString().toLowerCase()) // convert path to string
.filter(f -> f.endsWith(".txt")) // check end with
.forEach(filename -> System.out.println(filename));
}
\ROOT\dir1\file3.txt
\ROOT\dir1\file4.txt
\ROOT\dir2\dir3\file7.txt
\ROOT\dir2\dir3\file8.txt
\ROOT\dir2\file5.txt
\ROOT\dir2\file6.txt
\ROOT\file1.txt
\ROOT\file2.txt
위와 같은 결과를 얻을 수 있다.
WatchService 인터페이스
import java.nio.file.WatchService
자바에서 기본적으로 제공하는 API이며, Java 1.7 이후로 사용이 가능하다.
WatchService 인터페이스는 특정 디렉토리를 감시하고 변경 이벤트 발생시, 지정한 액션을 수행하도록 도와준다.
👀 Try it out
watcherService = FileSystems.getDefault().newWatchService();
watcherKey = watcherService.poll();
Paths
.get(monitorDirectory)
.register(watcherService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY);
do {
try {
watchKey = watchService.take(); //이벤트 대기(Blocking)
} catch (InterruptedException e) {
e.printStackTrace();
}
for (WatchEvent<?> event : watcherKey.pollEvents()) {
WatchEvent<Path> watchEvent = (WatchEvent<Path>) event;
String filename = watchEvent.context().getFileName().toString();
if (!isValidExtension(filename)) {
continue;
}
Kind<Path> eventKind = watchEvent.kind();
LOG.info("└ Occurred [ " + eventKind + " ] event from " + filename);
if (eventKind.equals(StandardWatchEventKinds.ENTRY_CREATE)) {
doCreation(filename);
continue;
}
if (eventKind.equals(StandardWatchEventKinds.ENTRY_DELETE)) {
doDeletion(filename);
continue;
}
if (eventKind.equals(StandardWatchEventKinds.ENTRY_MODIFY)) {
doModification(filename);
continue;
}
}
valid = watcherKey.reset();
Thread.sleep(monitorPollingInterval * 1000); // milliseconds
} while(valid);
try {
watchService.close();
} catch (IOException e) {
e.printStackTrace();
}
필수 구현해야 하는 순서는 다음과 같다.
- FileSystems의 WatchService 생성
- Paths에 모니터링 대상인 디렉토리 경로 설정
- 대상 Paths에 생성한 WatchService와 감지 대상 Events 등록
- WatchKey로 WatchService에 이벤트가 오면, 원하는 액션취하기
원하는 액션은 요구사항에 따라 적절하게 변경해주면 된다.
필자의 경우, SFTP 서버 간에 파일을 싣어 나르는 SFTP Agent를 구현했었고, SFTP Source/Target Server 정보들을 파일 단위로 관리를 하게 되었다. 굳이 DB라는 또 다른 connection resource를 만들기 보다 좀 더 가볍게 만들고자 했다.
이에 대한 구현은 다음에 정리하고자 한다..!!
🔥 Caution
WatchService를 구현시 주의해야할 점은 다음과 같다.
- WatchKey는 일회성이다.
WatchKey는 사용 후 반드시 reset() 해줘야 다시 이벤트를 받을 수 있다.
reset() 메서드는 올바르게 리셋되면 이벤트를 다시 감시할 수 있고 true를 리턴한다.
단, 감지하던 디렉토리가 없어지는 등의 예외 케이스가 발생하면 false를 리턴한다.
Exception 발생시, WatchService역시 I/O이기 때문에 watchService도 close() 시켜줘야 자원낭비가 없다. - WatchService는 이벤트 방식이다.
- 한 번에 여러 개의 이벤트가 발생할 수 있다.
파일의 생성 / 수정 / 삭제 등 동시 다발적인 이벤트가 발생할 수 있다.
만약, 기존의 파일의 이름을 변경되거나, 파일 수정이 여러 곳에서 일어나는 등의 케이스이다. - 파일을 지정할 수 없다.
감지하는 경로로 반드시 디렉토리를 지정해야한다. 하나의 파일만 감지하는 것은 없는 것 같다. 파일을 감지하고 싶을 때는 위에 event.context()로 이벤트가 발생한 파일 이름을 가져올 수 있으니 따로 처리해야 할 것 같다. - 무한 루프를 주의해야 한다.
이벤트가 감지 범위 내 파일을 수정하면 어떻게 될까?
이벤트 발생 ⇌ 파일 수정 ⇌ 이벤트 발생 무한 루프가 돌면서 PC 자원을 갉아먹을 것이다. - 디렉토리의 서브 디렉토리(하위 디렉토리)에 변경 사항도 감지한다.
서브 디렉토리의 변경도 감지된다.
주의하지 않으면, 위와 같은 무한루프 혹은 의도치 않은 이벤트 발생으로 원하지 않는 액션을 경험할 것이다.
출처
- 특정 디렉토리의 파일 리스트 탐색 및 출력하는 방법, codechacha, 2020-04-15
- Java WatchService API vs Apache Commons IO Monitor, Baeldung, 2021-08-06
- Watching a Directory for Changes, Oracle Java Docs
- Implementing renaming and deletion in java watchservice, SubOptimal, 2015-03-31
Last Updated. 2022. 06. 10.
'Java' 카테고리의 다른 글
[Java] 코드 실행시간 측정 (0) | 2023.03.22 |
---|---|
[Java] Properties 한글 깨짐 이슈 (0) | 2022.10.25 |
[Java] 파일 입출력 소화하기 (0) | 2022.06.09 |
[Java] Lombok 소화하기 (0) | 2022.06.07 |
[Java] Null Safe한 자바 컬렉션 정렬 (0) | 2022.05.24 |
블로그의 정보
배부른코딩로그
배부른코딩로그