[파이썬 100강] 55강. tempfile로 임시 파일·디렉터리 안전하게 다루기
실무 자동화 코드를 짜다 보면 “중간 결과를 잠깐만 저장”해야 할 순간이 계속 나옵니다. 예를 들면 API에서 내려받은 압축 파일을 풀기 전 잠시 보관하거나, 대용량 CSV를 전처리한 뒤 최종 파일로 원자적 교체(atomic replace)하기 전 중간 산출물을 두는 경우가 그렇죠. 이때 대충 tmp.txt 같은 고정 파일명을 쓰면 충돌, 권한, 정리 누락 문제가 금방 터집니다.
이번 강의 목표는 간단합니다. tempfile을 써서 임시 리소스를 안전하게 만들고, 자동으로 정리되게 하며, 운영 환경에서 재현 가능한 방식으로 다루는 습관을 익히는 것입니다. 서론은 여기까지 하고 바로 예제로 들어가겠습니다.
핵심 개념
tempfile은 임시 파일/디렉터리를 안전한 이름과 권한으로 생성하는 표준 라이브러리입니다.TemporaryFile,NamedTemporaryFile,TemporaryDirectory는 쓰임이 다르며, “경로가 필요한지”가 선택 기준입니다.- 실무에서는
with블록을 사용해 생명주기를 코드로 명시하고, 종료 시점 정리를 자동화해야 누수와 충돌을 줄일 수 있습니다.
임시 파일에서 가장 흔한 오해는 “어차피 잠깐 쓰는 파일이니 아무 데나 만들면 된다”는 생각입니다. 하지만 운영 서버, 컨테이너, CI 환경에서는 작업이 동시에 돌아가기 때문에 파일명 충돌과 정리 누락이 누적됩니다. 특히 실패한 배치가 임시 파일을 남기면 디스크를 갉아먹고, 다음 실행에서 예상치 못한 상태를 만들 수 있습니다. tempfile은 이 문제를 언어 차원에서 줄여주는 도구입니다. 핵심은 기능 자체보다 사용 패턴입니다. 생성-사용-정리의 경계를 명확히 두면, 장애 원인 추적이 쉬워지고 코드 리뷰에서도 의도를 이해하기 쉽습니다.
기본 사용
예제 1) TemporaryFile로 내용만 잠깐 쓰고 버리기
>>> import tempfile
>>> with tempfile.TemporaryFile(mode="w+t", encoding="utf-8") as f:
... _ = f.write("중간 계산 결과\n")
... _ = f.write("row_count=120\n")
... f.seek(0)
... print(f.read().strip())
중간 계산 결과
row_count=120
해설:
TemporaryFile은 보통 이름(path)을 신경 쓰지 않아도 되는 순수 임시 버퍼 용도에 적합합니다.with가 끝나면 정리되므로 “파일 삭제 코드”를 별도로 작성하지 않아도 됩니다.
예제 2) NamedTemporaryFile로 경로가 필요한 라이브러리와 연동
>>> import tempfile
>>> from pathlib import Path
>>> with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=True, encoding="utf-8") as tf:
... _ = tf.write("name,score\nlee,91\nkim,88\n")
... tf.flush()
... p = Path(tf.name)
... print(p.suffix, p.exists())
.csv True
해설:
- 외부 도구가 “파일 경로”를 요구하면
NamedTemporaryFile이 편합니다. flush()로 버퍼를 비운 뒤 다른 함수에서 읽도록 넘겨야 데이터 누락을 막을 수 있습니다.
예제 3) TemporaryDirectory로 작업공간 전체를 임시로 만들기
>>> import tempfile
>>> from pathlib import Path
>>> with tempfile.TemporaryDirectory(prefix="etl_") as d:
... root = Path(d)
... (root / "raw").mkdir()
... (root / "work").mkdir()
... (root / "raw" / "input.txt").write_text("hello", encoding="utf-8")
... print((root / "raw" / "input.txt").read_text(encoding="utf-8"))
hello
해설:
- 파이프라인 단계가 여러 파일을 만들면 파일 하나보다 “임시 디렉터리” 단위 관리가 훨씬 깔끔합니다.
- 블록 종료 시 디렉터리 전체가 정리되므로 잔여 파일 문제를 크게 줄일 수 있습니다.
예제 4) 원자적 교체 패턴: 임시 파일에 쓰고 마지막에 replace
>>> import tempfile
>>> from pathlib import Path
>>> target = Path("final_report.txt")
>>> with tempfile.NamedTemporaryFile(mode="w", delete=False, encoding="utf-8") as tf:
... _ = tf.write("version=2\nstatus=ok\n")
... temp_path = Path(tf.name)
>>> temp_path.replace(target)
PosixPath('final_report.txt')
>>> target.read_text(encoding="utf-8").strip()
'version=2\nstatus=ok'
해설:
- 실패 가능성이 있는 계산은 임시 파일에서 끝내고, 마지막에만 교체하면 “반쯤 써진 파일”을 남기지 않습니다.
- 이 패턴은 리포트 생성, 설정 파일 갱신, 캐시 갱신에 매우 자주 씁니다.
자주 하는 실수
실수 1) 고정 파일명(tmp.txt)을 여러 실행이 함께 사용
>>> from pathlib import Path
>>> p = Path("tmp.txt")
>>> p.write_text("run-A", encoding="utf-8")
5
>>> p.write_text("run-B", encoding="utf-8")
5
>>> p.read_text(encoding="utf-8")
'run-B'
원인:
- 동시 실행을 고려하지 않은 고정 파일명은 덮어쓰기 충돌을 유발합니다.
- 실패 시 파일이 남아 다음 실행에 영향을 주는 “유령 상태”가 생깁니다.
해결:
>>> import tempfile
>>> with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8") as f1, \
... tempfile.NamedTemporaryFile(mode="w", encoding="utf-8") as f2:
... _ = f1.write("run-A")
... _ = f2.write("run-B")
... print(f1.name != f2.name)
True
실수 2) NamedTemporaryFile에 쓴 뒤 flush 없이 즉시 다른 함수로 읽기
- 증상: 파일은 존재하는데 내용이 비어 있거나 일부만 읽힙니다.
- 원인: 버퍼가 디스크로 내려가기 전에 다른 코드가 파일을 열어 읽었습니다.
- 해결:
write()후flush()를 호출하고, 필요하면seek(0)으로 포인터를 되돌린 뒤 읽습니다.
실수 3) 정리 책임을 직접 코드에 흩뿌려 누락
>>> # 나쁜 예: 중간에 예외가 나면 cleanup까지 못 가는 구조
>>> # f = open("/tmp/manual_temp.txt", "w", encoding="utf-8")
>>> # ... 작업 ...
>>> # f.close(); os.remove("/tmp/manual_temp.txt")
원인:
- 정리 로직이 마지막 줄에만 있으면, 예외 경로에서 실행되지 않습니다.
- 코드가 길어질수록 누락 확률이 기하급수로 커집니다.
해결:
>>> import tempfile
>>> from pathlib import Path
>>> with tempfile.TemporaryDirectory() as d:
... p = Path(d) / "step.txt"
... p.write_text("safe cleanup", encoding="utf-8")
... print(p.exists())
True
핵심은 “정리 호출을 기억하는 것”이 아니라 “정리가 자동으로 일어나게 구조화”하는 것입니다.
실무 패턴
- 입력 검증 규칙: 임시 파일을 만들기 전에 대상 작업 크기(예: 예상 파일 용량)를 로그로 남겨 디스크 부족을 조기 감지합니다.
- 로그/예외 처리 규칙: 임시 경로 자체를 전체 로그에 남기기보다 실행 ID와 함께 요약 기록하고, 민감 경로 노출은 최소화합니다.
- 재사용 함수/구조화 팁:
prepare_temp_workspace() -> run_pipeline() -> finalize_replace()처럼 단계를 분리해 임시 리소스 생명주기를 한눈에 보이게 합니다. - 성능/메모리 체크 포인트: 작은 데이터는 메모리(
io.StringIO)가 유리할 수 있고, 대용량은tempfile이 안전합니다. 기준(예: 10MB 이상은 파일)을 팀 규칙으로 명시하세요.
실무에서 특히 효과적인 패턴은 “임시 디렉터리 안에서 모든 중간 산출물을 만들고, 성공 시 최종 결과만 밖으로 내보내기”입니다. 이렇게 하면 실패한 실행의 흔적은 자동으로 사라지고, 성공한 결과만 남습니다. 또한 배치 잡을 병렬로 돌릴 때 실행마다 별도 temp workspace를 쓰면 서로 간섭이 사라집니다. 여기에 실행 시작/종료 시점에 job_id, duration, result_size를 함께 기록하면, 장애 분석이 훨씬 빨라집니다. 결국 임시 파일 설계는 사소한 문법 문제가 아니라 운영 안정성을 결정하는 구조 문제입니다.
오늘의 결론
한 줄 요약: 임시 파일은 “편의용 임시 저장소”가 아니라, 실패와 동시성에 강한 파이프라인을 만드는 안전장치입니다.
기억할 것:
- 경로가 필요 없으면
TemporaryFile, 필요하면NamedTemporaryFile을 선택하세요. - 파일 하나보다 작업공간 단위가 필요하면
TemporaryDirectory를 쓰세요. - 중간 산출물은 임시에서 완성하고 마지막에
replace로 교체해야 운영 사고를 줄일 수 있습니다.
연습문제
TemporaryDirectory안에서raw.csv -> cleaned.csv -> report.txt를 생성하는 미니 파이프라인을 만들고, 블록 종료 뒤 임시 경로가 삭제되는지 확인하세요.NamedTemporaryFile로 JSON을 쓴 뒤flush()유무에 따라 외부 함수 읽기 결과가 어떻게 달라지는지 비교 실험해 보세요.- 기존
result.txt를 직접 덮어쓰는 코드와 “임시 파일 작성 후replace” 코드를 각각 만들어, 중간 예외 발생 시 어떤 파일 상태가 남는지 관찰해 보세요.
이전 강의 정답
logs/하위에서app_2026-02-*.log만 선택하고*_debug.log,archive/제외
>>> from pathlib import Path
>>> from fnmatch import fnmatch
>>> def select_logs(root: str):
... out = []
... for p in Path(root).rglob("*"):
... if not p.is_file():
... continue
... name = p.name
... full = p.as_posix()
... if not fnmatch(name, "app_2026-02-*.log"):
... continue
... if fnmatch(name, "*_debug.log") or fnmatch(full, "*/archive/*"):
... continue
... out.append(full)
... return out
>>> isinstance(select_logs("logs"), list)
True
- include/exclude를 JSON에서 읽어 dry-run 10개 출력
>>> import json
>>> from pathlib import Path
>>> from fnmatch import fnmatch
>>> def load_rules(path: str):
... obj = json.loads(Path(path).read_text(encoding="utf-8"))
... return obj.get("include", []), obj.get("exclude", [])
>>> def dry_run_pick(root: str, include: list[str], exclude: list[str], limit: int = 10):
... picked = []
... for p in Path(root).rglob("*"):
... if not p.is_file():
... continue
... n, f = p.name, p.as_posix()
... if include and not any(fnmatch(n, pat) or fnmatch(f, pat) for pat in include):
... continue
... if exclude and any(fnmatch(n, pat) or fnmatch(f, pat) for pat in exclude):
... continue
... picked.append(f)
... return picked[:limit]
>>> isinstance(dry_run_pick("logs", ["*.log"], ["*_debug.log"]), list)
True
- 선택 파일을 날짜 폴더로 복사하고 전/후 개수 비교 리포트
>>> import shutil
>>> from pathlib import Path
>>> def copy_with_count(files: list[str], dist_root: str, date_str: str):
... dst = Path(dist_root) / date_str
... dst.mkdir(parents=True, exist_ok=True)
... before = len(list(dst.glob("*")))
... for f in files:
... src = Path(f)
... shutil.copy2(src, dst / src.name)
... after = len(list(dst.glob("*")))
... return {"before": before, "after": after, "copied": len(files)}
>>> isinstance(copy_with_count([], "dist", "2026-02-17"), dict)
True
실습 환경/재현 정보
- 실행 환경:
condaenvpython100(Python 3.11.14) - 가정한 OS: macOS/Linux 공통
- 사용 모듈:
tempfile,pathlib,json,shutil,fnmatch - 재현 절차:
- 빈 작업 디렉터리에서 예제 1~4를 순서대로 실행합니다.
- 예제 4의
replace패턴을 직접 적용해final_report.txt가 원자적으로 교체되는지 확인합니다. - 연습문제 3번처럼 의도적으로 예외를 넣어 직접 덮어쓰기와 임시 교체 방식의 차이를 비교합니다.
- 검증 포인트: 임시 리소스 자동 정리 여부, flush 누락 시 데이터 누락 여부, 동시 실행 시 파일명 충돌 여부, 최종 교체의 일관성