CodeGym /행동 /JAVA 25 SELF /파일 시스템 변경 감시: WatchService

파일 시스템 변경 감시: WatchService

JAVA 25 SELF
레벨 40, 레슨 4
사용 가능

1. 소개

WatchService는 Java NIO(New IO)의 일부로, 파일 시스템의 변경을 실시간으로 감시할 수 있게 해줍니다. 폴더용 경보 장치라고 생각하면 됩니다. 누군가 파일을 추가, 삭제 또는 수정하면 곧바로 알림을 받습니다. 이 기능은 Java 7의 NIO.2에서 도입되었으며, 그 전에는 폴더를 수동으로 폴링(polling)하거나 서드파티 라이브러리를 사용해야 했습니다.

WatchService의 활용처는 매우 다양합니다. 새 파일을 자동으로 처리하고, 로그를 기록하며 백업을 만들고, 폴더를 서버나 클라우드와 동기화하고, 설정 파일의 변경을 감시하는 데 도움을 줍니다.

감시할 디렉터리 등록

변경 감시를 시작하려면 다음이 필요합니다:

  1. WatchService 인스턴스를 얻는다.
  2. 대상 폴더를 등록하고 관심 있는 이벤트를 지정한다.

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())를 사용하세요.

1
과제
JAVA 25 SELF, 레벨 40, 레슨 4
잠금
새 폴더 감시자 설정 👁️‍🗨️
새 폴더 감시자 설정 👁️‍🗨️
1
과제
JAVA 25 SELF, 레벨 40, 레슨 4
잠금
핵심 디렉터리 변경 및 삭제 모니터링 🚨
핵심 디렉터리 변경 및 삭제 모니터링 🚨
1
설문조사/퀴즈
디렉터리 작업, 레벨 40, 레슨 4
사용 불가능
디렉터리 작업
파일과 디렉터리 작업
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION