Huny's Dev Blog
huny.dev

Linux 설정 파일 갱신 중 JSON이 깨지는 이유: os.replace로 원자적 교체하기

설정 파일 덮어쓰기 중간의 파싱 실패를 재현한다. Python os.replace·fsync 예제로 기존 핸들과 새 독자, 직렬화 실패와 내구성의 경계를 확인한다.

Hun Jang
Hun Jang Nov 1, 2026

Linux에서 서비스가 읽는 JSON 설정 파일을 갱신할 때는 대상 파일을 직접 비우고 다시 쓰기보다, 같은 디렉터리에 새 내용을 완성한 뒤 os.replace로 교체하는 편이 적절하다. 이 방식은 이름을 통해 파일을 여는 독자가 작성 중간 상태를 읽는 문제를 피한다. 다만 이미 열린 파일, 여러 작성자의 경쟁, 전원 장애 후 보존은 별도로 다뤄야 한다. 아래 Python 예제로 그 경계를 확인한다.

완성된 문서 카트리지와 교체 장치로 설정 스냅샷의 교체를 표현한 개념 일러스트. 실제 파일시스템 구조도는 아니다.

작성·문서 확인: 2026-11-01. 실행 검증: Python 3.12.14, Linux amd64의 로컬 임시 디렉터리. 사용자 서비스나 실제 설정 파일에는 접근하지 않았다.

덮어쓰기 중간의 파일도 독자는 열 수 있다

open(path, "w")는 기존 내용을 비운 뒤 쓴다. JSON 앞부분만 기록한 순간 다른 코드가 파일을 열면 파싱할 수 없는 내용을 볼 수 있다. 아래 재현은 임의의 지연이나 스레드 경쟁 대신 두 번의 쓰기 사이에서 읽는다. 경쟁이 발생할 수 있는 상태를 결정적으로 보여 주며, 장애 발생 빈도를 측정하지는 않는다.

해결의 핵심은 독자가 사용하는 이름에 불완전한 내용을 쓰지 않는 것이다. Python os.replace 문서는 성공한 이름 변경의 원자성을 설명한다. 임시 파일은 대상과 같은 디렉터리에 만들어 파일시스템 경계를 넘는 이동을 피한다.

교체 순서와 각 단계의 역할

단계
역할
문서를 먼저 직렬화한다
이 단계에서 실패하면 대상 파일을 건드리지 않는다
같은 디렉터리의 임시 파일에 쓴다
독자가 사용하는 대상 이름은 유지한다
flush 후 임시 파일을 fsync한다
Python 버퍼와 파일 동기화를 순서대로 요청한다
os.replace로 이름을 교체한다
완성된 새 파일을 대상 이름으로 공개한다
부모 디렉터리를 fsync한다
이름 변경의 디렉터리 항목 동기화를 요청한다

원자성은 독자가 관찰하는 교체의 성질이다. 저장장치까지 반영되는 내구성과는 구별한다. Linux fsync(2)는 파일 동기화만으로 부모 디렉터리 항목의 저장을 보장하지 않으며, 디렉터리에 대한 별도 호출이 필요함을 설명한다. 이 예제는 그 호출 순서를 사용하지만 전원 차단 복구 시험을 수행하지 않았다.

실행 가능한 최소 예제

아래를 atomic_config.py로 저장하고 python atomic_config.py로 실행한다. 외부 패키지는 필요 없다. 신뢰할 수 있고 실행 중 바뀌지 않는 부모 디렉터리, 하나의 협조적인 작성자를 전제로 한다. 예제는 전체 JSON을 메모리에 직렬화하므로 큰 파일용 스트리밍 구현이 아니다.

allow_nan=False는 예제에서 NaN을 직렬화 오류로 만드는 데 사용한다. 이것만으로 애플리케이션 설정의 필수 필드·허용 범위를 검증하는 것은 아니다. 실제 서비스의 스키마 검증은 임시 파일을 공개하기 전에 수행해야 한다.

실제 실행 결과와 독자의 관찰 범위

검증한 다섯 항목은 덮어쓰기 중간의 파싱 실패, 기존 열린 파일의 버전 1, 새로 연 파일의 버전 2, 직렬화 실패 시 버전 2 보존, 정상 실행 후 임시 파일이 남지 않는 상태다. 파일·디렉터리 fsync 호출도 이 실행에서 오류 없이 완료됐다. I/O 오류 주입, 강제 종료, 정전, NFS, Windows 환경은 시험하지 않았다.

Linux rename(2)에 설명된 것처럼 이름 교체는 기존 열린 파일 디스크립터를 새 파일로 바꾸지 않는다. 독자는 한 번 열어서 그 핸들로 끝까지 읽고 파싱해야 한다. 처리 중 여러 번 다시 열면 서로 다른 버전을 섞을 수 있다. 예제에서 version 1을 보는 기존 핸들은 교체 실패의 증거가 아니다.

운영 코드로 옮길 때 남는 조건

  • 교체 이후의 오류는 롤백을 뜻하지 않는다. os.replace가 성공한 뒤 디렉터리 열기나 동기화가 실패하면 함수가 예외를 내더라도 새 내용은 이미 보일 수 있다. 호출자는 버전·내용을 다시 읽어 상태를 확인해야 한다. 예제는 이 오류를 주입하지 않았다.
  • 메타데이터는 별도 정책이다. 이 코드는 기존 파일의 권한·소유자·ACL·확장 속성을 복사하지 않는다. tempfile 문서의 임시 파일 생성 규칙을 확인하고, 다른 계정의 서비스가 읽어야 한다면 필요한 메타데이터를 공개 전에 설정한다.
  • 원자적 교체는 동시 갱신 잠금이 아니다. 여러 작성자는 서로의 변경을 덮어쓸 수 있다. 잠금이나 버전 비교를 추가하고, 서로 독립적인 파일 여러 개를 하나의 트랜잭션처럼 취급하지 않는다.
  • 경로와 배포 환경을 확인한다. 이 패턴은 다른 사용자가 부모 디렉터리나 경로를 바꿀 수 있는 상황을 방어하지 않는다. 심볼릭 링크·하드 링크·마운트된 설정 파일·네트워크 파일시스템에서는 교체 대상과 동작을 별도 검증한다.
  • 서비스 재적용과 임시 파일 정리는 별도다. 파일 교체만으로 서비스가 설정을 다시 읽지는 않는다. 강제 종료 시에는 finally가 실행되지 않을 수 있으므로, 남은 임시 파일을 운영 정책에 따라 정리한다.

English version: Atomic Config File Updates on Linux: Avoid Partial JSON with os.replaceAtomic Config File Updates on Linux: Avoid Partial JSON with os.replace

추천 글