1. 소개
WatchService는 Java NIO(New IO)의 일부로, 파일 시스템의 변경을 실시간으로 감시할 수 있게 해줍니다. 폴더용 경보 장치라고 생각하면 됩니다. 누군가 파일을 추가, 삭제 또는 수정하면 곧바로 알림을 받습니다. 이 기능은 Java 7의 NIO.2에서 도입되었으며, 그 전에는 폴더를 수동으로 폴링(polling)하거나 서드파티 라이브러리를 사용해야 했습니다.
WatchService의 활용처는 매우 다양합니다. 새 파일을 자동으로 처리하고, 로그를 기록하며 백업을 만들고, 폴더를 서버나 클라우드와 동기화하고, 설정 파일의 변경을 감시하는 데 도움을 줍니다.
감시할 디렉터리 등록
변경 감시를 시작하려면 다음이 필요합니다:
- WatchService 인스턴스를 얻는다.
- 대상 폴더를 등록하고 관심 있는 이벤트를 지정한다.
WatchService 가져오기
import java.nio.file.*;
WatchService watchService = FileSystems.getDefault().newWatchService();
폴더 등록
등록에는 Path 객체의 register 메서드를 사용합니다:
Path dir = Paths.get("data/uploads");
dir.register(
watchService,
StandardWatchEventKinds.ENTRY_CREATE, // 파일/폴더 생성
StandardWatchEventKinds.ENTRY_DELETE, // 파일/폴더 삭제
StandardWatchEventKinds.ENTRY_MODIFY // 파일/폴더 변경
);
설명:
- ENTRY_CREATE — 무언가가 새로 추가됨.
- ENTRY_DELETE — 무언가가 삭제됨.
- ENTRY_MODIFY — 파일이 변경됨(예: 내용을 추가 저장).
중요! WatchService는 한 번에 하나의 폴더만 감시합니다(하위 폴더는 포함하지 않음). 전체 계층을 감시하려면 각 하위 폴더를 별도로 등록해야 합니다.
2. 이벤트 처리: 대기 루프
이제 감시 설정을 마쳤으니(“이웃-관찰자”에 가깝지 “빅브라더”식은 아닙니다), 이벤트를 기다릴 수 있습니다. WatchService는 “이벤트 큐” 패턴을 구현합니다. 무언가가 발생하면 이벤트가 큐에 들어갑니다.
메인 루프
while (true) {
// 이벤트를 기다림(블로킹 호출)
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
// 이벤트 종류: 생성, 삭제, 변경
WatchEvent.Kind<?> kind = event.kind();
// 파일/폴더 이름(Path, 감시 폴더 기준의 상대 경로)
Path filename = (Path) event.context();
if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
System.out.println("파일/폴더 생성됨: " + filename);
} else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
System.out.println("파일/폴더 삭제됨: " + filename);
} else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
System.out.println("파일/폴더 변경됨: " + filename);
}
}
// 반드시 키를 reset해야 합니다. 그렇지 않으면 감시가 중단됩니다!
boolean valid = key.reset();
if (!valid) {
break; // 폴더를 사용할 수 없음. 종료.
}
}
동작 원리
- WatchService.take() — 이벤트가 올 때까지 스레드를 블록합니다(논블로킹 모드가 필요하면 poll()을 사용할 수 있습니다).
- key.pollEvents() — 누적된 모든 이벤트 목록.
- event.context() — 변경된 파일 또는 폴더의 이름(감시 디렉터리 기준의 상대 경로).
- 이벤트를 처리한 뒤에는 반드시 key.reset()을 호출합니다. 폴더가 삭제되었거나 접근할 수 없게 되면 reset()은 false를 반환하며, 그 경우 루프를 종료하면 됩니다.
전체 예제: "data/uploads" 폴더 감시
학습용 애플리케이션에 다운로드 폴더에 대한 간단한 “경보”를 추가해 봅시다:
import java.nio.file.*;
import java.io.IOException;
public class WatcherDemo {
public static void main(String[] args) throws IOException, InterruptedException {
Path dir = Paths.get("data/uploads");
if (!Files.exists(dir)) {
Files.createDirectories(dir);
}
WatchService watchService = FileSystems.getDefault().newWatchService();
dir.register(
watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY
);
System.out.println("폴더 감시 시작: " + dir.toAbsolutePath());
while (true) {
WatchKey key = watchService.take(); // 이벤트를 대기
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
Path filename = (Path) event.context();
System.out.printf("[%s] %s\n", kind.name(), filename);
}
boolean valid = key.reset();
if (!valid) {
System.out.println("폴더를 사용할 수 없어 감시를 종료합니다.");
break;
}
}
}
}
시도해 보세요: 이 코드를 실행한 뒤 "data/uploads" 폴더에서 파일을 생성, 삭제 또는 수정해 보세요. 프로그램이 즉시 반응합니다!
3. WatchService의 제약과 특징
한 번에 한 폴더만, 하위 폴더 제외
WatchService는 등록한 폴더만 감시합니다. 그 안의 하위 폴더에서 일어나는 변경은 감지되지 않으므로 — 각 하위 폴더를 개별적으로 등록해야 합니다.
어떻게 해야 할까?
전체 계층을 감시하려면 모든 하위 폴더를 순회하며 하나씩 등록해야 합니다. 예를 들어 새 하위 폴더가 생성되면 즉시 그 폴더도 등록하세요.
운영체제별 특징
Windows: WatchService는 비교적 안정적으로 동작하지만, 때때로 여러 이벤트가 하나로 “합쳐져” 들어올 수 있습니다(예: 큰 파일 복사 시).
Linux/macOS: 구현은 시스템 메커니즘(inotify, kqueue)을 기반으로 합니다. 때로는 이벤트가 지연되어 오거나, 반대로 너무 많이 발생할 수 있습니다(예: 매번 저장할 때 ENTRY_MODIFY가 발생).
이름 기반 이벤트만 제공
WatchService는 변경된 객체의 이름(감시 폴더 기준의 상대 경로)만 알려 줄 뿐, 파일 내부에서 무엇이 바뀌었는지는 제공하지 않습니다. 정확히 무엇이 변경되었는지 알아야 한다면 파일을 직접 읽어 확인하세요.
부하 시 이벤트 손실 가능
짧은 시간에 너무 많은 변경이 발생하면(예: 수천 개 파일 대량 복사) 이벤트 큐가 가득 차 일부 이벤트가 유실될 수 있습니다. 중요한 작업이라면 추가적인 검증을 도입하는 것이 좋습니다.
4. 실용 예제
새 파일 자동 처리
예를 들어, "photos/incoming" 폴더에 새로 들어오는 이미지를 자동으로 처리해야 하는 프로그램을 만든다고 합시다.
Path dir = Paths.get("photos/incoming");
WatchService watchService = FileSystems.getDefault().newWatchService();
dir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);
while (true) {
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
Path filename = (Path) event.context();
if (filename.toString().endsWith(".jpg")) {
System.out.println("새 사진: " + filename);
// 여기서 복사, 압축, 분석 등 처리를 추가할 수 있습니다.
}
}
}
key.reset();
}
간단한 변경 로거 구현
모든 이벤트를 별도의 로그 파일에 저장할 수 있습니다:
import java.nio.file.*;
import java.io.*;
import java.time.LocalDateTime;
public class SimpleLogger {
public static void main(String[] args) throws IOException, InterruptedException {
Path dir = Paths.get("logs/monitored");
Files.createDirectories(dir);
Path logFile = Paths.get("logs/changes.log");
try (BufferedWriter writer = Files.newBufferedWriter(logFile, StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {
WatchService watchService = FileSystems.getDefault().newWatchService();
dir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE);
System.out.println("감시 중: " + dir);
while (true) {
WatchKey key = watchService.take();
for (WatchEvent<?> event : key.pollEvents()) {
String log = String.format("%s [%s] %s\n",
LocalDateTime.now(), event.kind().name(), event.context());
writer.write(log);
writer.flush();
System.out.print(log);
}
key.reset();
}
}
}
}
새 하위 폴더 생성 감지(및 즉시 등록)
감시 중인 폴더에 새 하위 폴더가 생성되면, 즉시 그 폴더를 등록하여 계속 모니터링할 수 있습니다:
if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
Path createdPath = dir.resolve((Path) event.context());
if (Files.isDirectory(createdPath)) {
createdPath.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
System.out.println("새 하위 폴더 감시 시작: " + createdPath);
}
}
5. 중요한 포인트와 흔한 실수
오류 №1: key.reset() 호출 누락. 이벤트를 처리한 뒤 키를 리셋하지 않으면 폴더 감시가 중지되어 더 이상 이벤트를 받지 못합니다. 초보자가 자주 겪는 “함정”입니다. 처음엔 잘 동작하는 것 같다가 갑자기 프로그램이 조용해집니다.
오류 №2: 예외 무시. 파일 시스템 작업에는 항상 변수가 많습니다. 폴더가 삭제되거나, 디스크가 분리되거나, 권한이 바뀔 수 있습니다. 예외(IOException, ClosedWatchServiceException)를 처리하지 않으면 프로그램이 비정상 종료될 수 있습니다.
오류 №3: 한 폴더만 감시한다고 착각. 폴더를 등록하면 모든 하위 폴더도 함께 감시된다고 기대하는 경우가 많지만, 그렇지 않습니다! 전체 구조를 감시하려면 재귀 등록을 구현하세요.
오류 №4: 메인 스레드 블로킹. WatchService.take()는 이벤트가 올 때까지 스레드를 블록합니다. 메인 스레드가 다른 작업도 해야 한다면, 감시를 별도의 스레드에서 실행하세요.
오류 №5: 부하가 높을 때 이벤트 유실. 폴더에서 변경이 너무 많이 발생하면 이벤트 큐가 가득 찰 수 있습니다. 중요 애플리케이션의 경우 폴더 상태를 주기적으로 대조하는 로직을 구현하는 것이 좋습니다(예: 1분마다 파일 목록 비교).
오류 №6: 상대 경로 처리 실수. event.context()는 감시 폴더 기준의 상대 파일 이름을 반환합니다. 절대 경로가 필요하면 dir.resolve((Path) event.context())를 사용하세요.
GO TO FULL VERSION