파이썬의 파일 작업
이 글은 파이썬의 파일 작업에 대해 설명합니다.
이 가이드는 '안전성', '효율성', '가독성'을 염두에 두고, 기초부터 실전적인 기법까지 개념을 단계별로 설명합니다.
YouTube Video
파이썬의 파일 작업
파일 조작은 작은 스크립트부터 대규모 애플리케이션까지 필수적인 기본 기술입니다.
파일 열기와 닫기
먼저 텍스트 파일을 읽고 쓰는 예시를 살펴보겠습니다. with 문(컨텍스트 매니저)을 사용하면 파일이 올바르게 닫히는 것을 보장할 수 있습니다.
다음 코드는 텍스트 파일을 열고, 내용을 읽으며, 한 줄씩 처리하는 예시입니다.
1# Open and read a text file safely using a context manager.
2with open("example.txt", "r", encoding="utf-8") as f:
3 # Read all lines lazily (as an iterator) and process each line.
4 for line in f:
5 print(line.rstrip())encoding="utf-8"을 명시적으로 지정하면 플랫폼마다 발생할 수 있는 문제를 줄일 수 있습니다.- 큰 파일에서도
for line in f를 사용하면 메모리 효율적으로 처리할 수 있습니다.
쓰기(덮어쓰기 및 추가 쓰기)
파일에 쓸 때 모드에 주의해야 합니다. w는 덮어쓰기, a는 이어쓰기(추가)에 사용됩니다. 쓰기 시에도 with 문을 사용하세요.
다음 코드는 덮어쓰기와 추가 쓰기의 기본 예시입니다.
1# Write (overwrite) to a file.
2with open("output.txt", "w", encoding="utf-8") as f:
3 f.write("First line\n")
4 f.write("Second line\n")
5
6# Append to the same file.
7with open("output.txt", "a", encoding="utf-8") as f:
8 f.write("Appended line\n")- 이 코드는
output.txt에 텍스트를 쓰고, 같은 파일에 텍스트를 추가합니다."w"모드는 파일을 덮어쓰고,"a"모드는 기존 내용 뒤에 새 줄을 추가합니다. - 필요하다면
flush()를 호출할 수 있지만, 일반적으로 컨텍스트가 끝나면 자동으로 flush됩니다. - 여러 프로세스나 스레드가 동시에 쓸 수 있는 경우, 파일 잠금 등 배타적 제어를 고려해야 합니다.
이진 데이터 읽기 및 쓰기
이미지나 압축 파일은 바이너리 모드(rb 또는 wb)로 다루어야 합니다. 텍스트 모드와 달리, 바이너리 모드에서는 encoding이 무시됩니다.
아래 코드는 이진 파일을 읽어, 다른 파일로 바이너리 모드로 복사하는 예시입니다.
1# Read and write binary files.
2with open("image.png", "rb") as src:
3 data = src.read()
4
5with open("copy.png", "wb") as dst:
6 dst.write(data)- 큰 이진 파일을 다룰 때는
read()로 모두 읽지 말고, 나눠서 읽고 쓰는 것이 메모리 효율적입니다.
큰 파일을 청크(덩어리) 단위로 처리하는 예시
메모리에 한 번에 올라가지 않는 큰 파일은 고정된 크기의 청크 단위로 읽고 써야 합니다. I/O가 병목이 되므로, 버퍼 크기를 조절하는 것이 유용합니다.
아래 코드는 파일을 64KB 단위로 복사하는 예제입니다. 이렇게 하면 메모리 사용량을 낮게 유지하면서 빠르게 동작합니다.
1# Copy a large file in chunks to avoid using too much memory.
2CHUNK_SIZE = 64 * 1024 # 64 KB
3
4with open("large_source.bin", "rb") as src, open("large_dest.bin", "wb") as dst:
5 while True:
6 chunk = src.read(CHUNK_SIZE)
7 if not chunk:
8 break
9 dst.write(chunk)- 네트워크나 디스크 특성에 맞게 청크 크기를 조절할 수 있습니다. SSD 환경에서는 조금 더 큰 청크 크기가 더 효율적인 경우가 많습니다.
buffering 인수를 사용하는 예시
open() 함수에서 buffering 인수를 지정하면 Python 내부에서 사용하는 버퍼 크기를 제어할 수 있습니다. 이를 통해 입력/출력 효율성을 더욱 최적화할 수 있습니다.
1# Explicitly set the internal buffer size to 128 KB for faster I/O.
2CHUNK_SIZE = 64 * 1024
3BUFFER_SIZE = 128 * 1024 # 128 KB internal buffer
4
5with open("large_source.bin", "rb", buffering=BUFFER_SIZE) as src, open("large_dest.bin", "wb", buffering=BUFFER_SIZE) as dst:
6 while True:
7 chunk = src.read(CHUNK_SIZE)
8 if not chunk:
9 break
10 dst.write(chunk)buffering값을 0으로 설정하면 버퍼링 없이 입출력이 수행되고, 1로 설정하면 라인 버퍼링이 활성화되며, 2 이상으로 설정하면 지정한 바이트 크기의 버퍼가 사용됩니다.- 일반적으로 운영체제가 캐싱을 효율적으로 처리하므로 기본 값이면 충분하지만, 매우 큰 파일이나 특수 장치에는 이 값을 조정하는 것이 효과적일 수 있습니다.
Pathlib을 이용한 현대적인 파일 조작
표준 pathlib 모듈을 사용하면 경로 처리가 더욱 직관적이 됩니다. 문자열 경로를 사용하는 것보다 가독성과 안전성이 향상됩니다.
1from pathlib import Path
2
3path = Path("data") / "notes.txt"
4
5# Ensure parent directory exists.
6path.parent.mkdir(parents=True, exist_ok=True)
7
8# Write and read using pathlib.
9path.write_text("Example content\n", encoding="utf-8")
10content = path.read_text(encoding="utf-8")
11print(content)- 이 코드는
pathlib로 디렉토리를 생성하고, 텍스트 파일을 작성한 후 내용을 읽는 예시입니다.Path객체를 사용하면 직관적이고 안전하게 경로를 처리할 수 있습니다. Path에는iterdir(),glob()등 편리한 API가 있습니다. 운영 체제마다 경로 구분자를 신경 쓰지 않고 코드를 작성할 수 있습니다.
임시 파일과 디렉토리(tempfile 사용)
tempfile을 사용하면 임시 파일을 안전하게 생성할 수 있습니다. 이렇게 하면 보안 경쟁 조건이나 이름 충돌을 피할 수 있습니다.
아래 코드는 임시 파일을 이용해 임시 데이터를 생성하는 예시입니다.
1import tempfile
2
3# Create a temporary file that is deleted on close.
4with tempfile.NamedTemporaryFile(
5 mode="w+",
6 encoding="utf-8",
7 delete=True
8) as tmp:
9 tmp.write("temporary data\n")
10 tmp.seek(0)
11 print(tmp.read())
12
13# tmp is deleted here- 이 코드는 임시 파일을 만들고 데이터를 쓰고 읽은 뒤,
with블록이 끝나면 자동으로 삭제합니다.tempfile.NamedTemporaryFile을 사용하면 안전하게 충돌 없이 임시 파일을 다룰 수 있습니다.delete=True가 지정되어 있으면 파일은 자동으로 삭제됩니다. - Windows에서는 다른 핸들로 바로 파일을 열 수 없으므로,
delete=False로 설정하고 삭제를 직접 관리할 수 있습니다.
shutil: 복사, 이동, 삭제의 고수준 작업
shutil을 이용하면 파일과 디렉토리의 재귀적 복사, 이동, 삭제가 간편합니다.
1import shutil
2
3# Copy a file preserving metadata.
4shutil.copy2("source.txt", "dest.txt")
5
6# Move a file or directory.
7shutil.move("old_location", "new_location")
8
9# Remove an entire directory tree (use with caution).
10shutil.rmtree("some_directory")shutil.copy2는 변경 시간 등 메타데이터도 복사합니다. 이름 변경이 불가능한 경우에도move는 파일을 옮기기 위한 다른 방법을 사용합니다.rmtree는 위험한 작업이므로 삭제 전에 반드시 확인하고 백업하는 것이 좋습니다.
파일 메타데이터(os.stat) 및 권한 처리
파일 크기, 변경 시간, 권한 등은 os와 stat로 읽거나 수정할 수 있습니다.
아래 코드는 os.stat으로 파일 정보를 얻고, os.chmod로 권한을 변경하는 예시입니다.
1import os
2import stat
3from datetime import datetime
4
5st = os.stat("example.txt")
6print("size:", st.st_size)
7print("modified:", datetime.fromtimestamp(st.st_mtime))
8
9# Make file read-only for owner.
10os.chmod("example.txt", stat.S_IREAD)- POSIX와 Windows 시스템에서 권한 동작이 다를 수 있습니다. 크로스플랫폼 호환성이 필요하다면, 고수준 API를 사용하거나 조건별 처리를 추가하세요.
파일 잠금(배타적 제어) — Unix와 Windows의 차이
여러 프로세스가 동시에 같은 파일에 접근할 경우, 배타적 제어가 필요합니다. UNIX에서는 fcntl, Windows에서는 msvcrt를 사용합니다.
아래 코드는 UNIX 계열에서 fcntl.flock을 이용해 파일을 쓸 때 배타 잠금을 거는 방법을 보여줍니다.
1# Unix-style file locking example
2import fcntl
3
4with open("output.txt", "a+", encoding="utf-8") as f:
5 fcntl.flock(f, fcntl.LOCK_EX)
6 try:
7 f.write("Safe write\n")
8 f.flush()
9 finally:
10 fcntl.flock(f, fcntl.LOCK_UN)- 이 코드는 UNIX 계열에서
fcntl.flock으로 배타 잠금을 취해, 안전하게 파일을 쓰고 동시에 여러 프로세스가 쓰지 않도록 방지합니다. 처리 후에는 반드시 잠금을 해제해 다른 프로세스가 접근할 수 있도록 해야 합니다. - Windows에서는
msvcrt.locking()을 사용하세요. 더 고수준의 사용을 위해portalocker,filelock과 같은 외부 라이브러리를 고려할 수 있습니다.
원자적(Atomic) 파일 쓰기 패턴
업데이트 중 파일 손상을 막기 위해, 임시 파일에 기록하고 성공 시 os.replace로 교체하세요.
임시 파일에 쓰고 교체하는 방식은 충돌이 발생해도 파일 손상을 예방합니다.
1import os
2from pathlib import Path
3import tempfile
4
5def atomic_write(path: Path, data: str, encoding="utf-8"):
6 # Write to a temp file in the same directory and atomically replace.
7 dirpath = path.parent
8 with tempfile.NamedTemporaryFile(
9 mode="w",
10 encoding=encoding,
11 dir=str(dirpath),
12 delete=False
13 ) as tmp:
14 tmp.write(data)
15 tmp.flush()
16 tempname = tmp.name
17
18 # Atomic replacement (on most OSes)
19 os.replace(tempname, str(path))
20
21# Usage
22atomic_write(Path("config.json"), '{"key":"value"}\n')os.replace는 동일 파일 시스템 내에서 원자적 교체를 실행합니다. 다른 마운트 지점 간에는 원자성이 보장되지 않으니 주의하세요.
mmap을 이용한 빠른 접근(대용량 데이터용)
큰 파일의 임의 접근(random access)은 mmap을 사용하면 I/O 성능이 향상됩니다. 주로 이진 데이터 작업에 사용됩니다.
아래 코드는 파일을 메모리 매핑하고 특정 바이트 영역을 읽거나 쓰는 예시입니다. 파일 크기를 변경할 때는 주의하세요.
1import mmap
2
3with open("data.bin", "r+b") as f:
4 mm = mmap.mmap(f.fileno(), 0) # map entire file
5 try:
6 print(mm[:20]) # first 20 bytes
7 mm[0:4] = b"\x00\x01\x02\x03" # modify bytes
8 finally:
9 mm.close()- 이 코드는 이진 파일을
mmap으로 메모리에 매핑하고 바이트 단위로 직접 읽기/쓰기를 수행합니다. 메모리 매핑을 사용하면 대용량 데이터 집합에서도 빠른 임의 접근이 가능합니다. mmap은 효율적이지만, 잘못 사용하면 데이터 일관성 문제가 생길 수 있습니다. 필요할 때는flush()를 호출하여 동기화하세요.
CSV / JSON / Pickle: 포맷별 읽기와 쓰기
특정 데이터 포맷별로 전용 모듈이 있습니다. CSV는 csv, JSON은 json, 파이썬 객체 저장은 pickle을 사용하세요.
아래 코드는 CSV/JSON의 읽기/쓰기와 Pickle 사용의 기본 예시입니다. Pickle은 임의의 코드를 실행할 수 있으므로, 신뢰할 수 없는 소스에서 데이터를 불러오지 마세요.
1import csv
2import json
3import pickle
4
5# CSV write
6with open("rows.csv", "w", encoding="utf-8", newline="") as f:
7 writer = csv.writer(f)
8 writer.writerow(["name", "age"])
9 writer.writerow(["Alice", 30])
10
11# JSON write
12data = {"items": [1, 2, 3]}
13with open("data.json", "w", encoding="utf-8") as f:
14 json.dump(data, f, ensure_ascii=False, indent=2)
15
16# Pickle write (only for trusted environments)
17obj = {"key": "value"}
18with open("obj.pkl", "wb") as f:
19 pickle.dump(obj, f)- Windows에서 빈 줄이 생기는 것을 막으려면 CSV에서
newline=""지정을 권장합니다.ensure_ascii=False로 지정하면 JSON에서 UTF-8 문자가 깨지지 않고 읽을 수 있습니다.
압축 파일의 직접 읽기/쓰기(gzip / bz2 / zipfile)
gzip, zip 파일을 직접 다루면 디스크 공간을 절약할 수 있습니다. 표준 라이브러리에서 관련 모듈을 제공합니다.
아래는 gzip 파일을 텍스트로 읽고 쓰는 간단한 예시입니다.
1import gzip
2
3# Write gzipped text.
4with gzip.open("text.gz", "wt", encoding="utf-8") as f:
5 f.write("Compressed content\n")
6
7# Read gzipped text.
8with gzip.open("text.gz", "rt", encoding="utf-8") as f:
9 print(f.read())- 압축 수준과 형식에 따라 압축률과 속도 사이에 상호 절충이 있습니다.
보안 및 취약점 대책
보안 및 취약점 대책으로 다음과 같은 점들을 고려할 수 있습니다.
- 신뢰할 수 없는 입력을 파일명이나 경로에 직접 사용하지 마세요.
- Pickle은 반드시 신뢰할 수 있는 데이터에서만 사용하세요.
- 파일을 다루는 프로세스에는 실행 권한을 최소화하고, 필요한 최소한의 권한만 부여하세요.
- 임시 파일은
tempfile을 사용하고, 일반 파일을 공용 디렉토리에 저장하지 마세요.
사용자 입력을 파일 경로에 사용할 때는 반드시 경로 정규화와 검증을 거쳐야 합니다. 예를 들어, Path.resolve()를 사용하고 상위 디렉토리를 확인하세요.
1from pathlib import Path
2
3def safe_path(base_dir: Path, user_path: str) -> Path:
4 candidate = (base_dir / user_path).resolve()
5 if base_dir.resolve() not in candidate.parents and base_dir.resolve() != candidate:
6 raise ValueError("Invalid path")
7 return candidate
8
9# Usage
10base = Path("/srv/app/data")
11
12# Raises an exception: attempted path traversal outside `base`
13safe = safe_path(base, '../etc/passwd')- 웹 앱이나 공개 API에서 외부 입력을 파일 경로로 사용할 때는 특히 주의하세요.
자주 쓰이는 패턴 요약
- 항상
with문을 사용하세요(자동 닫힘 보장). - 텍스트 데이터에는
encoding을 명시적으로 지정하세요. - 대용량 파일은 청크 단위로 읽고 쓰세요.
- 공유 자원을 위해 파일 잠금 기능을 도입하세요.
- 중요한 업데이트에는 '임시 파일에 쓰기 → os.replace'와 같은 원자적 패턴을 사용하세요.
- 삭제나 덮어쓰기 등 위험한 작업을 수행하기 전에 항상 확인하고 백업을 작성하세요.
- 외부 입력을 파일 경로로 사용할 때는 반드시 정규화 및 검증을 하세요.
요약
파일 작업에서는 with문 사용, 인코딩 명시, 원자적 쓰기 등 안전하고 신뢰할 수 있는 기법을 사용하는 것이 중요합니다. 대규모 처리나 병렬 접근 시 데이터 손상과 충돌 방지를 위해 잠금과 로그 관리 시스템을 도입해야 합니다. 효율성과 안전성의 균형이 신뢰할 수 있는 파일 작업의 핵심입니다.
위의 기사를 보면서 Visual Studio Code를 사용해 우리 유튜브 채널에서 함께 따라할 수 있습니다. 유튜브 채널도 확인해 주세요.