Devin.KR

파일 저장과 재실행

65분 안팎

학습 목표

파일 핸들을 닫고 메모리·디스크 수명을 구분합니다.

개념

종료 후에도 남아야 하는 기록

앞 모듈의 저장소는 ID를 키로 사용한 딕셔너리입니다. 프로그램이 실행되는 동안 추가와 조회가 가능하지만 다음 실행은 새로운 메모리에서 시작합니다. 어제 읽은 책이 오늘도 보여야 한다는 요구를 만족하려면 값을 실행 중인 프로세스 밖에 남겨야 합니다. 파일 저장은 메모리의 객체를 다시 읽을 수 있는 표현으로 디스크에 기록하는 작업입니다. 저장 버튼이 있다는 사실과 재실행 후 값이 유지된다는 사실은 서로 다른 증거입니다.

프로젝트의 저장 시점을 정합니다. 이번 모듈은 CSV 가져오기를 끝낸 뒤 전체 기록을 JSON 파일로 저장하고 다음 실행에서 다시 읽습니다. 매번 프로그램을 종료하기 직전에만 저장하면 사용자가 창을 갑자기 닫았을 때 변경을 잃을 수 있습니다. 반대로 필드 하나를 입력할 때마다 저장하면 아직 검증하지 않은 값이 남을 수 있습니다. 입력 검증을 통과한 기록 묶음을 저장 단위로 삼는 이유입니다.

모듈 실습은 Python 3 표준 라이브러리만 사용합니다. 로컬 실습은 starter.zip을 풀고 그 폴더에서 README의 명령을 실행합니다. 테스트가 만드는 파일은 임시 폴더 안에 있고 종료하면 정리합니다. 브라우저 실습은 표준 입력 문자열을 읽고 정해진 결과만 출력합니다. 실제 개인 독서 목록 대신 제공한 작은 예제를 사용하면 원본을 지우는 위험 없이 실패를 반복할 수 있습니다.

경로와 열기 모드를 결정합니다

Path는 경로를 나타내는 객체이며 파일 내용을 담은 객체는 아닙니다. Path("data") / "records.json"은 폴더와 파일 이름을 연결합니다. 상대 경로는 명령을 실행한 현재 작업 폴더가 기준입니다. 프로그램 파일을 다른 폴더에서 실행하면 같은 문자열이 다른 위치를 가리킬 수 있으므로 오류 조사에서는 현재 위치와 계산된 경로를 함께 확인합니다. 실행 파일 위치를 기준으로 하고 싶다면 실제 .py 파일에서 Path(__file__).resolve().parent를 사용합니다.

읽기 모드 r은 기존 파일을 읽고 없으면 FileNotFoundError를 발생시킵니다. 쓰기 모드 w는 새 파일을 만들거나 이미 있는 파일을 열면서 내용을 비웁니다. 이어쓰기 a는 기존 내용 뒤에 붙입니다. 텍스트 메모를 매번 전체 교체하는 실습에는 w가 적합하지만 JSON 문서 두 개를 a로 붙이면 하나의 JSON 문서가 되지 않습니다. 모드는 저장하려는 정보의 의미를 정한 다음 선택합니다.

파일이 없는 첫 실행과 이미 저장한 파일을 찾지 못하는 경로 오류는 같은 예외 이름일 수 있습니다. 따라서 없는 파일을 빈 목록으로 처리한다는 정책만으로 잘못된 경로가 해결되지는 않습니다. 이번 실습의 load_note는 없으면 빈 문자열을 반환하지만 실제 프로젝트의 시작 안내에는 어느 파일을 읽었는지 표시하는 것이 좋습니다. 부모 폴더가 없는데 w로 열어도 파일을 만들 수 없으므로 폴더 준비와 파일 생성은 분리해서 생각합니다.

파일 핸들의 책임과 with

open은 열린 파일과 연결된 핸들을 반환합니다. write는 문자열을 이 핸들에 전달하며 파일이 아니라 문자열 길이에 해당하는 수를 반환합니다. write가 줄바꿈을 추가하지 않으므로 두 줄을 저장하려면 문자열 안에 줄바꿈을 넣습니다. 텍스트는 UTF-8로 인코딩되어 바이트로 기록됩니다. 한글 글자 수와 바이트 수는 다를 수 있으므로 write 반환값을 파일 바이트 크기로 해석하지 않습니다.

with 블록에 파일 열기를 배치하면 블록을 나갈 때 파일이 닫힙니다. 정상적으로 끝나거나 블록 안에서 예외가 발생해도 닫기 작업을 수행합니다. 닫힌 핸들에 다시 쓰면 ValueError: I/O operation on closed file 같은 메시지를 만날 수 있습니다. 이는 파일이 사라졌다는 뜻이 아니라 수명이 끝난 연결을 재사용했다는 뜻입니다. 저장 함수를 호출할 때마다 필요한 범위에서 열고 닫으면 핸들을 전역 변수로 공유할 이유가 줄어듭니다.

출력 내용은 운영체제와 Python의 버퍼를 거칠 수 있습니다. with를 벗어나 닫는 일은 정상 종료 시 파일 내용을 내보내는 데 필요하지만 정전에도 살아남는다는 보장은 아닙니다. 또한 with는 w로 이미 지운 기존 내용을 되돌리지 않습니다. 이 레슨에서는 정상적인 저장·읽기 수명을 익히고, 마지막 미션에서는 먼저 검증하고 임시 파일을 닫은 뒤 교체하는 순서로 실패 범위를 줄입니다.

저장한 값을 다른 실행에서 읽습니다

변수에 넣었던 제목을 print로 다시 보여 주면 메모리의 값만 확인한 것입니다. 저장 검증은 파일을 다시 열어 내용을 읽고 비교해야 합니다. 더 강한 확인은 첫 프로세스에서 파일을 쓰고 두 번째 프로세스에서 읽는 방법입니다. 첫 실행의 딕셔너리나 지역 변수에 기대지 않고 같은 제목이 나와야 합니다. 테스트의 test_new_process가 이 상황을 재현합니다. 디스크 지속성은 프로그램을 두 번 실행하는 사용자 행동과 연결해서 설명합니다.

인코딩은 쓰는 쪽과 읽는 쪽의 약속입니다. 이 실습은 양쪽 모두 encoding="utf-8"을 지정합니다. UnicodeDecodeError는 바이트를 지정한 문자 인코딩으로 읽지 못했다는 뜻이며 파일 없음과 다릅니다. except Exception으로 모든 오류를 빈 문자열로 바꾸면 손상 파일도 정상적인 첫 실행처럼 보여 버립니다. 기대한 FileNotFoundError만 잡고 나머지는 호출자에게 전달하면 실제 원인을 보존할 수 있습니다.

문자열을 읽었을 때 마지막 줄바꿈도 내용에 포함됩니다. print는 자체 줄바꿈을 추가하므로 파일 끝에 줄바꿈이 있으면 화면에 빈 줄이 더 생길 수 있습니다. repr을 사용하면 보이지 않는 줄바꿈과 공백을 확인할 수 있습니다. 제목의 양끝 공백을 정리하는 입력 규칙과 저장 문자열을 그대로 읽는 규칙을 섞지 않습니다. load_note에서 strip을 해 버리면 저장했던 내용을 정확히 복원하는 계약을 깨뜨립니다.

실패를 읽고 수정합니다

FileNotFoundError가 나면 우선 예외 메시지의 파일 이름을 읽고 현재 폴더에서 그 경로가 존재하는지 확인합니다. PermissionError이면 위치는 맞더라도 읽기 또는 쓰기 권한이 없는 상황을 의심합니다. UnicodeDecodeError이면 원본 파일의 실제 인코딩과 읽기 옵션을 확인합니다. 오류 이름과 파일 경로를 함께 적으면 동료가 같은 문제를 재현하기 쉽습니다. 예외를 숨기기보다 분류하는 습관이 파일 작업의 기본입니다.

로컬 과제는 save_note(path,text)가 내용을 전체 교체하고 load_note(path)가 파일 내용을 그대로 반환하도록 완성합니다. 부모 폴더는 이미 존재합니다. 없는 파일만 빈 문자열로 처리하며 한글·쉼표·여러 줄·두 번 저장·새 프로세스 읽기·잘못된 바이트를 확인합니다. 정상 저장의 반복과 실패 처리 정책을 분리해서 구현하고 테스트 이름으로 어느 책임이 어긋났는지 판단합니다. 한 테스트만 보고 구현을 바꾸기 전에 함수 계약을 다시 읽습니다.

따라하기

닫힌 핸들과 남은 파일

다음 코드를 Python 파일에 저장해 실행합니다. 파일 연결이 닫혀도 파일 내용은 남습니다.

from pathlib import Path
path = Path("note.txt")
with path.open("w", encoding="utf-8") as f:
    f.write("책, 산책\n")
print(f.closed)
with path.open(encoding="utf-8") as f:
    print(repr(f.read()))

실행 결과

True
'책, 산책\n'

읽기 예외를 구분하기

파일 없음과 문자 해석 실패를 각각 재현합니다. 예외 이름이 다른 것을 확인합니다.

from pathlib import Path
for name in ("missing.txt", "bad.txt"):
    if name == "bad.txt":
        Path(name).write_bytes(b"\xff")
    try:
        print(Path(name).read_text(encoding="utf-8"))
    except (FileNotFoundError, UnicodeDecodeError) as e:
        print(type(e).__name__)

실행 결과

FileNotFoundError
UnicodeDecodeError

새 프로세스에서 확인하기

첫 실행이 저장한 내용을 새 Python 프로세스가 읽도록 합니다.

from pathlib import Path
import subprocess, sys
Path("note.txt").write_text("한글 기록", encoding="utf-8")
result = subprocess.run([sys.executable, "-c", 'from pathlib import Path; print(Path("note.txt").read_text(encoding="utf-8"))'], capture_output=True, text=True, check=True)
print(result.stdout, end="")

실행 결과

한글 기록

확인 문제

실습

notebook.py의 save_note·load_note를 완성합니다. 저장은 UTF-8 전체 교체이고 읽기는 내용 그대로 반환합니다. 없는 파일만 빈 문자열로 처리하며 다른 오류는 전달합니다. README의 명령으로 starter 일부 실패와 수정 후 5개 통과를 확인합니다.

시작 코드·테스트 내려받기

실행 명령

python3 -m unittest discover -s tests -p "test_*.py"

기대 결과

5개 테스트 통과

모범 답안모범 답안 내려받기

더 읽기

면접 질문

  • 메모리와 디스크에 저장된 데이터의 차이를 설명합니다.