
들어가며
Python 개발자라면 sqlite3 모듈을 한 번쯤은 사용해봤을 것이다. 별도의 서버 없이 파일 하나로 데이터베이스를 사용할 수 있어 가볍고 편리하다. 하지만 이 편리함 때문에, 가독성이 떨어지거나 성능·보안 면에서 아쉬운 코드가 만들어지기 쉽다.
아직도 조회 결과를 row[0], row[1]처럼 다루고 있거나, 데이터 양과 상관없이 항상 fetchall()을 쓰고 있지는 않은가? 또는 f-string으로 SQL을 만들고 있지는 않은가?
이 글에서는 sqlite3를 사용할 때 꼭 알아야 할 4가지 핵심 팁을 정리한다. 이 습관들만 바꿔도 코드는 훨씬 읽기 쉬워지고, 안정적이며, 안전해진다.
1. row[2] 대신 row['close']
sqlite3.Row로 가독성 높이기
조회 결과를 숫자 인덱스로 접근하는 방식은 흔하다. 하지만 이 방식은 코드만 봐서는 값의 의미를 알기 어렵다.
price = row[2] # 이게 종가인지 거래량인지 바로 알 수 없다
이 문제는 한 줄로 해결할 수 있다.
conn.row_factory = sqlite3.Row
이 설정을 하면 조회 결과가 sqlite3.Row 객체로 반환된다.
컬럼 이름으로 접근할 수 있고, 기존처럼 인덱스로 접근하는 것도 가능하다.
비교 예제
# 기본 설정 (튜플)
conn.row_factory = None
cursor.execute("SELECT stk_cd, date, close FROM ohlcv LIMIT 1")
row = cursor.fetchone()
print(row[0]) # '005930'
print(row[2]) # 73500
# Row 객체 사용
conn.row_factory = sqlite3.Row
cursor.execute("SELECT stk_cd, date, close FROM ohlcv LIMIT 1")
row = cursor.fetchone()
print(row["stk_cd"]) # '005930'
print(row["close"]) # 73500
컬럼 순서가 바뀌어도 코드 수정이 필요 없다.
sqlite3를 쓴다면 기본 습관으로 가져가는 것이 좋다.
2. 무심코 쓴 fetchall()은 메모리 폭탄이 될 수 있다
sqlite3에서 데이터를 가져오는 메서드는 세 가지다. 메서드동작 방식메모리 사용추천 상황
| 메서드 | 동작 방식 | 메모리 사용 | 추천 상황 |
| fetchall() | 전체 결과를 한 번에 로드 | 큼 | 소량 데이터 |
| fetchone() | 한 행씩 처리 | 매우 적음 | 대용량 데이터 |
| fetchmany(n) | n개씩 묶음 처리 | 조절 가능 | 배치 처리 |
fetchall()은 결과 전체를 리스트로 메모리에 올린다.
데이터가 많으면 **MemoryError(메모리 부족 오류)**가 발생할 수 있다.
실무 예제: 로그 테이블 처리
# ❌ 위험한 방식 (로그 100만 건)
rows = cursor.execute("SELECT * FROM logs").fetchall()
for row in rows:
process(row)
# ✅ 안전한 방식
cursor.execute("SELECT * FROM logs")
while True:
row = cursor.fetchone()
if row is None:
break
process(row)
선택 기준 요약
데이터 수천 건 이하 → fetchall()
데이터 수만 건 이상 → fetchone()
페이지/배치 단위 처리 → fetchmany()
3. f-string으로 SQL 작성은 보안 사고로 이어진다
SQL에 사용자 입력값을 직접 문자열로 넣으면 안 된다. 이는 SQL 인젝션 공격에 매우 취약하다.
잘못된 예
cursor.execute(
f"SELECT * FROM students WHERE name = '{name}'"
)
올바른 예 (파라미터 바인딩)
cursor.execute(
"SELECT * FROM students WHERE name = ?",
(name,)
)
이 방식에서는 SQL 문과 데이터가 분리된다. 입력값은 코드로 실행되지 않고 데이터로만 처리된다. sqlite3에서는 ? 또는 :name 방식 모두 사용할 수 있다.
문자열 포맷으로 SQL을 만드는 습관은 반드시 버려야 한다.
4. INSERT 했는데 데이터가 없다면 commit()을 확인하자
sqlite3는 트랜잭션 기반으로 동작한다.
INSERT, UPDATE, DELETE는 즉시 파일에 저장되지 않는다.
cursor.execute("INSERT INTO stocks VALUES (?, ?)", ("GOOG", 50))
# commit을 호출하지 않으면 저장되지 않는다
반드시 commit()을 호출해야 한다.
try:
cursor.execute(
"INSERT INTO stocks VALUES (?, ?)",
("GOOG", 50)
)
conn.commit()
except Exception:
conn.rollback()
더 나은 방법: with 구문 사용
import sqlite3
with sqlite3.connect("data.db") as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute(
"INSERT INTO stocks VALUES (?, ?)",
("AAPL", 120)
)
- 정상 종료 → 자동 commit
- 예외 발생 → 자동 rollback
실무에서는 이 방식이 가장 안전하다.
나가며
sqlite3는 가볍지만, 사용 습관은 대형 데이터베이스와 동일하게 가져가야 한다.
- sqlite3.Row로 가독성을 높이고
- 데이터 크기에 맞게 fetch 방식을 선택하고
- SQL은 반드시 파라미터 바인딩을 사용하고
- 데이터 변경 후에는 commit을 확실히 하자
이 네 가지만 지켜도 sqlite3 코드는 훨씬 견고해진다.
'잡(job)기술 > 파이썬 공부' 카테고리의 다른 글
| 이제야 알게 된 Python else의 힘 (1) | 2026.01.12 |
|---|---|
| 내 코드가 외부 API와 통신한 척하게 만드는 방법 - 파이썬 Mocking 핵심 개념 2가지 (3) | 2026.01.02 |
| 당신의 테스트는 안녕한가요? Pytest로 데이터베이스를 제대로 검증하는 3가지 핵심 원리 (0) | 2026.01.02 |
| Poetry로 Python 프로젝트 환경 만들기 (2) | 2025.06.24 |
| pyproject.toml 시작하기: Python 프로젝트의 현대적 설정 파일 (2) | 2025.06.24 |