파이썬 파일 저장 중단에 안전한 임시 파일·os.replace 사용법

파이썬 자동화가 JSON·CSV 같은 파일을 갱신할 때 중단되어 기존 파일이 손상되지 않도록 하려면, 같은 디렉터리의 임시 파일에 내용을 완성하고 닫은 뒤 os.replace()로 대상 파일을 교체하는 순서를 사용해야 합니다. 파일을 직접 열어 기존 파일에 덮어쓰는 방식은 쓰기 도중 예외나 프로세스 중단이 발생할 때 불완전한 내용이 남을 수 있습니다. 다만 이 방법은 임시 파일과 대상 파일이 같은 파일시스템에 있고, 교체 권한이 있는 조건에서의 안전한 교체 절차입니다.

이 글에서는 파일을 만드는 위치, 쓰기와 닫기의 순서, NamedTemporaryFile의 정리 방식, 교체 실패 시 확인할 부분을 운영 관점에서 정리하겠습니다.

기존 파일에 직접 쓰면 왜 문제가 생기나

다음처럼 대상 파일을 바로 쓰면 파일을 여는 순간부터 기존 내용이 새 내용으로 바뀌기 시작합니다.

with open("result.json", "w", encoding="utf-8") as file:
    file.write(new_text)

정상적으로 블록을 빠져나오면 with가 파일을 닫습니다. 그러나 write()가 여러 번 호출되는 중 예외가 발생하거나 프로세스가 강제로 끝나면, 다음 실행이 읽을 파일에 일부만 기록될 수 있습니다. write() 뒤에 close()가 실행되지 않으면 내용이 디스크에 완전히 기록되지 않을 수 있다는 점도 파이썬 공식 입출력 문서가 설명하는 부분입니다.

여기서 with는 필요한 도구지만, 기존 파일을 보호하는 도구는 아닙니다. 파일 핸들을 정리해도 이미 대상 파일을 직접 수정한 사실까지 되돌리지는 못합니다.

안전한 저장 순서: 같은 디렉터리에서 완성 후 교체

운영 파일을 보존해야 한다면 다음 흐름으로 나누어 생각하면 됩니다.

  1. 대상 파일과 같은 디렉터리에 임시 파일을 만듭니다.
  2. 새 내용을 임시 파일에 모두 씁니다.
  3. 임시 파일을 닫아 쓰기 작업을 끝냅니다.
  4. os.replace()로 임시 파일을 대상 경로로 교체합니다.
  5. 교체가 성공한 뒤 임시 파일이 남았는지 확인합니다.

os.replace(src, dst)는 src를 dst로 이름 변경하고, 권한이 있으면 기존 파일을 조용히 교체합니다. 두 경로가 서로 다른 파일시스템에 있으면 실패할 수 있으므로, 임시 파일을 시스템 기본 임시 디렉터리에 만들고 다른 위치의 결과 파일로 옮기는 방식은 피하는 편이 안전합니다.

import os
import tempfile
from pathlib import Path


def save_text_safely(path: str | Path, text: str) -> None:
    target = Path(path)
    target.parent.mkdir(parents=True, exist_ok=True)
    temporary_name: str | None = None

    try:
        with tempfile.NamedTemporaryFile(
            mode="w", encoding="utf-8", dir=target.parent,
            prefix=f".{target.name}.", suffix=".tmp", delete=False,
        ) as temporary:
            temporary_name = temporary.name
            temporary.write(text)

        os.replace(temporary_name, target)
    except Exception:
        if temporary_name is not None:
            try:
                os.unlink(temporary_name)
            except FileNotFoundError:
                pass
        raise

핵심은 dir=target.parent와 delete=False입니다. NamedTemporaryFile은 파일시스템에 보이는 이름을 만들고 name으로 그 경로를 알려 줍니다. 기본 설정에서는 닫을 때 임시 파일이 삭제될 수 있으므로, 닫은 뒤 이름을 사용해 교체하려면 교체할 때까지 파일을 남겨 두어야 합니다. 위 코드는 with 블록을 빠져나와 파일을 닫은 다음 os.replace()를 호출합니다.

임시 파일을 닫은 뒤 교체해야 하는 이유

파일을 열어 둔 채 교체하는 코드는 플랫폼별 파일 잠금과 삭제 동작 차이의 영향을 받기 쉽습니다. Windows 환경을 포함해 여러 실행 환경을 고려한다면, 먼저 with 블록을 끝내고 임시 파일 핸들을 닫은 뒤 교체하는 순서를 고정하는 것이 좋습니다.

os.replace()에 전달하는 임시 경로와 대상 경로는 같은 파일시스템 안에 있어야 합니다. 같은 디렉터리를 지정하면 이 조건을 자연스럽게 만족시키기 쉽습니다.

pathlib를 선호한다면 Path(temporary_name).replace(target)처럼 대상 경로의 메서드를 사용할 수도 있습니다. Path.replace()도 대상에 기존 파일이 있으면 교체하는 동작을 제공합니다. 프로젝트의 경로 처리 방식에 맞춰 하나를 선택하면 됩니다.

저장 실패 후 무엇을 확인할까

os.replace() 전에 오류가 나면 기존 대상 파일은 위 저장 흐름에서 직접 수정되지 않았습니다. 재실행 전에 대상 파일이 여전히 읽을 수 있는지 확인하고, 임시 파일이 남았다면 이번 실행이 남긴 파일인지 점검한 뒤 정리합니다. 예제의 except는 임시 경로를 알고 있을 때만 삭제를 시도하고, 이미 교체되어 사라진 경우에는 FileNotFoundError를 무시합니다.

프로세스가 강제 종료되어 예외 처리 코드 자체가 실행되지 않을 수도 있습니다. 파이썬 공식 문서는 POSIX에서 SIGKILL로 종료되면 임시 파일 자동 정리가 보장되지 않는다고 설명합니다. 따라서 다음 실행을 시작할 때 숨김 접두사와 .tmp 확장자를 가진 잔여 파일을 점검하는 운영 절차를 별도로 둘 수 있습니다. 잔여 파일을 무조건 최신 결과로 간주해 대상 파일로 자동 복구하는 것은 안전하지 않습니다.

JSON·CSV에 적용할 때의 판단 기준

JSON이든 CSV든 안전한 부분은 최종 문자열 또는 행을 임시 파일에 완성한 뒤 교체한다는 파일 교체 순서입니다. JSON의 들여쓰기와 인코딩, CSV의 열 구성 같은 직렬화 선택은 별도의 문제이므로, 저장할 내용을 결정한 다음 안전한 저장 함수에 넘기는 편이 구조가 분명합니다.

저장 대상이 재생성 가능한 자동화 결과이고 중단 시 기존 결과를 보존해야 한다면 임시 파일과 교체 방식을 우선 검토합니다. 반대로 여러 프로세스가 동시에 갱신하거나 전원 장애 뒤 저장장치에 어느 시점까지 내용이 보존되는지까지 보장해야 한다면 이 글의 절차만으로 충분하다고 단정할 수 없습니다. os.replace()의 교체 동작은 같은 파일시스템과 권한 조건 안에서 판단해야 합니다.

적용 전 점검표

  • 임시 파일을 대상 파일과 같은 디렉터리에 만드는가
  • 임시 파일을 with 블록 안에서 끝까지 쓰고 닫은 뒤 교체하는가
  • NamedTemporaryFile의 삭제 설정과 임시 경로를 알고 있는가
  • 교체 전에 실패하면 기존 대상 파일을 건드리지 않는 구조인가
  • 교체 실패나 강제 종료 뒤 남은 임시 파일을 구분해 정리할 수 있는가
  • 같은 파일시스템과 권한 조건을 벗어난 보장을 주장하고 있지 않은가

파일 저장 중단 때문에 기존 결과가 깨지는 문제가 핵심이라면, 직접 덮어쓰기부터 임시 파일 완성 후 교체 방식으로 바꾸는 것이 판단의 출발점입니다. 그 다음에 재실행 정책과 잔여 임시 파일 정리 기준을 정하면 됩니다.

출처

Leave a Comment