지식
Python
sqlite3: 내장 관계형 데이터베이스
Python sqlite3 모듈 사용법을 정리합니다. Connection과 Cursor, execute/executemany, row_factory, 트랜잭션 관리, SQL 인젝션 방지, 메모리 DB 활용, 커스텀 타입 어댑터까지 다룹니다.
지난 글에서 pickle로 Python 객체를 직렬화하는 방법을 살펴봤습니다. 이번 글에서는 Python에 내장된 관계형 데이터베이스 sqlite3를 다룹니다. 별도 서버 없이 파일 하나로 완전한 SQL 데이터베이스를 쓸 수 있어서, 소규모 앱, 로컬 캐시, 테스트 환경에서 매우 유용합니다.
기본 연결과 테이블 생성
import sqlite3
# 파일 DB — 없으면 새로 생성
conn = sqlite3.connect('app.db')
# 메모리 DB — 프로세스 종료 시 사라짐 (테스트에 적합)
conn = sqlite3.connect(':memory:')
# 테이블 생성
conn.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER,
email TEXT UNIQUE
)
""")
conn.commit()
conn.close()
with 문으로 트랜잭션 관리
with sqlite3.connect(...) 패턴을 사용하면 블록이 정상 종료될 때 자동으로 commit(), 예외 발생 시 rollback()을 수행합니다.
import sqlite3
with sqlite3.connect('app.db') as conn:
conn.execute(
"INSERT INTO users (name, age) VALUES (?, ?)",
('Alice', 30)
)
conn.execute(
"INSERT INTO users (name, age) VALUES (?, ?)",
('Bob', 25)
)
# 블록 종료 시 자동 commit
SQL 인젝션 방지: 파라미터 바인딩
절대로 f-string이나 문자열 포매팅으로 SQL을 만들지 마세요.
# 절대 금지 — SQL 인젝션 취약
name = user_input
conn.execute(f"SELECT * FROM users WHERE name = '{name}'")
# name = "'; DROP TABLE users; --" 이면 테이블 삭제됨!
# 올바른 방법 1: ? 플레이스홀더
conn.execute("SELECT * FROM users WHERE name = ?", (name,))
# 올바른 방법 2: :name 형식 (가독성 높음)
conn.execute(
"SELECT * FROM users WHERE name = :name AND age > :age",
{'name': name, 'age': 18}
)
데이터 조회
import sqlite3
with sqlite3.connect('app.db') as conn:
# 커서로 조회
cursor = conn.execute("SELECT * FROM users WHERE age > ?", (20,))
# 한 행씩
row = cursor.fetchone() # 첫 번째 행 또는 None
# 전체
rows = cursor.fetchall() # [(1, 'Alice', 30), ...]
# 이터레이터로 (메모리 효율)
for row in conn.execute("SELECT * FROM users"):
print(row)
row_factory로 dict처럼 접근
기본은 튜플로 반환되어 컬럼 이름 대신 인덱스를 써야 합니다. sqlite3.Row를 row_factory로 설정하면 컬럼 이름으로 접근 가능합니다.
import sqlite3
with sqlite3.connect('app.db') as conn:
conn.row_factory = sqlite3.Row # 핵심 설정
rows = conn.execute("SELECT id, name, age FROM users").fetchall()
for row in rows:
print(row['name'], row['age']) # 컬럼명으로 접근
print(dict(row)) # 딕셔너리로 변환
executemany — 배치 삽입
import sqlite3
users = [
('Charlie', 28),
('Diana', 32),
('Eve', 27),
]
with sqlite3.connect('app.db') as conn:
conn.executemany(
"INSERT INTO users (name, age) VALUES (?, ?)",
users
)
# conn.commit()은 with 블록에서 자동 처리
executemany()는 단건 execute()를 루프로 반복하는 것보다 훨씬 빠릅니다.
커스텀 타입 어댑터와 변환기
Python datetime처럼 SQLite가 지원하지 않는 타입을 저장할 때 씁니다.
import sqlite3
import datetime
# 어댑터: Python → SQLite (저장 시)
sqlite3.register_adapter(datetime.date, lambda d: d.isoformat())
# 변환기: SQLite → Python (읽을 때)
sqlite3.register_converter(
'DATE',
lambda s: datetime.date.fromisoformat(s.decode())
)
with sqlite3.connect('app.db', detect_types=sqlite3.PARSE_DECLTYPES) as conn:
conn.execute("CREATE TABLE events (name TEXT, date DATE)")
conn.execute("INSERT INTO events VALUES (?, ?)", ('Launch', datetime.date(2026, 5, 21)))
row = conn.execute("SELECT * FROM events").fetchone()
print(type(row[1])) # <class 'datetime.date'>
트랜잭션 격리 수준
# isolation_level=None → autocommit 모드 (트랜잭션 없음)
conn = sqlite3.connect('app.db', isolation_level=None)
# isolation_level='DEFERRED' (기본)
# isolation_level='IMMEDIATE'
# isolation_level='EXCLUSIVE'
실전: 간단한 캐시 저장소
import sqlite3
import json
from datetime import datetime
class LocalCache:
def __init__(self, path='cache.db'):
self.conn = sqlite3.connect(path, check_same_thread=False)
self.conn.row_factory = sqlite3.Row
self.conn.execute("""
CREATE TABLE IF NOT EXISTS cache (
key TEXT PRIMARY KEY,
value TEXT,
updated_at TEXT
)
""")
def set(self, key, value):
with self.conn:
self.conn.execute(
"INSERT OR REPLACE INTO cache VALUES (?, ?, ?)",
(key, json.dumps(value), datetime.now().isoformat())
)
def get(self, key):
row = self.conn.execute(
"SELECT value FROM cache WHERE key = ?", (key,)
).fetchone()
return json.loads(row['value']) if row else None
INSERT OR REPLACE는 key가 이미 있으면 업데이트, 없으면 삽입합니다.
지난 글: pickle: Python 객체 직렬화
다음 글: uuid: 유일 식별자 생성
읽어주셔서 감사합니다. 😊