파일과 예외 처리, 그리고 첫 자동화 프로젝트
파일 경로와 인코딩, 텍스트·CSV·JSON 파일 읽고 쓰기, try와 except로 오류를 다루는 법을 익히고, 지금까지 배운 것을 모아 다운로드 폴더 정리기와 가계부 요약기를 만들어요.
프로그램이 끝나면 변수에 담긴 값은 모두 사라져요. 결과를 남기고, 남이 만든 데이터를 읽어 오려면 파일을 다룰 줄 알아야 해요. 그리고 파일을 다루기 시작하면 "파일이 없어요", "권한이 없어요" 같은 일이 반드시 생겨요. 그래서 이번 강의는 파일과 예외 처리를 함께 배우고, 마지막에 실제로 쓸모 있는 자동화 프로그램 두 개를 만들어요.
경로: 파일의 주소
파일은 폴더 안에 있고, 폴더는 또 다른 폴더 안에 있어요. 파일까지 가는 길을 경로라고 해요.
- 절대 경로: 맨 꼭대기부터 적은 전체 주소. 윈도우는
C:\Users\minji\study\data.txt, 맥과 리눅스는/Users/minji/study/data.txt처럼 생겼어요. - 상대 경로: 지금 위치(현재 작업 폴더)를 기준으로 적은 주소.
data.txt,reports/2026.csv,../backup/old.txt처럼요...은 한 단계 위 폴더예요.
상대 경로는 "프로그램을 어디서 실행했는가"에 따라 가리키는 곳이 달라져요. 파일을 못 찾는 오류의 절반은 여기서 생겨요.
pathlib으로 경로 다루기
경로를 문자열로 이어 붙이면 운영체제마다 구분 기호(\와 /)가 달라서 문제가 생겨요. pathlib을 쓰면 알아서 처리해 줘요.
from pathlib import Path
base = Path(__file__).parent # 이 코드 파일이 있는 폴더
data = base / "data" / "sales.csv" # / 로 이어 붙이기
print(data.exists()) # 있는지 확인
print(data.suffix) # .csv 확장자
print(data.stem) # sales 이름만
(base / "output").mkdir(exist_ok=True) # 폴더 만들기 (이미 있으면 그냥 넘어감)
for p in base.glob("*.txt"): # 이 폴더의 모든 txt 파일
print(p.name)
Path(__file__).parent를 기준으로 경로를 만들면, 어디서 실행하든 코드 파일 옆의 데이터를 정확히 찾아요. 습관으로 만드세요.
텍스트 파일 읽고 쓰기
쓰기
with open("memo.txt", "w", encoding="utf-8") as f:
f.write("첫 줄\n")
f.write("둘째 줄\n")
"w": 쓰기 모드. 파일이 있으면 내용을 지우고 새로 써요."a": 이어 쓰기 모드. 기존 내용 뒤에 덧붙여요."r": 읽기 모드(기본값).\n: 줄바꿈 문자예요.
with 블록이 끝나면 파일이 자동으로 닫혀요. 파일을 닫지 않으면 내용이 저장되지 않거나 다른 프로그램이 그 파일을 못 쓰는 일이 생겨요. 파일은 언제나 with로 여세요.
읽기
with open("memo.txt", encoding="utf-8") as f:
text = f.read() # 전체를 한 문자열로
with open("memo.txt", encoding="utf-8") as f:
for line in f: # 한 줄씩 (큰 파일도 메모리 걱정 없음)
print(line.strip())
인코딩을 꼭 적는 이유
글자는 컴퓨터에 숫자로 저장되는데, 어떤 숫자를 어떤 글자로 볼지 정한 약속이 인코딩이에요. 지금 세계 표준은 UTF-8이에요. 문제는 윈도우의 일부 환경이 기본값으로 옛 한국어 인코딩(CP949)을 쓴다는 거예요. 인코딩을 적지 않으면 같은 코드가 맥에서는 되고 윈도우에서는 한글이 깨지거나 UnicodeDecodeError가 나요.
주의 파일을 열 때는 항상
encoding="utf-8"을 적으세요. 엑셀이 만든 CSV를 읽을 때 첫 칸 이름 앞에 이상한 글자가 붙으면encoding="utf-8-sig"로 열어 보세요.
CSV: 표 데이터
엑셀 같은 표 데이터를 주고받는 가장 흔한 형식이 CSV예요. 쉼표로 칸을 나눈 텍스트 파일이에요.
날짜,분류,금액
2026-11-01,식비,12000
2026-11-02,교통,1450
2026-11-02,식비,8500
import csv
with open("expenses.csv", encoding="utf-8-sig", newline="") as f:
reader = csv.DictReader(f) # 첫 줄을 열 이름으로 써요
rows = list(reader)
print(rows[0]) # {'날짜': '2026-11-01', '분류': '식비', '금액': '12000'}
total = sum(int(r["금액"]) for r in rows)
읽어 온 값은 모두 문자열이라는 점을 잊지 마세요. 계산하려면 int()로 바꿔야 해요. 쓰기는 csv.DictWriter로 해요.
with open("summary.csv", "w", encoding="utf-8-sig", newline="") as f:
writer = csv.DictWriter(f, fieldnames=["분류", "합계"])
writer.writeheader()
writer.writerow({"분류": "식비", "합계": 20500})
utf-8-sig로 저장하면 엑셀에서 열어도 한글이 깨지지 않아요. newline=""은 윈도우에서 줄이 하나씩 비는 문제를 막아 줘요.
JSON 파일
지난 강의의 JSON은 파일로도 그대로 저장할 수 있어요.
import json
settings = {"theme": "dark", "font_size": 14}
with open("settings.json", "w", encoding="utf-8") as f:
json.dump(settings, f, ensure_ascii=False, indent=2)
with open("settings.json", encoding="utf-8") as f:
loaded = json.load(f)
프로그램의 설정, 지난번 실행 결과, 작은 데이터베이스 대용으로 아주 편해요.
예외 처리: 오류가 나도 무너지지 않기
예외란
실행 중에 생기는 오류를 예외라고 해요. 처리하지 않으면 프로그램이 그 자리에서 멈춰요. 예외 처리는 "이런 일이 생기면 이렇게 대응하라"를 미리 적어 두는 거예요.
try:
age = int(input("나이: "))
print(f"내년에 {age + 1}살")
except ValueError:
print("숫자로 입력해 주세요.")
try 블록에서 ValueError가 나면 프로그램이 멈추는 대신 except 블록으로 건너가요.
전체 구조
from pathlib import Path
def read_number_file(path):
try:
text = Path(path).read_text(encoding="utf-8")
number = int(text.strip())
except FileNotFoundError:
print(f"파일이 없어요: {path}")
return None
except ValueError:
print("파일 내용이 숫자가 아니에요.")
return None
else:
return number # 오류가 없을 때만 실행
finally:
print("읽기 시도 끝") # 오류가 있든 없든 항상 실행
좋은 예외 처리의 원칙
- 구체적인 예외를 잡으세요.
except:나except Exception:으로 모든 오류를 뭉뚱그려 잡으면 진짜 버그까지 숨겨져요. 예상한 오류만 이름을 대고 잡으세요. - 조용히 삼키지 마세요.
except ValueError: pass처럼 아무것도 안 하면, 나중에 왜 결과가 이상한지 아무도 몰라요. 최소한 기록은 남기세요. - 복구할 수 없으면 다시 던지세요. 처리할 방법이 없는 오류는 잡지 말고 위로 올려 보내는 게 맞아요.
직접 예외를 일으킬 수도 있어요.
def withdraw(balance, amount):
if amount > balance:
raise ValueError(f"잔액 부족: {balance}원 있는데 {amount}원을 뺄 수 없어요")
return balance - amount
잘못된 값을 조용히 넘기는 것보다, 문제를 분명한 메시지와 함께 바로 드러내는 편이 훨씬 안전해요.
기록 남기기: logging
자동화 프로그램은 사람이 보지 않을 때 돌아가요. 무슨 일이 있었는지 나중에 알 수 있게 기록을 남겨야 해요.
import logging
logging.basicConfig(
filename="app.log",
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
encoding="utf-8",
)
logging.info("작업 시작")
logging.warning("파일 3개를 건너뜀")
logging.error("저장 실패")
print는 화면에서 사라지지만 로그 파일은 남아요. 문제가 생겼을 때 "언제, 무엇이" 있었는지 거꾸로 따라갈 수 있어요.
프로젝트 1: 다운로드 폴더 정리기
다운로드 폴더에 쌓인 파일을 확장자별 폴더로 옮기는 프로그램이에요. 파일을 옮기는 프로그램은 실수하면 되돌리기 어려워서, 먼저 무엇을 할지 보여 주고(미리보기), 확인한 뒤에 실행하게 만들 거예요.
import shutil
from pathlib import Path
RULES = {
"이미지": {".jpg", ".jpeg", ".png", ".gif", ".webp"},
"문서": {".pdf", ".docx", ".xlsx", ".pptx", ".txt", ".hwp"},
"압축": {".zip", ".7z", ".rar"},
"설치파일": {".exe", ".msi", ".dmg"},
}
def folder_for(path):
ext = path.suffix.lower()
for folder, exts in RULES.items():
if ext in exts:
return folder
return None
def plan_moves(target):
moves = []
for p in target.iterdir():
if p.is_file():
folder = folder_for(p)
if folder:
moves.append((p, target / folder / p.name))
return moves
def main():
target = Path.home() / "Downloads"
moves = plan_moves(target)
if not moves:
print("정리할 파일이 없어요.")
return
for src, dst in moves:
print(f"{src.name} → {dst.parent.name}/")
if input(f"{len(moves)}개 파일을 옮길까요? (y/n): ") != "y":
print("취소했어요.")
return
for src, dst in moves:
try:
dst.parent.mkdir(exist_ok=True)
if dst.exists():
print(f"건너뜀(같은 이름 있음): {src.name}")
continue
shutil.move(src, dst)
except PermissionError:
print(f"사용 중이라 못 옮김: {src.name}")
print("정리 완료!")
if __name__ == "__main__":
main()
짚어 볼 점이에요.
- 계획과 실행을 나눴어요.
plan_moves는 아무것도 바꾸지 않고 목록만 만들어요. 그래서 안심하고 미리 볼 수 있어요. - 덮어쓰지 않아요. 같은 이름의 파일이 있으면 건너뛰어요. 데이터를 지우는 쪽보다 남기는 쪽이 언제나 안전해요.
- 예상한 예외만 잡았어요. 열려 있는 파일을 옮기려 할 때 나는
PermissionError만 처리했어요.
프로젝트 2: 가계부 요약기
앞의 expenses.csv처럼 생긴 파일을 읽어 분류별 합계와 비율을 보여 주고, 결과를 파일로 저장해요.
import csv
from collections import defaultdict
from pathlib import Path
def load(path):
with open(path, encoding="utf-8-sig", newline="") as f:
return list(csv.DictReader(f))
def summarize(rows):
totals = defaultdict(int)
skipped = 0
for r in rows:
try:
totals[r["분류"]] += int(r["금액"])
except (KeyError, ValueError):
skipped += 1
return dict(totals), skipped
def main():
base = Path(__file__).parent
rows = load(base / "expenses.csv")
totals, skipped = summarize(rows)
grand = sum(totals.values())
lines = [f"총 지출: {grand:,}원"]
for cat, amount in sorted(totals.items(), key=lambda x: x[1], reverse=True):
lines.append(f"- {cat}: {amount:,}원 ({amount / grand:.1%})")
if skipped:
lines.append(f"(형식이 잘못된 {skipped}줄은 건너뜀)")
report = "\n".join(lines)
print(report)
(base / "report.txt").write_text(report, encoding="utf-8")
if __name__ == "__main__":
main()
defaultdict(int)는 처음 보는 키를 0으로 시작해 주는 딕셔너리예요. 지난 강의의 get(key, 0) 패턴을 더 짧게 쓴 거예요. 금액 칸이 비어 있거나 숫자가 아닌 줄이 있어도 프로그램이 멈추지 않고, 몇 줄을 건너뛰었는지 알려 줘요.
매일 저절로 돌게 하기
만든 프로그램을 정해진 시간에 자동으로 실행하면 진짜 자동화가 돼요.
- 윈도우: 작업 스케줄러에서 "기본 작업 만들기"를 누르고, 프로그램으로
python, 인수로 스크립트 경로를 넣어요. - 맥과 리눅스:
crontab -e로0 9 * * * /usr/bin/python3 /home/me/tidy.py같은 줄을 추가하면 매일 9시에 실행돼요. - 클라우드: GitHub Actions의 예약 실행을 쓰면 내 컴퓨터가 꺼져 있어도 돌아가요(서버 트랙에서 다뤄요).
자주 하는 실수
- "w" 모드로 기존 파일을 날리기. 덧붙이려면
"a"예요. 중요한 파일은 쓰기 전에 백업하세요. - 인코딩 생략. 윈도우에서 한글이 깨지는 대부분의 원인이에요.
- 모든 예외를 잡고 무시하기. 버그를 숨기는 가장 빠른 길이에요.
- 실행 위치에 기대는 상대 경로.
Path(__file__).parent를 기준으로 경로를 만드세요.
점검 문제
open("a.txt", "w")로 연 파일이 이미 있으면 어떻게 되나요?with를 쓰는 이유는?- 아래 코드의 문제점은?
try:
result = process(data)
except:
pass
- CSV에서 읽은 금액으로 합계를 구했더니 숫자가 아니라 이어 붙인 글자가 나왔어요. 원인은?
정답
- 기존 내용이 모두 지워지고 새로 써져요.
- 블록이 끝나면 오류가 났더라도 파일을 자동으로 닫아 줘서, 저장 누락이나 파일 잠김을 막아요.
- 모든 예외를 잡고 아무것도 하지 않아서, 어떤 오류가 났는지 알 수 없어요. 예상한 예외만 이름을 대고 잡고, 최소한 기록을 남겨야 해요.
- CSV에서 읽은 값은 모두 문자열이라서
+가 이어 붙이기로 동작했어요.int()로 바꾼 뒤 더해야 해요.
프로그래밍 기초를 마치며
여기까지 왔다면 변수, 조건, 반복, 함수, 자료 구조, 파일, 예외라는 프로그래밍의 핵심 재료를 모두 손에 쥔 거예요. 이 재료만으로도 일상의 반복 작업 상당 부분을 자동화할 수 있어요.
다음 걸음은 목표에 따라 갈려요. 서버와 웹을 향한다면 리눅스와 터미널 트랙으로, AI를 향한다면 수학 기초 트랙으로 이어 가세요. 어느 쪽이든, 이번 강의의 프로젝트를 내 상황에 맞게 고쳐서 실제로 매일 쓰는 것이 최고의 복습이에요.