결론: 형식에 맞는 표준 API와 pathlib로 안전하게 읽고 쓴다
분석 코드는 원본 데이터를 파일에서 읽어 오고 중간 결과를 다시 파일로 저장하는 일을 끊임없이 반복한다. 이때 파일 형식마다 저장 구조와 강점이 달라서 하나의 방법으로 통일할 수 없다. CSV는 사람이 읽기 쉽고 엑셀·스프레드시트와 호환되지만 모든 값이 문자열로 저장된다. JSON은 중첩된 객체와 리스트를 그대로 표현하지만 대용량에서는 파싱 비용이 크다. Parquet은 컬럼 단위로 저장하는 이진 포맷이라 대용량 분석에서 필요한 열만 빠르게 읽지만 사람이 직접 읽을 수 없고 별도 라이브러리가 필요하다. 경로는 운영체제 차이를 흡수하는 pathlib.Path로 다루고, 파일을 열 때는 항상 with 문으로 자동으로 닫는다.
pathlib로 경로 다루기
문자열을 슬래시로 이어 붙여 경로를 만들면 운영체제마다 구분자가 달라 오류가 난다. pathlib.Path는 경로를 객체로 다루고 / 연산자로 하위 경로를 안전하게 이어 붙인다. 표준 라이브러리 경로 처리는 Python 공식 문서의 pathlib을 참고한다 (2026-08-10 확인).
from pathlib import Path
data_dir = Path("data")
data_dir.mkdir(parents=True, exist_ok=True) # 이미 있으면 조용히 통과
csv_path = data_dir / "sales.csv"
print(csv_path)
print(csv_path.exists())
print(csv_path.suffix, csv_path.stem)
for path in data_dir.glob("*.csv"):
print("found:", path)
data/sales.csv
False
.csv sales
mkdir(parents=True, exist_ok=True)는 상위 폴더가 없어도 만들고, 폴더가 이미 있어도 예외를 던지지 않는다. glob은 패턴에 맞는 경로를 지연 평가로 순회한다. 아직 파일을 만들지 않았으므로 exists()는 False를 반환한다.
CSV 파일 읽고 쓰기
CSV(Comma-Separated Values)는 값을 쉼표로 구분한 줄 단위 텍스트 파일이다. 구조가 단순해 엑셀·스프레드시트·데이터베이스 내보내기에 널리 쓰이지만, 모든 값이 문자열로 저장되므로 숫자·불리언은 읽은 뒤 직접 변환해야 한다. 표준 라이브러리 csv 모듈은 따옴표로 감싼 필드 안의 쉼표·줄바꿈까지 규칙대로 해석한다.
csv.reader와 csv.writer
import csv
with open("data/sales.csv", encoding="utf-8", newline="") as f:
reader = csv.reader(f)
header = next(reader)
rows = [row for row in reader]
print(header)
print(rows)
['id', 'product', 'amount', 'region']
[['1', '노트북', '1250000', '서울'], ['2', '키보드', '89000', '부산'], ['3', '모니터', '310000', '서울']]
csv.reader는 한 줄을 문자열 리스트로 돌려준다. amount 열도 문자열 '1250000'이므로 합계를 구하려면 int()나 float()로 직접 변환해야 한다.
파일을 열 때
newline=""를 지정하지 않으면 따옴표로 감싼 필드 안의 줄바꿈이 잘못 해석되고, 줄바꿈이 \r\n인 환경에서 쓸 때 빈 줄이 추가로 끼어들 수 있다. csv 모듈이 줄바꿈 처리를 직접 담당하므로 항상 newline=""로 여는 것이 안전하다.
import csv
rows = [
{"id": 4, "product": "마우스", "amount": 45000, "region": "인천"},
]
with open("data/sales.csv", "a", encoding="utf-8", newline="") as f:
writer = csv.writer(f)
for row in rows:
writer.writerow(row.values())
csv.DictReader와 csv.DictWriter
행마다 열 이름을 계속 기억하며 인덱스로 접근하면 열 순서가 바뀔 때 코드가 조용히 깨진다. csv.DictReader는 첫 줄을 헤더로 읽고 각 행을 헤더 이름을 키로 하는 딕셔너리로 돌려준다.
import csv
with open("data/sales.csv", encoding="utf-8", newline="") as f:
reader = csv.DictReader(f)
total = sum(int(row["amount"]) for row in reader)
print(total)
fieldnames = ["id", "product", "amount", "region"]
with open("data/sales_seoul.csv", "w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerow({"id": 1, "product": "노트북", "amount": 1250000, "region": "서울"})
1694000
DictWriter는 fieldnames를 반드시 지정해야 하고, writeheader()를 먼저 호출해야 헤더 줄이 생긴다. 딕셔너리에 없는 키가 있으면 기본적으로 ValueError가 나므로 열이 고정된 데이터에 적합하다. csv 모듈의 전체 옵션은 Python 공식 문서의 csv를 참고한다 (2026-08-10 확인).
JSON 파일 읽고 쓰기
JSON(JavaScript Object Notation)은 문자열·숫자·불리언·null·배열·객체를 그대로 표현하는 경량 데이터 포맷이며, Python의 dict·list와 구조가 거의 같다. API 응답, 설정 파일, 중첩된 구조의 중간 결과를 저장할 때 CSV보다 적합하다. json 모듈은 파일과 직접 주고받는 load/dump와 문자열과 주고받는 loads/dumps를 제공한다.
import json
records = [
{"id": 1, "product": "노트북", "amount": 1250000, "region": "서울"},
{"id": 2, "product": "키보드", "amount": 89000, "region": "부산"},
]
with open("data/sales.json", "w", encoding="utf-8") as f:
json.dump(records, f, ensure_ascii=False, indent=2)
with open("data/sales.json", encoding="utf-8") as f:
loaded = json.load(f)
print(loaded == records)
print(loaded[0]["amount"], type(loaded[0]["amount"]))
True
1250000 <class 'int'>
CSV와 달리 JSON은 숫자·불리언·null 타입을 저장 시점 그대로 복원한다. 그래서 amount가 문자열이 아니라 int로 그대로 돌아온다. 기본값 ensure_ascii=True는 비-ASCII 문자를 \uXXXX 이스케이프로 바꾸므로, 한글을 사람이 읽을 수 있게 저장하려면 ensure_ascii=False를 지정한다. indent는 들여쓰기 폭을 지정해 사람이 읽기 좋게 만들지만 파일 크기는 커진다.
datetime, set, 사용자 정의 객체처럼 JSON 기본 타입이 아닌 값을 그대로 dump하면
TypeError: Object of type ... is not JSON serializable가 발생한다. 저장 전에 문자열·숫자 등 JSON이 아는 타입으로 직접 변환하거나json.dump(obj, f, default=변환함수)로 변환 방법을 알려준다.
json 모듈의 옵션과 타입 대응표는 Python 공식 문서의 json에서 확인할 수 있다 (2026-08-10 확인).
Parquet 파일 읽고 쓰기
Parquet은 데이터를 행이 아니라 열(컬럼) 단위로 묶어 저장하는 이진 포맷이다. 열마다 값의 자료형이 같아 압축률이 높고, 분석에 필요한 몇 개 열만 골라 읽을 때 전체 행을 다 읽지 않아도 된다. 표준 라이브러리에는 Parquet 지원이 없어 pandas와 함께 pyarrow 또는 fastparquet 같은 엔진 패키지를 설치해야 한다.
pip install pandas pyarrow
import pandas as pd
df = pd.DataFrame(records) # 위에서 만든 records 리스트
df.to_parquet("data/sales.parquet", index=False)
# 필요한 열만 골라 읽기
df_amounts = pd.read_parquet("data/sales.parquet", columns=["region", "amount"])
print(df_amounts)
region amount
0 서울 1250000
1 부산 89000
to_parquet과 read_parquet의 engine 매개변수는 기본값이 "auto"라 pyarrow가 설치돼 있으면 자동으로 사용하고, 없으면 fastparquet로 넘어간다. 둘 다 없으면 가져오기 오류가 나므로 둘 중 하나를 반드시 설치한다. columns 인자로 열을 지정하면 디스크에서 그 열만 읽어 들여 대용량 파일에서 특히 유리하다. 정확한 기본값과 엔진 동작은 pandas 공식 문서의 read_parquet에서 확인할 수 있다 (2026-08-10 확인).
실습: 같은 데이터를 세 형식으로 저장하고 비교하기
CSV로 저장된 원본을 읽어 세 형식으로 나란히 저장하고 파일 크기를 비교한다.
import csv
import json
from pathlib import Path
import pandas as pd
data_dir = Path("data")
data_dir.mkdir(exist_ok=True)
# 1) CSV 읽기: DictReader로 헤더를 키로 사용하고 값을 직접 변환
with open(data_dir / "sales.csv", encoding="utf-8", newline="") as f:
reader = csv.DictReader(f)
rows = [
{
"id": int(row["id"]),
"product": row["product"],
"amount": int(row["amount"]),
"region": row["region"],
}
for row in reader
]
# 2) JSON으로 저장
with open(data_dir / "sales.json", "w", encoding="utf-8") as f:
json.dump(rows, f, ensure_ascii=False, indent=2)
# 3) Parquet으로 저장
df = pd.DataFrame(rows)
df.to_parquet(data_dir / "sales.parquet", index=False)
# 4) 세 파일 크기 비교
for name in ("sales.csv", "sales.json", "sales.parquet"):
path = data_dir / name
print(name, path.stat().st_size, "bytes")
sales.csv 107 bytes
sales.json 287 bytes
sales.parquet 2817 bytes
행이 3개뿐인 예제에서는 오히려 Parquet 파일이 가장 크다. Parquet은 스키마·통계·압축 사전 같은 메타데이터를 파일마다 포함하므로 작은 데이터에서는 그 고정 비용이 두드러진다. 데이터가 수십만~수백만 행으로 늘어나면 컬럼 단위 압축과 열 선택 읽기의 이득이 고정 비용을 훨씬 앞질러, 실무에서는 대체로 CSV보다 작고 빠르게 읽힌다. 파일 형식은 데이터 규모와 읽기 패턴을 함께 고려해 고른다.
형식 비교
항목 | CSV | JSON | Parquet |
|---|---|---|---|
구조 | 쉼표로 구분한 표 | 중첩 가능한 객체·배열 | 컬럼 단위 이진 저장 |
사람이 읽기 | 쉬움 | 쉬움 | 불가능(전용 도구 필요) |
타입 보존 | 없음(전부 문자열) | 기본 타입 보존 | 스키마와 열 타입 보존 |
압축 | 없음(별도 gzip 필요) | 없음(별도 gzip 필요) | 기본 내장(예: snappy) |
필요 패키지 | 표준 라이브러리 | 표준 라이브러리 | pandas + pyarrow/fastparquet |
강점 | 범용성, 엑셀 호환 | 중첩 구조, API 교환 | 대용량 컬럼 선택 읽기·압축 |
자주 발생하는 문제
newline=""를 빠뜨리면 Windows에서 줄마다 빈 줄이 끼거나, 따옴표 안에 줄바꿈이 있는 필드가 잘못 나뉜다.CSV에서 읽은 값은 전부 문자열이라 합계나 비교를 하기 전에 반드시
int()·float()로 변환해야 한다. 변환을 잊으면"10" + "20"이"1020"이 되는 식으로 조용히 틀린 값이 나온다.encoding을 지정하지 않으면 운영체제 기본 인코딩을 따르므로, 같은 코드가 macOS에서는 되고 Windows에서는 한글이 깨질 수 있다. 항상
encoding="utf-8"을 명시한다.json.dump에 datetime·set처럼 JSON이 모르는 타입을 그대로 넘기면TypeError가 난다. 저장 전에isoformat()같은 변환을 직접 하거나default인자를 쓴다.pyarrow와 fastparquet를 둘 다 설치하지 않은 환경에서
read_parquet/to_parquet을 호출하면 가져오기 오류가 난다.pip install pyarrow로 해결한다.
다음 글: 데코레이터와 클로저를 실무에 적용하기
다음 글에서는 클로저가 외부 변수를 어떻게 붙잡아 두는지부터 시작해, 함수를 감싸 공통 기능을 추가하는 데코레이터 패턴과 functools.wraps로 원본 함수의 메타데이터를 보존하는 방법을 다룬다.
참고 자료
Python 공식 문서: pathlib — 객체 지향 경로 처리와
PathAPI (2026-08-10 확인)Python 공식 문서: csv — reader·writer·DictReader·DictWriter와 dialect (2026-08-10 확인)
Python 공식 문서: json — load·dump와 Python-JSON 타입 대응표 (2026-08-10 확인)
pandas 공식 문서: read_parquet — engine 선택과 columns 인자 (2026-08-10 확인)
Apache Parquet 공식 문서 — 컬럼형 저장 포맷의 설계 목표 (2026-08-10 확인)
댓글 0
댓글을 불러오는 중…