wisheasy project(2) - CSV 데이터는 DB에 어떻게 올릴까?
1편 보러가기 클릭
현재 개발 환경
이 프로젝트는 Django 기반 백엔드 서버로 구성되어 있으며, 개발과 배포 환경을 명확히 분리해 관리하고 있다.
아래는 전체적인 개발 구조와 환경 설정 개요이다.
- 사용 기술 스택
- Backend Framework : Django
- Database :
- 개발(local) 환경: SQLite (
db.sqlite3) - 운영(prod) 환경: MySQL
- 개발(local) 환경: SQLite (
- 배포 환경 : AWS EC2 (Ubuntu 기반)
- 환경 변수 관리 :
.env파일 (비공개) - 버전 관리 : Git + GitHub
- CI/CD : GitHub Actions (Ruff 자동 린트 및 포맷 적용)
- 운영 서버 실행 : Gunicorn + Nginx 조합
- 프로젝트 디렉토리 구조
project-wisheasy/ ├── apps/ # Django 앱 폴더 │ ├── accounts/ # 사용자 인증 관련 앱 │ ├── stations/ # 역 정보, 노선 데이터 관리 앱 │ ├── journeys/ # 경로 탐색 관련 앱 │ └── common/ # 공용 유틸, 상수 등 │ ├── config/ # Django 전역 설정 │ ├── settings/ │ │ ├── base.py # 공통 설정 │ │ ├── local.py # 개발 환경용 설정 (SQLite) │ │ ├── prod.py # 운영 환경용 설정 (MySQL) │ │ └── __init__.py │ ├── urls.py │ ├── wsgi.py │ └── asgi.py │ ├── data_create/ # 기타 정적 CSV/DB 초기 데이터 ├── manage.py # Django 실행 스크립트 ├── .env # 환경 변수 (비공개) ├── .gitignore └── requirements.txt # Python 패키지 목록
서버의 DB는 비어있다
“로컬 DB(db.sqlite3)는 .gitignore로 제외했는데, 배포하면 DB가 비어 있는 거 아닌가?”
=> 맞음!
db.sqlite3는 개발용으로만 사용하는 SQLite 데이터베이스라 GitHub에 올리지 않는다.
환경마다 파일 경로나 내용이 다르고, 개인 테스트 데이터가 들어 있을 수 있기 때문이다.
그래서 대부분의 Django 프로젝트에서는 .gitignore에 db.sqlite3를 등록하고, Git에는 코드와 설정만 관리한다.
배포 서버에서는 보통 MySQL이나 PostgreSQL 같은 운영용 데이터베이스를 사용하고,
이 DB 연결 정보는 .env 파일과 config/settings/prod.py에 설정한다.
해당 프로젝트는 서버용으로로 MySQL, 로컬용으로 sqlite를 사용한다.
# config/settings/prod.py
from .base import *
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"NAME": env.str("MYSQL_DB"),
"USER": env.str("MYSQL_USER"),
"PASSWORD": env.str("MYSQL_PASSWORD"),
"HOST": env.str("MYSQL_HOST"),
"PORT": env.int("MYSQL_PORT", default=3306),
"OPTIONS": {"charset": "utf8mb4"},
}
}
그럼 고정적인 csv,xlxs 같은 파일은 어떻게 DB에 넣지?
배포 서버는 처음엔 비어 있는 상태이기 때문에 한 번은 반드시 CSV 데이터를 읽어서 DB에 적재해야 한다.
이 방법에는 두 가지가 있다.
다음과 같은 예시로 설명하겠다.
-
수동 실행 (가장 많이 사용되는 방식) => 서버에 올리기 위해서 이 과정을 할 것
배포 후 서버에서 다음 명령을 한 번 실행한다.
python manage.py makemigrations --settings=config.settings.prod python manage.py migrate --settings=config.settings.prod python manage.py load_csv --settings=config.settings.prod이렇게 하면 MySQL에 CSV 데이터가 삽입되고, 이후에는 다시 실행하지 않아도 된다.
-
자동 실행 (앱 시작 시 자동 로딩)
apps/stations/apps.py의 ready() 메서드에 아래 로직을 추가하면 앱이 처음 구동될 때 DB가 비어 있을 경우 자동으로 CSV 데이터를 넣는다.
from django.apps import AppConfig import csv from pathlib import Path from django.db.utils import OperationalError class StationsConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'apps.stations' def ready(self): from apps.stations.models import Station csv_path = Path(__file__).resolve().parent / 'data' / 'stations.csv' try: if Station.objects.count() == 0: with open(csv_path, newline='', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: Station.objects.create(**row) except OperationalError: pass이 방식은 서버가 처음 실행될 때 자동으로 데이터를 채워주지만, 마이그레이션이 끝난 뒤에 실행돼야 하므로 주의가 필요하다.
| 방식 | 추천 환경 | 이유 |
|---|---|---|
| (1)수동 실행 (명시적 로딩) | 운영 서버 | 한번만 실행하면 끝, 안전함 |
(2)자동 실행 (ready()) |
개발 환경 | 초기화 자동화에 편리 |
CSV 파일은 Git에 올리기
CSV 파일은 고정된 공통 데이터이기 때문에 Git에 포함시켜도 문제 없다. 오히려 운영 서버와 로컬에서 동일한 데이터를 보장하는 데 도움이 된다.
Django로 csv를 DB에 적재하는 전체 과정 정리
1. 폴더 구조 구축
apps/
└── journeys/
├── management/
│ └── commands/
│ └── load_csv.py ← csv를 DB에 업로드하는 파일
├── models.py
├── views.py
├── ...
2. load_csv.py 작성
import csv
import os
from django.core.management.base import BaseCommand, CommandError
from django.conf import settings
from apps.journeys.models import Lines, Edges, Nodes
class Command(BaseCommand):
help = "static/data 폴더의 CSV 파일(line2_7_edges.csv, line2_7_lines.csv, line2_7_nodes.csv)을 DB에 로드"
def add_arguments(self, parser):
parser.add_argument(
'--file',
type=str,
help='불러올 CSV 파일 이름 (예: line2_7_lines.csv)',
)
def handle(self, *args, **options):
csv_file = options['file']
if not csv_file:
raise CommandError("CSV 파일 이름을 '--file' 옵션으로 지정해야 합니다. 예: python manage.py load_csv --file line2_7_lines.csv")
# static/data 폴더 기준 경로 설정
csv_path = os.path.join(settings.BASE_DIR, 'static', 'data', csv_file)
if not os.path.exists(csv_path):
raise CommandError(f"파일을 찾을 수 없습니다: {csv_path}")
self.stdout.write(f"📂 '{csv_file}' 불러오는 중...")
with open(csv_path, newline='', encoding='utf-8-sig') as csvfile:
reader = csv.DictReader(csvfile)
# 파일 이름으로 분기
if 'lines' in csv_file:
self.load_lines(reader)
elif 'edges' in csv_file:
self.load_edges(reader)
elif 'nodes' in csv_file:
self.load_nodes(reader)
else:
raise CommandError(f"⚠️ '{csv_file}'은(는) 인식되지 않는 파일입니다.")
# --- Lines ---
def load_lines(self, reader):
count_new = 0
count_update = 0
for row in reader:
obj, created = Lines.objects.update_or_create(
line=row["line"],
order_in_line=row["order_in_line"],
defaults={
"station": row["station"],
},
)
if created:
count_new += 1
else:
count_update += 1
self.stdout.write(
self.style.SUCCESS(f"✅ line2_7_lines.csv 업로드 완료: {count_new}개 추가, {count_update}개 업데이트됨")
)
# --- Edges ---
def load_edges(self, reader):
count_new = 0
count_update = 0
for row in reader:
obj, created = Edges.objects.update_or_create(
edge_key=row["edge_key"], # PK 기준으로 찾음
defaults={
"relation": row["relation"],
"escalator": row.get("escalator", 0),
"out_of_order": row.get("out_of_order", 0),
"is_escalator": row.get("is_escalator") or None,
"source": row["source"],
"target": row["target"],
},
)
if created:
count_new += 1
else:
count_update += 1
self.stdout.write(
self.style.SUCCESS(f"✅ edges.csv 업로드 완료: {count_new}개 추가, {count_update}개 업데이트됨")
)
# --- Nodes ---
def load_nodes(self, reader):
count_new = 0
count_update = 0
def parse_str(value):
return value.strip() if value else None
for row in reader:
obj, created = Nodes.objects.update_or_create(
node_id=row["node_id"], # PK 기준으로 중복 체크
defaults={
"line": parse_str(row.get("line")),
"node_name": parse_str(row.get("node_name")),
"floor": parse_str(row.get("floor")),
"type": parse_str(row.get("type")),
"station": parse_str(row.get("station")),
},
)
if created:
count_new += 1
else:
count_update += 1
self.stdout.write(
self.style.SUCCESS(
f"✅ nodes.csv 업로드 완료: {count_new}개 추가, {count_update}개 업데이트됨"
)
)
3. 로컬에 csv파일 업로드 (makemigrations , migrate 전제 조건)
python manage.py load_csv --file line2_7_edges.csv --settings=config.settings.local
python manage.py load_csv --file line2_7_stations.csv --settings=config.settings.local
python manage.py load_csv --file line2_7_lines.csv --settings=config.settings.local
근데, 이떄 두가지 에러가 발생했다.
첫번째는 primary-key error,
두번째는 type error
해결 과정은 따로 log 포스팅 업로드하였다.
해결 완료!
그럼 이제 내 로컬 sqlite에 csv파일이 잘 들어간 것을 확인할 수 있다.

.github/workflows/cicd_web-db-nginx.yml 파일을 보면 makemigrations를 하면 자동으로 MySQL에 반영하게 되어있다.
4. 커밋메세지 작성하고 dev에 push 하기
5. 서버 MySQL에도 CSV 파일 올려주기
python manage.py load_csv --file line2_7_edges.csv --settings=config.settings.prod
python manage.py load_csv --file line2_7_nodes.csv --settings=config.settings.prod
python manage.py load_csv --file line2_7_lines.csv --settings=config.settings.prod
그런데, 이 코드를 실행하면
django.db.utils.OperationalError: (2006, "Can't connect to MySQL server on ...)
이런 오류가 발생한다.
EC2 보안그룹에서 3306 포트가 닫혀 있어서 올릴 수 없다.
컨테이너 관리자가 위의 3줄 코드를 한번 실행해주면 잘 올라갈 것임.
끝 !