특정 디렉터리에 새 파일이 생기거나 로그 파일이 갱신될 때마다 즉시 반응해야 하는 스크립트를 짤 때, 가장 손쉬운 방법은 os.listdir()이나 파일의 mtime을 주기적으로 비교하는 폴링(polling)이다. 하지만 폴링 주기를 짧게 잡으면 CPU를 계속 깎아먹고, 길게 잡으면 반응이 느려진다. 리눅스 커널에는 이런 문제를 근본적으로 해결하는 inotify라는 파일시스템 이벤트 알림 서브시스템이 있다. 이 글에서는 Python에서 inotify를 이용해 파일 생성·수정·삭제 이벤트를 실시간으로 감시하는 방법을 정리한다.
inotify란
inotify는 파일이나 디렉터리에 발생하는 이벤트(생성, 삭제, 수정, 이동, 권한 변경 등)를 커널이 직접 감지해서 알려주는 리눅스 전용 커널 인터페이스다. 폴링과 달리 이벤트가 실제로 발생했을 때만 애플리케이션을 깨우기 때문에 지연도 없고 불필요한 CPU 낭비도 없다. 다만 리눅스에만 있는 기능이라 macOS나 Windows에서는 동작하지 않는다.
Python 표준 라이브러리에는 inotify 바인딩이 없어서 서드파티 패키지가 필요하다. 대표적인 선택지는 세 가지다.
| 패키지 | 특징 |
|---|---|
| inotify_simple | ctypes로 inotify 시스템 콜을 그대로 감싼 경량 래퍼. API가 단순하고 최근까지 유지보수됨 |
| pyinotify | 과거 널리 쓰였지만 오랫동안 업데이트가 없어 최신 Python 환경에서는 권장하지 않음 |
| watchdog | 리눅스 외 macOS/Windows까지 지원하는 고수준 래퍼. 내부적으로 리눅스에서는 inotify를 사용 |
리눅스 서버에서만 동작하면 되고 raw 이벤트를 다루고 싶다면 inotify_simple이 가장 다루기 쉽다. 이 글에서도 이 패키지를 기준으로 설명한다.
설치 및 기본 사용법
pip install inotify-simple
디렉터리 하나를 감시하며 생성·삭제·수정 이벤트를 출력하는 가장 단순한 예제다.
from inotify_simple import INotify, flags
inotify = INotify()
watch_flags = flags.CREATE | flags.DELETE | flags.MODIFY
wd = inotify.add_watch('/var/log/myapp', watch_flags)
print(f"감시 시작: watch descriptor={wd}")
while True:
for event in inotify.read():
for flag in flags.from_mask(event.mask):
print(f"{event.name}: {flag.name}")add_watch()는 감시할 경로와, 관심 있는 이벤트를 비트 OR로 묶은 마스크를 받아 watch descriptor를 반환한다. read()는 기본적으로 이벤트가 생길 때까지 블로킹되며, 발생한 이벤트를 Event(wd, mask, cookie, name) 형태의 namedtuple 제너레이터로 돌려준다. flags.from_mask()로 마스크 값을 사람이 읽을 수 있는 플래그 목록으로 변환할 수 있다.
실제로 파일을 만들고 지워보면 다음처럼 출력된다.
$ touch /var/log/myapp/test.log
test.log: CREATE
$ echo "hello" >> /var/log/myapp/test.log
test.log: MODIFY
$ rm /var/log/myapp/test.log
test.log: DELETE
파일 쓰기 완료 감지와 rename 추적
로그 수집기나 업로드 감시처럼 “파일 내용이 확정된 시점”을 알아야 하는 경우 MODIFY만으로는 부족하다. 파일이 열려서 여러 번 쓰이는 동안 MODIFY가 계속 발생하기 때문이다. 이럴 때는 파일이 쓰기 모드로 닫히는 순간을 알려주는 CLOSE_WRITE를 쓴다. 또한 애플리케이션이 임시 파일에 다 쓴 뒤 최종 이름으로 rename하는 패턴(atomic write)도 흔한데, 이때는 MOVED_FROM/MOVED_TO 이벤트가 같은 cookie 값을 공유해서 온다.
from inotify_simple import INotify, flags
inotify = INotify()
watch_flags = flags.CLOSE_WRITE | flags.MOVED_FROM | flags.MOVED_TO
inotify.add_watch('/var/uploads', watch_flags)
pending_moves = {}
while True:
for event in inotify.read():
names = [f.name for f in flags.from_mask(event.mask)]
if 'MOVED_FROM' in names:
pending_moves[event.cookie] = event.name
elif 'MOVED_TO' in names:
old_name = pending_moves.pop(event.cookie, None)
print(f"rename: {old_name} -> {event.name}")
elif 'CLOSE_WRITE' in names:
print(f"쓰기 완료: {event.name}")cookie는 같은 rename 동작에서 발생한 MOVED_FROM과 MOVED_TO를 짝지어주는 값이다. 이걸로 “어떤 파일이 어떤 이름으로 바뀌었는지”까지 정확히 추적할 수 있다.
주의사항
재귀 감시를 지원하지 않는다: inotify는 등록한 디렉터리 자체의 직계 항목만 감시한다. 하위 디렉터리까지 감시하려면 각 디렉터리마다 add_watch()를 따로 호출해야 하고, 감시 중 새 하위 디렉터리가 생기면(CREATE + ISDIR) 그 디렉터리에 대한 watch도 동적으로 추가해줘야 한다.
watch 개수에는 커널 제한이 있다: 감시 대상 디렉터리가 많으면 fs.inotify.max_user_watches sysctl 한도에 걸려 add_watch()가 ENOSPC 에러를 낼 수 있다. 현재 값은 sysctl fs.inotify.max_user_watches로 확인하고, 필요하면 /etc/sysctl.d/에 설정을 추가해 늘린다.
이벤트 큐 오버플로우: 이벤트가 애플리케이션이 처리하는 속도보다 훨씬 빠르게 쌓이면 커널이 일부 이벤트를 버리고 Q_OVERFLOW 플래그를 보낸다. 이 경우 어떤 이벤트가 유실됐는지는 알 수 없으므로, 오버플로우가 감지되면 감시 대상 디렉터리를 다시 한번 os.listdir()로 훑어 상태를 재동기화하는 로직을 함께 넣는 게 안전하다.
네트워크 파일시스템은 지원이 불완전하다: NFS처럼 원격 마운트된 파일시스템은 로컬 파일시스템과 달리 inotify 이벤트가 제대로 전달되지 않거나 아예 오지 않을 수 있다. 이런 경로를 감시해야 한다면 폴링으로 폴백하는 것도 고려해야 한다.
마무리
inotify는 폴링 없이 파일시스템 변화를 즉시 감지할 수 있게 해주는 리눅스 커널 기능이고, inotify_simple은 이를 Python에서 거의 그대로, 최소한의 추상화로 다룰 수 있게 해준다. 단순히 파일 변화를 로그로 남기는 수준이라면 몇 줄로 충분하지만, 재귀 감시나 rename 추적, 큐 오버플로우 대응까지 고려하면 실전에서는 생각보다 신경 쓸 부분이 많다는 점을 감안해서 설계하는 게 좋다.