목차
Java NIO WatchService(와치서비스)
WatchService는 등록한 디렉터리에서 파일의 생성·수정·삭제를 감지할 때 사용한다. 에디터의 파일 목록 갱신, 설정 파일 변경 알림처럼 변화가 있을 수 있다는 신호가 필요한 곳에 맞다. 파일의 최신 내용이 이미 완전히 기록되었다는 보증이나 빠짐없는 변경 이력은 제공하지 않는다.
flowchart LR
A[디렉터리와 이벤트 종류 등록] --> B[변경 발생]
B --> C[WatchKey가 큐에 들어감]
C --> D[take로 키 수신]
D --> E[pollEvents로 이벤트 처리]
E --> F[reset으로 재등록 상태 전환]
F --> B
그림에서 WatchKey는 등록한 디렉터리를 나타낸다. 한 번 큐에 들어온 키에 이벤트가 여러 개 쌓일 수 있으므로 pollEvents()를 반복한다. 처리가 끝난 뒤 reset()을 호출해야 다음 변경에 대해 다시 키가 큐에 들어올 수 있다. reset()이 false면 디렉터리 삭제 등으로 등록이 무효가 된 상태다.
한 디렉터리 감시 예제
다음 코드는 Java 11 이상에서 경로를 인자로 받아 해당 디렉터리를 감시한다. 종료하려면 실행 스레드를 인터럽트하거나 프로세스를 종료한다. 이 글에서는 코드의 구조만 보여주며 파일 시스템 이벤트의 실제 순서·횟수는 환경에 따라 달라진다.
import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
public class WatchDirectory {
public static void main(String[] args) throws Exception {
if (args.length != 1) throw new IllegalArgumentException("디렉터리 경로가 필요합니다");
Path directory = Path.of(args[0]);
if (!Files.isDirectory(directory)) throw new IllegalArgumentException("디렉터리가 아닙니다");
try (WatchService watcher = FileSystems.getDefault().newWatchService()) {
directory.register(watcher, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);
while (!Thread.currentThread().isInterrupted()) {
WatchKey key = watcher.take();
for (WatchEvent<?> event : key.pollEvents()) {
if (event.kind() == OVERFLOW) {
System.out.println("이벤트 누락: 디렉터리를 다시 스캔하세요");
continue;
}
Path relative = (Path) event.context();
Path changed = directory.resolve(relative);
System.out.println(event.kind().name() + " " + changed);
}
if (!key.reset()) break;
}
}
}
}
event.context()는 보통 감시 디렉터리에 대한 상대 경로다. 전체 경로가 필요하면 directory.resolve(relative)로 결합한다. OVERFLOW에는 정상 파일 경로가 있다고 가정하지 않으므로 먼저 분기한다. 파일을 만들 때 ENTRY_CREATE와 여러 ENTRY_MODIFY가 연속해서 올 수도 있어 이벤트 하나를 파일 한 건의 완성으로 해석하면 안 된다.
누락·중복·완료 시점을 다루기
WatchService는 플랫폼과 파일 시스템 구현의 영향을 받는다. 감시 시작 전의 파일, 순간적으로 만들어졌다 지워진 파일, 네트워크 파일 시스템의 변경을 모두 감지한다고 보장할 수 없다. 이벤트가 너무 빨리 쌓이면 OVERFLOW가 발생할 수 있다. 이때는 마지막 상태를 신뢰하기보다 디렉터리를 다시 스캔해 현재 파일 목록과 대조한다.
수정 이벤트 직후 파일을 읽으면 작성 중인 중간 내용일 수 있다. 작성자가 임시 파일을 완성한 뒤 원자적으로 이름을 바꾸는 약속을 만들거나, 일정 시간 변경이 멈춘 뒤 읽는 정책을 적용한다. 하위 디렉터리까지 자동으로 재귀 감시하지 않으므로 필요하면 각 디렉터리를 별도로 등록하고 새로 생긴 디렉터리도 등록해야 한다. 이벤트 처리가 오래 걸리면 감시 루프가 밀리므로 실제 파일 분석 작업은 별도 작업 큐로 넘기고, 큐의 크기도 제한한다.
변경 자체가 업무상 절대 누락되면 안 되는 요구라면 파일 이벤트만으로 처리 완료를 기록하지 않는다. 별도 작업 상태 저장소나 주기적 전체 대조와 함께 사용한다.