문서 읽는 데 52분 · E2

E-2: 파일과 데이터

목차 29
전체 22강 중 16강 · 파이썬 기초
난이도 · 입문

ℹ️프로그래밍 경험이 없어도 시작할 수 있는 파이썬 입문 — ‘개발 입문’을 먼저 들으면 더 수월해요. AI·데이터 분석으로 이어지는 출발점이에요.

안녕하세요, 홍순구 튜터입니다. 지난 시간엔 에러를 대하는 자세를 완전히 바꿨죠. try/except로 받아내고, raise로 직접 일으키고, 의미 있는 이름의 커스텀 예외까지 만들었어요. 그런데 그때 한 가지 아쉬운 게 있었어요. 우리가 만든 회원 저장소(MemberRepository)는 프로그램이 켜져 있는 동안만 살아 있었거든요. 프로그램을 끄면? 애써 가입시킨 회원이 전부 사라졌죠.

오늘은 그 데이터를 파일에 저장하고 다시 불러와요. 콘솔에만 머물던 우리 객체들이 드디어 파일로 옮겨가서, 프로그램을 껐다 켜도 그대로 남아 있게 만드는 거예요. 이걸 영속화(persistence, 데이터를 오래 보존하기)라고 불러요.

지난 시간 finally에서 "무슨 일이 있어도 정리한다"고 한 약속 기억나시죠? 오늘 그 정신이 with라는 한 줄로 더 편하게 이어져요. 자, 데이터에 생명을 불어넣으러 가봅시다.

텍스트
 [지난 시간]  메모리에만 살던 데이터  프로그램 끄면 사라짐
                         │
                         
 [오늘]  파일로 영속화 — 껐다 켜도 남는다

   with open    파일에 쓰고 읽기 (블록 끝나면 자동으로 닫힘)
   pathlib      경로를 객체로 (data/note.txt)
   json         데이터  글자 (저장 dump · 복원 load)
   객체  JSON   asdict 로 재우고, Member(**d) 로 깨우기
   csv · toml   표 형식 · 설정 파일 맛보기
                         │
                         
        회원 저장소를 JSON 파일에 영속화

💡 오늘 수업의 핵심 — "파일에 저장하고 다시 불러오기, 객체를 JSON으로 재웠다 깨우기"

🎯 학습 목표

  • with open()pathlib로 파일을 안전하게 읽고 써요.
  • json으로 파이썬 데이터를 글자로 바꿔 저장하고, 다시 데이터로 되살려요.
  • 우리 도메인 객체(Member)를 JSON 파일로 영속화하고, CSV·TOML 같은 다른 형식도 가볍게 다뤄요.

Step 1: "파일에 처음 적어보기 — with open()"

지금까지 우리가 다룬 데이터는 전부 print로 화면에 잠깐 보여주고 끝이었어요. 화면은 프로그램이 끝나면 사라지죠. 데이터를 오래 남기려면 어딘가에 적어둬야 해요. 그 "어딘가"가 바로 파일이에요.

화이트보드와 공책을 떠올려 보세요. 화면(print)은 화이트보드라서 지우면 끝이지만, 파일은 공책이라 덮어도 다음에 펴보면 그대로 남아 있어요. 파이썬에서 파일에 적는 가장 기본적인 방법을 봅시다.

Python
# src/instagram/e2_write.py
with open("greeting.txt", "w", encoding="utf-8") as f:
    f.write("안녕하세요 @minji\n")
    f.write("인스타그램에 오신 걸 환영해요\n")

print("greeting.txt 에 저장했어요")

실행하면 greeting.txt라는 파일이 새로 생기고, 그 안에 두 줄이 적혀요. 한 줄씩 뜯어볼게요.

  • open("greeting.txt", "w", encoding="utf-8") — 파일을 연다는 뜻이에요. 첫 번째는 파일 이름, "w"는 쓰기(write) 모드, encoding="utf-8"은 한글이 깨지지 않게 하는 약속이에요.
  • with ... as f: — 연 파일을 f라는 이름으로 쓰겠다는 거예요. 그리고 with 블록(들여쓴 부분)이 끝나면 파일을 자동으로 닫아줘요(close).
  • f.write(...) — 파일에 글자를 적어요.

여기서 with가 핵심이에요. 원래 파일은 다 쓰고 나면 반드시 닫아야 해요. 안 닫으면 내용이 제대로 저장 안 되거나 자원이 새거든요. 그런데 사람은 깜빡하기 마련이죠. 지난 시간 finally로 "무슨 일이 있어도 닫는다"를 보장했던 것 기억나시죠? with는 그 finally를 한 줄에 담아둔 거예요. 블록을 벗어나는 순간, 에러가 났든 안 났든 파일을 알아서 닫아줘요.

⚠️ "w" 모드는 파일이 이미 있으면 기존 내용을 통째로 지우고 새로 써요. 다음 Step에서 "지우지 않고 덧붙이는" 모드를 따로 볼게요.

💡 한 줄 정리

with open(파일, "w", encoding="utf-8") as f: 안에서 f.write(...)로 파일에 적고, 블록이 끝나면 파일이 자동으로 닫혀요.

🙋 학생 질문 — "튜터님, 왜 글자 끝마다 \n을 일일이 붙여요?"

좋은 관찰이에요. print는 출력할 때마다 줄바꿈을 자동으로 해줬지만, f.write는 그러지 않아요. 적으라는 글자를 딱 그대로만 적죠. 그래서 줄을 나누고 싶으면 줄바꿈 문자인 \n을 직접 붙여줘야 해요. \n을 빼면 두 문장이 한 줄에 쭉 이어 붙어요.


Step 2: "파일 읽기와 모드 — r / w / a"

방금 파일에 적었으니, 이번엔 적어둔 걸 다시 읽어봐요. 그리고 파일을 여는 방식(모드)이 세 가지 있다는 것도 함께 정리할게요.

Python
# src/instagram/e2_read.py

# 먼저 쓰기 모드 "w" 로 두 줄을 적어둔다 (Step 1 복습)
with open("diary.txt", "w", encoding="utf-8") as f:
    f.write("1일차: 시작\n")
    f.write("2일차: 계속\n")

# 읽기 모드 "r" — 파일 전체를 하나의 문자열로
with open("diary.txt", "r", encoding="utf-8") as f:
    content = f.read()
print(content)

# 한 줄씩 리스트로
with open("diary.txt", "r", encoding="utf-8") as f:
    lines = f.readlines()
print(lines)

출력을 보면 이래요.

텍스트
1일차: 시작
2일차: 계속

['1일차: 시작\n', '2일차: 계속\n']
  • f.read() — 파일 내용을 통째로 하나의 문자열로 가져와요.
  • f.readlines() — 한 줄씩 끊어서 리스트로 가져와요. 각 줄 끝에 \n이 그대로 붙어 있는 게 보이죠?

이제 모드 이야기예요. open의 두 번째 글자가 파일을 어떻게 열지를 정해요.

모드 동작
"r" read(읽기) 파일을 읽어요 (적지 않으면 기본값)
"w" write(쓰기) 새로 써요 — 기존 내용을 전부 덮어써요 ⚠️
"a" append(이어쓰기) 기존 내용 뒤에 덧붙여요

세 번째 모드 "a"를 직접 보면 차이가 분명해져요.

Python
# 이어쓰기 모드 "a" — 기존 내용을 지우지 않고 뒤에 덧붙인다
with open("diary.txt", "a", encoding="utf-8") as f:
    f.write("3일차: 추가\n")

with open("diary.txt", "r", encoding="utf-8") as f:
    print(f.read())
텍스트
1일차: 시작
2일차: 계속
3일차: 추가

"w"였다면 앞의 두 줄이 사라지고 "3일차"만 남았을 거예요. "a"라서 일기를 이어 쓰듯 뒤에 붙었죠. 그리고 매번 encoding="utf-8"을 적어주는 습관을 들이세요. 이걸 빼면 컴퓨터 환경에 따라 한글이 깨질 수 있어서, 한글을 다루는 우리는 항상 챙겨주는 게 안전해요.

💡 한 줄 정리

"r"은 읽기, "w"는 덮어쓰기, "a"는 이어쓰기예요. read()는 통째로, readlines()는 줄 단위 리스트로 읽어와요.

🙋 학생 질문 — "튜터님, read()readlines()는 언제 뭘 써요?"

파일 내용을 한 덩어리 문자열로 다루고 싶으면 read(), 한 줄 한 줄 따로 처리하고 싶으면 readlines()예요. 예를 들어 일기를 통째로 화면에 보여줄 땐 read()가 편하고, 줄마다 번호를 매기거나 특정 줄만 거를 거면 리스트로 받는 readlines()가 편해요. 상황에 맞게 고르면 돼요.


Step 3: "경로를 객체로 — pathlib.Path"

파일을 다루다 보면 경로(path)를 자주 만나요. "data/note.txt" 같은 거요. 그런데 경로를 그냥 문자열로 다루면 불편한 점이 있어요. 폴더 구분 기호가 운영체제마다 다르거든요(맥은 /, 윈도우는 \). 또 "이 폴더가 있나? 없으면 만들어야 하나?"를 확인하는 것도 번거롭죠.

이런 걸 깔끔하게 해주는 도구가 pathlib이에요. 경로를 그냥 글자가 아니라 경로 전용 객체로 다뤄요.

Python
# src/instagram/e2_pathlib.py
from pathlib import Path

# 경로를 객체로 만든다
data_dir = Path("data")
data_dir.mkdir(exist_ok=True)           # 폴더가 없으면 만들고, 있으면 그냥 둔다

# / 연산자로 경로를 잇는다 — 운영체제에 맞는 구분자를 알아서 쓴다
note_path = data_dir / "note.txt"
print(note_path)                        # data/note.txt

# read_text / write_text — with open 없이 한 줄로 쓰고 읽는다
note_path.write_text("오늘도 한 걸음\n", encoding="utf-8")
print(note_path.read_text(encoding="utf-8"))
텍스트
data/note.txt
오늘도 한 걸음

세 가지가 눈에 띄죠.

  • Path("data").mkdir(exist_ok=True)data 폴더를 만들어요. exist_ok=True는 "이미 있으면 에러 내지 말고 그냥 넘어가라"는 뜻이에요.
  • data_dir / "note.txt" — 나눗셈이 아니라 경로를 잇는 거예요. Path끼리는 /로 경로를 이어 붙일 수 있어요. 직관적이죠?
  • write_text / read_textwith open 없이 한 줄로 파일 전체를 쓰고 읽어요.

Path 객체는 경로에 대한 정보도 물어보면 알려줘요.

Python
print(note_path.exists())               # True — 파일이 있나?
print(note_path.name)                   # note.txt — 파일 이름
print(note_path.suffix)                 # .txt — 확장자

💡 한 줄 정리

pathlib.Path로 경로를 객체로 다루면 /로 경로를 잇고, mkdir로 폴더를 만들고, write_text/read_text로 한 줄에 읽고 쓸 수 있어요.

🙋 학생 질문 — "튜터님, with open이랑 write_text 중 뭘 써요?"

짧게 한 번에 파일 전체를 읽거나 쓸 거면 write_text/read_text가 간편해요. 한 줄이면 끝이니까요. 반대로 파일을 조금씩 여러 번 나눠 쓰거나, 아주 큰 파일을 다룰 땐 with open이 더 알맞아요. 오늘은 둘 다 쓰면서 감을 잡아갈 거예요.


Step 4: "데이터를 글자로 — json 기초"

여기서 한 가지 문제를 짚고 가요. 파일에는 글자(문자열)만 저장할 수 있어요. 그런데 우리가 다루는 데이터는 dict도 있고 list도 있고 숫자, True/False도 있죠. 이런 다양한 모양을 파일에 적으려면 일단 글자로 바꿔야 하고, 다시 읽을 땐 원래 모양으로 되돌려야 해요.

이 변환을 아무렇게나 하면 서로 못 알아보겠죠. 그래서 전 세계가 합의한 표준 형식이 있어요. 바로 JSON(JavaScript Object Notation)이에요. 데이터를 글자로 표현하는 약속인데, 거의 모든 프로그래밍 언어와 웹이 이 형식으로 데이터를 주고받아요.

이사를 떠올려 보세요. 큰 가구를 그대로는 못 옮기니까 납작하게 분해해서(직렬화) 트럭에 싣고, 도착하면 다시 조립(역직렬화)하잖아요. JSON이 딱 그 역할이에요.

Python
# src/instagram/e2_json_str.py
import json

profile = {
    "username": "minji",
    "followers": 12000,
    "interests": ["여행", "사진"],
    "verified": True,                   # 파이썬 True 가 JSON 에서는 true 로
}

# dict → JSON 문자열 (dumps = dump string)
text = json.dumps(profile)
print(text)

출력은 이래요.

텍스트
{"username": "minji", "followers": 12000, "interests": ["여행", "사진"], "verified": true}

dict가 글자 하나로 쫙 펴졌어요. 그런데 한글 "여행"이 여행처럼 알 수 없는 기호로 바뀌었죠? JSON이 기본적으로 한글을 이런 식으로 이스케이프(escape)해버려서 그래요. 사람이 읽기 어렵죠. 두 가지 옵션으로 고쳐봅시다.

Python
pretty = json.dumps(profile, ensure_ascii=False, indent=2)
print(pretty)
텍스트
{
  "username": "minji",
  "followers": 12000,
  "interests": [
    "여행",
    "사진"
  ],
  "verified": true
}
  • ensure_ascii=False — 한글을 그대로 보여줘요. 한국어 데이터를 다룰 땐 거의 항상 이걸 켜요.
  • indent=2 — 보기 좋게 들여쓰기해서 사람이 읽기 편하게 만들어요.

이제 반대로, 글자를 다시 데이터로 되돌려 봅시다.

Python
# JSON 문자열 → dict (loads = load string)
back = json.loads(text)
print(back["username"], back["followers"])
print(back["verified"], type(back["verified"]))   # True <class 'bool'>
print(profile == back)                              # True — 완벽하게 되돌아왔다
텍스트
minji 12000
True <class 'bool'>
True

JSON의 true가 다시 파이썬 True로 정확히 돌아왔어요. 마지막 줄 profile == backTrue라는 건, 분해했다 다시 조립한 데이터가 원본과 완전히 똑같다는 뜻이에요. 파이썬과 JSON의 자료형은 이렇게 짝지어져요.

파이썬 JSON
dict object { }
list array [ ]
str string
int / float number
True / False true / false
None null

💡 한 줄 정리

json.dumps는 데이터를 글자로(직렬화), json.loads는 글자를 데이터로(역직렬화) 바꿔요. 한글은 ensure_ascii=False로 살려요.

🙋 학생 질문 — "튜터님, ensure_ascii가 정확히 뭐예요?"

ensure_ascii는 "결과를 영어·숫자·기호(ASCII)만으로 보장할까?"를 정하는 옵션이에요. 기본값이 True라서 한글 같은 비영어 글자를 여행 같은 안전한 코드로 바꿔버려요. 옛날 환경에서도 안 깨지게 하려는 배려인데, 사람이 못 읽는 게 흠이죠. 우리처럼 한글 데이터를 다루면 ensure_ascii=False로 꺼서 한글 그대로 저장하는 게 보기 좋아요.


Step 5: "파일에 직접 — json.dump / json.load"

Step 4에서는 데이터를 글자로 바꾸기만 했어요. 그 글자를 파일에 저장하려면 Step 1의 f.write를 또 써야겠죠. 그런데 json은 이 두 단계를 한 줄로 합쳐주는 도구가 있어요. 바로 dumpload예요.

Python
# src/instagram/e2_json_file.py
import json
from pathlib import Path

profile = {"username": "minji", "followers": 12000, "interests": ["여행", "사진"]}

Path("data").mkdir(exist_ok=True)
path = Path("data/profile.json")

# 저장: dump — 파일 객체(f)에 직접 적는다 (글자로 바꿔 write 하던 두 단계를 한 줄로)
with open(path, "w", encoding="utf-8") as f:
    json.dump(profile, f, ensure_ascii=False, indent=2)

# 불러오기: load — 파일 객체(f)에서 직접 읽어 dict 로
with open(path, "r", encoding="utf-8") as f:
    loaded = json.load(f)

print(loaded)
print(profile == loaded)                # True
텍스트
{'username': 'minji', 'followers': 12000, 'interests': ['여행', '사진']}
True

이름이 헷갈릴 수 있으니 정리해요. 끝에 s가 붙은 dumps/loads는 글자(string)를 다뤄요. s가 없는 dump/load는 파일을 직접 다루고요.

  • json.dump(데이터, 파일) — 데이터를 글자로 바꿔서 파일에 바로 적어요.
  • json.load(파일) — 파일에서 글자를 읽어 데이터로 바로 되살려요.

data/profile.json 파일을 열어보면 Step 4의 보기 좋은 JSON이 그대로 저장돼 있어요. 이제 프로그램을 꺼도 이 프로필은 파일에 남아 있죠.

💡 한 줄 정리

json.dump(데이터, 파일)은 데이터를 파일에 바로 저장하고, json.load(파일)은 파일에서 바로 읽어와요. s가 붙으면 글자, 없으면 파일이에요.

🙋 학생 질문 — "튜터님, dumpsdump, s 하나 차이가 그렇게 중요해요?"

네, 다루는 대상이 달라요. dumps(string)는 데이터를 글자로 바꿔서 변수에 담아요. 화면에 출력하거나 잠깐 들고 있을 때 좋죠. dump는 그 글자를 파일에 직접 적어요. 파일로 저장하는 게 목적이면 dump/load가 한 단계 덜 거쳐서 깔끔해요. "s는 string"이라고 외워두면 안 헷갈려요.


Step 6: "객체를 파일에 재우고 깨우기 — dataclass ↔ JSON"

드디어 오늘의 진짜 목표예요. 지난 시간들에서 만든 Member 객체를 파일에 저장하고, 다시 객체로 되살리는 거예요.

그런데 한 가지 걸림돌이 있어요. json은 dict·list·숫자·글자 같은 기본 자료형만 알아요. 우리가 만든 Member 같은 객체는 처음 보는 거라 어떻게 글자로 바꿔야 할지 몰라요. 그래서 객체를 먼저 dict로 번역해줘야 해요. 다행히 @dataclass로 만든 객체엔 asdict라는 도구가 있어서, 필드를 골라 dict로 만들어줘요.

Python
# src/instagram/e2_objects.py
import json
from dataclasses import asdict
from pathlib import Path

from instagram.member import Member

minji = Member("minji", followers=12000, interests=["여행", "사진"])

# 1) 객체 → dict : asdict 가 dataclass 의 필드만 골라 dict 로 만든다
d = asdict(minji)
print(d)
텍스트
{'username': 'minji', 'followers': 12000, 'bio': '', 'interests': ['여행', '사진']}

Member 객체가 평범한 dict로 번역됐어요. 이제 dict는 Step 5에서 배운 대로 JSON 파일로 저장할 수 있죠. 저장하고, 다시 읽어서 객체로 되살려 봅시다.

Python
# 2) dict → JSON 문자열 → 파일
Path("data").mkdir(exist_ok=True)
Path("data/minji.json").write_text(
    json.dumps(d, ensure_ascii=False, indent=2), encoding="utf-8"
)

# 3) 파일 → dict → 객체 : Member(**loaded) 로 dict 를 다시 Member 로 되살린다
loaded = json.loads(Path("data/minji.json").read_text(encoding="utf-8"))
revived = Member(**loaded)

print(revived)
print(revived.handle)       # @minji
print(minji == revived)     # True
텍스트
Member(username='minji', followers=12000, bio='', interests=['여행', '사진'])
@minji
True

마지막 단계가 핵심이에요. Member(**loaded)에서 **는 C-1에서 배운 그 친구예요. dict를 풀어서 키워드 인자로 넘기는 거죠. 즉 Member(**loaded)Member(username="minji", followers=12000, ...)와 똑같아요. dict의 키가 그대로 Member의 필드 이름과 맞아떨어지니까 깔끔하게 객체로 되살아나요.

그리고 되살아난 revived에서 revived.handle@minji로 잘 동작하죠? @property로 만든 계산된 속성도, == 비교도 전부 그대로 부활했어요. 파일에 잠들었던 객체가 똑같은 모습으로 깨어난 거예요. 지난 시간 "콘솔에만 살던 객체를 파일로 영속화한다"던 약속이 여기서 이뤄졌어요.

💡 한 줄 정리

객체는 asdict로 dict로 번역해 JSON으로 저장하고, 읽어온 dict는 Member(**d)로 다시 객체로 되살려요.

🙋 학생 질문 — "튜터님, 왜 asdict를 거쳐요? Member를 json에 바로 주면 안 돼요?"

json.dumps(minji)처럼 객체를 바로 넘기면 "이건 JSON으로 바꿀 수 없다"는 에러가 나요. json은 dict·list·숫자·글자 같은 표준 자료형만 알거든요. 우리가 직접 만든 Member는 json 입장에선 처음 보는 낯선 물건이죠. 그래서 json이 아는 모양인 dict로 한 번 번역해주는 거예요. asdict가 그 번역기 역할을 해요.


Step 7: "JSON 말고 다른 형식들 — CSV·TOML 가볍게"

JSON이 만능처럼 보이지만, 상황에 따라 더 어울리는 형식이 있어요. 자주 만나는 두 가지를 가볍게 맛볼게요.

먼저 CSV(Comma-Separated Values)예요. 엑셀 표를 떠올리면 돼요. 한 줄이 한 행(row)이고, 값은 쉼표로 구분해요. 회원 목록처럼 칸이 정해진 표 데이터에 잘 맞아요.

Python
# src/instagram/e2_csv.py
import csv
from pathlib import Path

Path("data").mkdir(exist_ok=True)

# 쓰기: DictWriter — 각 행(row)을 dict 로 적는다
rows = [
    {"username": "minji", "followers": 12000},
    {"username": "jaehoon", "followers": 8500},
    {"username": "seungwoo", "followers": 320},
]
with open("data/members.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["username", "followers"])
    writer.writeheader()                # 맨 윗줄에 열 이름(username,followers)
    writer.writerows(rows)

# 읽기: DictReader — 각 행을 dict 로 돌려준다
with open("data/members.csv", "r", newline="", encoding="utf-8") as f:
    reader = csv.DictReader(f)
    for row in reader:
        print(row["username"], row["followers"], type(row["followers"]))
텍스트
minji 12000 <class 'str'>
jaehoon 8500 <class 'str'>
seungwoo 320 <class 'str'>

여기서 꼭 짚을 게 있어요. followers12000(숫자)이 아니라 <class 'str'>, 즉 문자열로 읽혔죠? CSV는 그냥 글자만 줄줄이 적는 형식이라 타입 정보가 없어요. 그래서 숫자처럼 보여도 읽으면 전부 글자예요. 숫자로 쓰려면 직접 int()로 바꿔야 해요. JSON이 숫자와 True/False를 보존했던 것과 비교되죠.

다음은 TOML이에요. 사람이 손으로 쓰는 설정 파일에 잘 맞는 형식이에요. 파이썬은 tomllib이라는 도구로 이걸 읽어요. 한 가지 특이한 점은, tomllib은 읽기만 한다는 거예요(쓰기 기능은 표준 라이브러리에 없어요).

Python
# src/instagram/e2_toml.py
import tomllib
from pathlib import Path

# tomllib 은 읽기 전용 — 그래서 설정은 평범한 텍스트로 적어두고, 읽을 때만 tomllib 을 쓴다
config_text = """\
[app]
name = "instagram-clone"
version = "0.1.0"

[feed]
page_size = 20
show_ads = false
"""

Path("data").mkdir(exist_ok=True)
Path("data/config.toml").write_text(config_text, encoding="utf-8")

# 읽기: 반드시 바이너리 모드("rb")로 열어 tomllib.load 에 넘긴다
with open("data/config.toml", "rb") as f:
    config = tomllib.load(f)

print(config["app"]["name"])            # instagram-clone
print(config["feed"]["page_size"])      # 20
print(config["feed"]["show_ads"])       # False
텍스트
instagram-clone
20
False

TOML을 읽으면 중첩된 dict로 돌아와요. [app], [feed] 같은 묶음이 dict 안의 dict가 되죠. 그리고 tomllib.load에 넘길 파일은 꼭 "rb"(바이너리 읽기) 모드로 열어야 해요. 작은 약속이지만 빼먹으면 에러가 나니 기억해 두세요. TOML의 false가 파이썬 False로 잘 들어온 것도 보이죠.

💡 한 줄 정리

CSV는 표 형식 데이터(값은 전부 문자열), TOML은 설정 파일(tomllib은 읽기 전용·"rb" 모드)에 어울려요.

🙋 학생 질문 — "튜터님, 그래서 언제 뭘 써야 해요?"

대략 이렇게 기억하면 편해요. JSON은 프로그램끼리 데이터를 주고받거나 중첩된 구조를 저장할 때(웹에서 가장 흔해요), CSV는 엑셀과 주고받는 표 형식 대량 데이터에, TOML은 사람이 직접 손으로 고치는 설정 파일에 잘 맞아요. 우리 프로젝트의 회원·게시물 데이터는 중첩 구조가 자연스러우니 JSON이 가장 편해요.


Step 8: "종합 — 게시물 저장소를 파일로 영속화"

오늘 배운 걸 한곳에 모아봅시다. 지난 시간 메모리에만 살던 회원 저장소를, 이제 파일로 영속화해요. 회원 목록을 JSON으로 저장하고, 다시 읽어 Member 객체로 되살리는 두 함수예요.

Python
# src/instagram/storage.py
import json
from dataclasses import asdict
from pathlib import Path

from instagram.member import Member


def save_members(members, path):
    """Member 목록을 JSON 파일로 저장한다."""
    data = [asdict(m) for m in members]                 # 객체들을 dict 목록으로
    text = json.dumps(data, ensure_ascii=False, indent=2)
    Path(path).write_text(text, encoding="utf-8")


def load_members(path):
    """JSON 파일을 읽어 Member 객체 목록으로 되살린다. 파일이 없으면 빈 목록."""
    file = Path(path)
    if not file.exists():
        return []                                       # 저장한 적이 없으면 빈 저장소로 시작
    data = json.loads(file.read_text(encoding="utf-8"))
    return [Member(**d) for d in data]                  # dict 들을 다시 Member 로

오늘 배운 도구가 전부 모였어요. save_members는 회원 목록을 asdict로 dict 목록으로 바꾸고(Step 6), json.dumps로 글자로 만든 뒤(Step 4), write_text로 파일에 저장해요(Step 3). load_members는 거꾸로 읽어서 Member(**d)로 객체를 되살리고요.

load_members에서 눈여겨볼 한 줄이 있어요. if not file.exists(): return []. 처음 실행하면 저장 파일이 아직 없겠죠. 그때 에러를 내는 대신 빈 목록을 돌려줘서, 빈 저장소로 자연스럽게 시작하게 해요. 지난 시간 배운 "없는 경우를 미리 대비한다"는 마음가짐의 연장이에요.

실제로 저장하고 다시 불러오면 이렇게 동작해요.

Python
members = [
    Member("minji", followers=12000, interests=["여행", "사진"]),
    Member("jaehoon", followers=8500),
    Member("seungwoo", followers=320, bio="개발 공부 중"),
]

save_members(members, "data/members.json")
loaded = load_members("data/members.json")

for m in loaded:
    print(f"{m.handle} — 팔로워 {m.followers} (인플루언서? {m.is_influencer})")
print(f"원본과 같은가? {members == loaded}")
텍스트
@minji — 팔로워 12000 (인플루언서? True)
@jaehoon — 팔로워 8500 (인플루언서? False)
@seungwoo — 팔로워 320 (인플루언서? False)
원본과 같은가? True

저장했다 불러온 회원들이 원본과 완전히 똑같아요. 이 저장·복원 동작은 코드베이스의 test_e2_file_data.py가 여러 경우로 검증해 뒀어요. 저장한 뒤 불러오면 원본과 같은지, 파일이 없을 때 빈 목록이 나오는지까지요. 콘솔에만 머물던 우리 회원 저장소가 드디어 파일에 영속화됐어요.

💡 한 줄 정리

save_members/load_members로 회원 목록을 JSON 파일에 저장하고 객체로 되살려요. 파일이 없으면 빈 목록으로 안전하게 시작해요.

🙋 학생 질문 — "튜터님, load할 때 파일이 없으면 왜 빈 목록을 줘요? 에러를 내야 하지 않나요?"

프로그램을 처음 켜면 저장 파일이 당연히 없겠죠. 그때 에러로 멈추면 사용자는 아무것도 못 해요. 그래서 "저장된 게 없으면 빈 저장소로 시작한다"가 훨씬 자연스러워요. 회원이 가입하면 그때 저장되고, 다음에 켜면 그 파일을 읽어오는 거죠. 없는 상황을 미리 대비하는 건 지난 시간 예외 처리에서 익힌 자세 그대로예요.


마무리

오늘은 콘솔에만 살던 데이터에 생명을 불어넣었어요. 파일에 적고, 다시 읽고, 객체를 JSON으로 재웠다 깨우면서, 프로그램을 껐다 켜도 데이터가 남는 영속화를 처음 경험했죠.

오늘 배운 핵심 세 가지

  • 💡 하나with openpathlib로 파일을 안전하게 읽고 써요. with가 지난 시간 finally의 "항상 정리한다"를 한 줄로 담아 파일을 자동으로 닫아줘요. "r"/"w"/"a" 모드와 encoding="utf-8"도 챙겼고요.
  • 💡 json으로 파이썬 데이터를 글자로 바꿔 저장(dump)하고 되살려요(load). 한글은 ensure_ascii=False로 살리고, True·숫자 같은 타입은 그대로 보존돼요.
  • 💡 — 우리 객체 Memberasdict로 dict로 번역해 저장하고, Member(**d)로 다시 객체로 되살려요. 회원 저장소가 파일로 영속화됐어요.

다음 시간 예고

오늘 만든 save_members(members, path) 함수를 다시 볼까요. members가 뭐고 path가 뭔지, 이름만 봐선 정확히 모르죠. 회원 목록을 넘겨야 하는데 실수로 숫자 하나를 넘겨도 함수는 그냥 받아버려요. 다음 시간(F-1: 타입 힌트)엔 이런 함수와 우리 도메인 모델에 "이건 Member 목록, 이건 문자열 경로"라고 타입을 또박또박 적어줘요. 그러면 VS Code가 잘못된 값을 넘기는 순간 빨간 줄로 미리 잡아주죠. 예외와 파일을 다룬 카테고리 E를 마치고, 모던 파이썬(카테고리 F)으로 들어가는 첫걸음이에요.


과제

오늘 배운 파일과 JSON을 직접 손으로 써볼 차례예요. 막히면 교안의 해당 Step을 다시 펼쳐 보세요.

[기초] 메모장 저장하고 불러오기

할 일 목록을 파일에 저장하고 다시 읽어오는 두 함수를 만들어 보세요.

  • save_lines(path, lines) — 문자열 목록(lines)을 한 줄에 하나씩 파일에 저장해요.
  • load_lines(path) — 파일을 읽어 줄 목록으로 돌려주되, 줄 끝의 개행(\n)은 떼고 돌려줘요.
  • 예: ["우유 사기", "사진 올리기"]를 저장한 뒤 다시 불러오면 똑같이 ["우유 사기", "사진 올리기"]가 나오게 하세요.

힌트: 저장은 with open(path, "w", ...) 안에서 for로 한 줄씩 f.write(line + "\n"), 읽기는 줄마다 line.rstrip("\n")을 쓰면 돼요.

[응용] 게시물을 JSON으로 저장·복원

게시물 한 건을 JSON 파일로 저장하고 다시 dict로 읽어오는 함수를 만들어 보세요.

  • 게시물은 {"title": "제주 여행", "likes": 1240, "hashtags": ["여행", "제주"]} 같은 dict예요.
  • save_post(path, post) — 게시물 dict를 JSON 파일로 저장하되, 한글이 깨지지 않고 보기 좋게 들여쓰기되게 하세요.
  • load_post(path) — JSON 파일을 읽어 게시물 dict로 돌려줘요.
  • 저장한 뒤 불러온 게시물이 원본과 같으면 성공이에요.

힌트: json.dumpsensure_ascii=False, indent=2를 주고 pathlibwrite_text로 저장, 읽을 땐 read_text + json.loads를 쓰면 돼요.

[심화] 안전한 회원 백업 — 파일이 없거나 깨져도 죽지 않기

회원 목록을 JSON으로 백업하고 복원하는 MemberBackup 클래스를 만들어요. 단, 복원할 때 파일이 없거나 내용이 깨져 있어도 프로그램이 죽지 않게 하세요.

  • save(members)Member 목록을 JSON 파일로 저장해요(asdict 사용).
  • load() — JSON을 읽어 Member 목록으로 되살려요. 그런데 파일이 아직 없거나(FileNotFoundError), 내용이 깨진 JSON이면(json.JSONDecodeError) 빈 목록을 돌려주세요.
  • 깨진 파일에도 멈추지 않고 빈 목록으로 시작하면 성공이에요.

힌트: load의 읽기·파싱을 try로 감싸고, except (FileNotFoundError, json.JSONDecodeError)에서 return [] 하세요. 지난 시간 배운 다중 예외 잡기를 파일에 적용하는 연습이에요.


생각해볼 주제

1. 데이터를 무슨 형식으로 저장할까

오늘 JSON·CSV·TOML 세 가지를 봤어요. 게시물 목록(제목·좋아요·해시태그가 섞인 중첩 구조), 회원 통계 표(이름·팔로워 수가 칸칸이), 앱 설정(페이지 크기·광고 표시 여부) — 이 셋을 각각 어떤 형식으로 저장하면 좋을까요? 그렇게 고른 이유를 형식의 특징과 연결해 생각해 보세요.

2. 객체를 왜 dict로 번역해서 저장할까

Member 객체를 저장할 때 우리는 asdict로 dict를 거쳤어요. json에 객체를 바로 넘기면 에러가 나기 때문이죠. 그런데 왜 json은 우리 객체를 모를까요? json이 dict·list·숫자·글자만 아는 게 불편해 보일 수도 있는데, 오히려 그게 어떤 장점이 될지 생각해 보세요. (힌트: 다른 언어·다른 프로그램도 이 데이터를 읽어야 한다면?)

3. 저장하다가 프로그램이 꺼지면

"w" 모드는 파일을 열자마자 기존 내용을 통째로 지우고 새로 써요. 그런데 만약 다 쓰기 전에 프로그램이 갑자기 꺼지면 어떻게 될까요? 소중한 회원 데이터가 반쯤 쓰이다 날아갈 수도 있겠죠. 이런 사고를 막으려면 어떻게 저장하는 게 안전할지 떠올려 보세요. (힌트: 새 파일에 먼저 다 쓴 다음, 원래 파일과 바꿔치기한다면?)

✅ 예시 답안정답 보기

과제와 생각해볼 주제의 예시답안이에요. 먼저 스스로 충분히 고민한 뒤 펼쳐 보세요. 정답이 하나만 있는 건 아니니, 내 코드와 비교하며 "왜 이렇게 했을까"를 짚어보면 좋아요.

🎯 [과제 1 예시답안] 메모장 저장하고 불러오기

채점 포인트

항목 확인
with open(..., "w", encoding="utf-8")로 저장했는가
for로 한 줄씩 f.write(line + "\n") 했는가
읽을 때 line.rstrip("\n")으로 개행을 떼었는가
저장→복원이 원본 리스트와 같은가

풀이 예시

Python
# src/instagram/e2_notepad_solution.py
def save_lines(path, lines):
    """문자열 목록을 한 줄에 하나씩 파일에 저장한다."""
    with open(path, "w", encoding="utf-8") as f:
        for line in lines:
            f.write(line + "\n")


def load_lines(path):
    """파일을 읽어 줄 목록으로 돌려준다 (줄 끝 개행은 떼고)."""
    with open(path, "r", encoding="utf-8") as f:
        return [line.rstrip("\n") for line in f]
Python
save_lines("memo.txt", ["우유 사기", "사진 올리기", "운동"])
print(load_lines("memo.txt"))

실행 결과예요.

텍스트
['우유 사기', '사진 올리기', '운동']

저장할 땐 f.write가 줄바꿈을 자동으로 안 해주니 line + "\n"으로 직접 붙였어요. 읽을 땐 반대로 각 줄 끝에 붙어 온 \nrstrip("\n")으로 떼어내야 원래 글자만 깔끔하게 남아요. for line in f처럼 파일을 직접 순회하면 한 줄씩 읽어와서, readlines()를 따로 부르지 않아도 돼요.

💡 튜터의 한마디 — 저장과 읽기는 거울처럼 짝이에요. 쓸 때 \n을 붙였으면 읽을 때 떼주는 식으로, 한쪽에서 한 일을 다른 쪽에서 되돌려야 원본이 그대로 복원돼요.


🎯 [과제 2 예시답안] 게시물을 JSON으로 저장·복원

채점 포인트

항목 확인
json.dumpsensure_ascii=False로 한글을 살렸는가
indent=2로 보기 좋게 저장했는가
pathlibwrite_text/read_text를 썼는가
저장→복원이 원본 게시물 dict와 같은가

풀이 예시

Python
# src/instagram/e2_post_json_solution.py
import json
from pathlib import Path


def save_post(path, post):
    """게시물 dict 를 JSON 파일로 저장한다 (한글 보존·들여쓰기)."""
    text = json.dumps(post, ensure_ascii=False, indent=2)
    Path(path).write_text(text, encoding="utf-8")


def load_post(path):
    """JSON 파일을 읽어 게시물 dict 로 돌려준다."""
    text = Path(path).read_text(encoding="utf-8")
    return json.loads(text)
Python
post = {"title": "제주 여행", "likes": 1240, "hashtags": ["여행", "제주"]}
save_post("post.json", post)
loaded = load_post("post.json")
print(loaded)
print(f"원본과 같은가? {post == loaded}")

실행 결과예요.

텍스트
{'title': '제주 여행', 'likes': 1240, 'hashtags': ['여행', '제주']}
원본과 같은가? True

저장은 json.dumps로 dict를 글자로 바꿔 write_text로 파일에 쓰고, 복원은 read_text로 글자를 읽어 json.loads로 dict로 되돌렸어요. ensure_ascii=False 덕분에 파일을 열어보면 한글이 그대로 보이고, indent=2로 줄 맞춤까지 돼서 사람이 읽기 좋아요. likes가 숫자 1240 그대로 돌아온 점도 확인해 보세요. JSON은 숫자 타입을 보존하니까요.

💡 튜터의 한마디ensure_ascii=Falseindent=2는 한국어 데이터를 저장할 때 거의 항상 같이 챙기는 짝이에요. 저장한 JSON 파일을 직접 열어보면 내가 만든 데이터가 사람 눈에 보이는 형태로 남아 있어서 디버깅할 때도 편해요.


🎯 [과제 3 예시답안] 안전한 회원 백업 — 파일이 없거나 깨져도 죽지 않기

채점 포인트

항목 확인
save에서 asdict로 객체를 dict로 바꿔 저장했는가
load의 읽기·파싱을 try로 감쌌는가
except (FileNotFoundError, json.JSONDecodeError)로 두 경우를 함께 잡았는가
파일이 없거나 깨졌을 때 빈 목록을 돌려주는가

풀이 예시

Python
# src/instagram/e2_member_backup_solution.py
import json
from dataclasses import asdict
from pathlib import Path

from instagram.member import Member


class MemberBackup:
    def __init__(self, path):
        self._path = Path(path)

    def save(self, members):
        data = [asdict(m) for m in members]
        text = json.dumps(data, ensure_ascii=False, indent=2)
        self._path.write_text(text, encoding="utf-8")

    def load(self):
        try:
            data = json.loads(self._path.read_text(encoding="utf-8"))
        except (FileNotFoundError, json.JSONDecodeError):
            return []                       # 파일이 없거나 내용이 깨졌으면 빈 목록으로 시작
        return [Member(**d) for d in data]
Python
backup = MemberBackup("backup.json")
print(backup.load())                        # 아직 파일이 없다

backup.save([Member("minji", followers=12000), Member("jaehoon", followers=8500)])
restored = backup.load()
for m in restored:
    print(f"{m.handle} — 팔로워 {m.followers}")

실행 결과예요.

텍스트
[]
@minji — 팔로워 12000
@jaehoon — 팔로워 8500

핵심은 loadtry/except예요. 처음엔 백업 파일이 없으니 read_text에서 FileNotFoundError가 나고, 누군가 파일을 잘못 건드려 깨진 JSON이 되면 json.loads에서 json.JSONDecodeError가 나요. 두 경우를 except (A, B)로 함께 잡아서 빈 목록을 돌려주면, 어떤 상황에서도 프로그램이 죽지 않고 빈 저장소로 시작할 수 있어요. 지난 시간 배운 다중 예외 잡기를 파일 다루기에 그대로 적용한 셈이에요.

💡 튜터의 한마디 — 파일은 "있을 것이다", "내용이 멀쩡할 것이다"라고 믿으면 안 돼요. 사용자가 지웠을 수도, 편집기로 잘못 고쳤을 수도 있죠. 읽는 쪽에서 미리 대비해 두면 작은 사고로 앱 전체가 멈추는 일을 막을 수 있어요.


🤔 [생각해볼 주제 1] 데이터를 무슨 형식으로 저장할까

문제 상황 요약

JSON·CSV·TOML 세 형식을 봤는데, 게시물 목록(중첩 구조)·회원 통계 표(칸칸이)·앱 설정(사람이 손으로 고침)을 각각 어디에 저장하면 좋을지 고르는 문제예요.

튜터의 가이드 및 해설

형식마다 잘 맞는 데이터의 모양이 달라요.

  • 게시물 목록 → JSON. 게시물은 제목·좋아요·해시태그 리스트가 한 덩어리로 묶인 중첩 구조예요. JSON은 dict 안에 list, list 안에 dict처럼 겹겹이 쌓인 구조를 자연스럽게 표현하고, 숫자·True/False 타입도 보존해요. 프로그램끼리 데이터를 주고받기에도 가장 흔한 형식이고요.
  • 회원 통계 표 → CSV. 이름·팔로워 수처럼 칸이 정해진 표 데이터는 CSV가 딱이에요. 엑셀에서 바로 열어볼 수 있고, 행이 수백만 개여도 가볍게 쌓여요. 단 모든 값이 글자로 읽히니, 숫자로 쓰려면 직접 변환해야 한다는 점은 기억해야 해요.
  • 앱 설정 → TOML. 페이지 크기·광고 표시 여부 같은 설정은 사람이 직접 손으로 고치는 경우가 많아요. TOML은 key = value 형태라 사람이 읽고 쓰기 편하고, [app]·[feed]처럼 묶음으로 정리돼서 설정 파일에 잘 어울려요.

정리하면, 데이터의 모양(중첩이냐 표냐)과 누가 읽느냐(프로그램이냐 사람이냐)를 기준으로 고르면 돼요.

💡 핵심을 한마디로

"중첩 구조에 타입까지 보존하려면 JSON, 표 형식 대량 데이터는 CSV, 사람이 손으로 고치는 설정은 TOML — 데이터의 모양과 읽는 주체로 형식을 고른다."


🤔 [생각해볼 주제 2] 객체를 왜 dict로 번역해서 저장할까

문제 상황 요약

Member 객체를 저장할 때 asdict로 dict를 거쳐야 했어요. json에 객체를 바로 주면 에러가 났죠. json이 우리 객체를 모르는 게 불편해 보이는데, 오히려 그게 어떤 장점일지 생각해 보는 주제예요.

튜터의 가이드 및 해설

json이 dict·list·숫자·글자·true/false·null만 아는 건 일부러 그렇게 만든 거예요. 이 자료형들은 어느 언어에나 다 있는 가장 기본적인 것들이거든요.

만약 json이 파이썬의 Member 객체를 그대로 저장할 수 있다고 해봐요. 그럼 그 파일은 파이썬과 우리 Member 클래스를 아는 프로그램만 읽을 수 있어요. 다른 언어로 짠 프로그램이나, Member가 뭔지 모르는 다른 앱은 그 파일을 못 읽죠. 반대로 json이 기본 자료형만 고집하면, 그 파일은 자바든 자바스크립트든 어떤 언어로도 읽을 수 있는 공용 데이터가 돼요.

그래서 "객체 → dict로 번역"하는 한 단계는 번거로운 게 아니라, 내 데이터를 특정 언어에 묶이지 않은 공용 형태로 만드는 과정이에요. 우리 Member의 속사정(메서드·@property 같은 것)은 빼고, 순수한 데이터(username·followers 같은 값)만 남기는 거죠. 되살릴 땐 그 값으로 Member(**d) 하면 객체가 다시 조립되고요.

💡 핵심을 한마디로

"json이 기본 자료형만 아는 덕분에, 저장한 데이터를 어떤 언어·어떤 프로그램도 읽을 수 있다. 객체를 dict로 번역하는 건 데이터를 특정 언어에 묶이지 않게 만드는 과정이다."


🤔 [생각해볼 주제 3] 저장하다가 프로그램이 꺼지면

문제 상황 요약

"w" 모드는 파일을 열자마자 기존 내용을 통째로 지워요. 그런데 다 쓰기 전에 프로그램이 갑자기 꺼지면 데이터가 반쯤 쓰이다 날아갈 수 있죠. 안전하게 저장하려면 어떻게 할지 떠올려 보는 주제예요.

튜터의 가이드 및 해설

문제의 핵심은 "w"로 여는 순간 원본이 먼저 비워진다는 거예요. 그 직후 쓰다가 멈추면, 새 데이터도 불완전하고 원본도 이미 사라진 최악의 상황이 돼요.

실무에서 쓰는 안전한 방법은 "바꿔치기"예요. 원래 파일에 바로 쓰지 않고, 임시 파일(예: members.json.tmp)에 먼저 전부 다 써요. 다 쓰는 데 성공했으면, 그때 임시 파일을 원래 이름(members.json)으로 바꿔치기(rename)하죠. 파일 이름을 바꾸는 건 아주 빠르고 한 번에 일어나는 동작이라, 그 짧은 순간에 사고가 날 확률은 거의 없어요.

이렇게 하면 쓰는 도중 프로그램이 꺼져도 원본 members.json은 멀쩡히 남아 있어요. 망가지는 건 임시 파일뿐이고, 다음에 다시 시도하면 되죠. "다 만들어진 걸 확인한 다음에야 진짜 자리에 놓는다"는 원리예요. 지금 당장 이렇게 구현할 필요는 없지만, 소중한 데이터를 다룰 땐 이런 안전장치가 있다는 걸 기억해 두면 좋아요.

💡 핵심을 한마디로

"\"w\"는 열자마자 원본을 비운다. 임시 파일에 먼저 다 쓴 뒤 이름을 바꿔치기하면, 저장 도중 사고가 나도 원본이 살아남는다."

전체 목록 파이썬 기초