6부 · 안전하고 빠르게 (1) 트랜잭션과 무결성
← 5부 집계와 GROUP BY · 목차 · 다음: 인덱스와 뷰 →
질의는 충분히 배웠습니다. 이제 데이터를 안전하게 바꾸는 법으로 시야를 넓힙니다. 현실의 작업은 종종 여러 단계로 이뤄집니다 — 주문이 들어오면 "주문을 기록하고 + 재고를 줄인다"가 한 묶음이죠. 둘 중 하나만 되고 멈추면 데이터가 어긋납니다. 이 "전부 되거나, 전부 안 되거나"를 보장하는 것이 트랜잭션(transaction) 입니다.
flowchart LR
C[행 추가·수정·삭제] -.규칙·안전.-> I["안전한 변경<br/>트랜잭션 · 무결성"]
classDef here fill:#fdf0d5,stroke:#e67e22,color:#000,stroke-width:3px
classDef dim fill:#eee,stroke:#bbb,color:#888
class I here
class C dim
6.1 왜 트랜잭션이 필요한가
서점에서 책 한 권이 주문되는 상황을 봅시다. 두 가지 변경이 일어나야 합니다.
orders에 주문 한 줄 추가books의 해당 책 재고 1 감소
INSERT INTO orders (customer_id, book_id, quantity) VALUES (1, 2, 1); UPDATE books SET stock = stock - 1 WHERE id = 2;
문제는 1번과 2번 사이에 무언가 잘못될 수 있다는 겁니다. 프로그램이 죽거나, 전원이 나가거나, 두 번째 줄에 오류가 나면? 주문은 기록됐는데 재고는 안 줄어든 어긋난 상태가 남습니다. 반대 순서면 "재고는 줄었는데 주문 기록이 없는" 유령 차감이 생기죠.
flowchart TB
s["주문 처리 시작"] --> a["① 주문 기록 ✅"]
a --> crash["💥 여기서 중단!"]
crash --> bad["주문은 있는데 재고는 그대로<br/>→ 데이터 불일치"]
classDef bad fill:#f5c6cb,stroke:#c0392b,color:#000
class crash,bad bad
트랜잭션은 이 두 단계를 하나의 단위로 묶어, "둘 다 성공하면 확정(commit), 하나라도 실패하면 전부 취소(rollback)"를 보장합니다. 중간에 끊겨도 어긋난 상태가 남지 않습니다.
6.2 BEGIN · COMMIT · ROLLBACK
트랜잭션은 세 명령으로 다룹니다.
BEGIN— "지금부터 한 묶음 시작"COMMIT— "여기까지 전부 확정해서 디스크에 영구 반영"ROLLBACK— "처음(BEGIN)으로 전부 되돌리기"
BEGIN; INSERT INTO orders (customer_id, book_id, quantity) VALUES (1, 2, 1); UPDATE books SET stock = stock - 1 WHERE id = 2; COMMIT; -- 두 변경이 함께 확정된다
만약 중간에 문제를 발견하면 COMMIT 대신 ROLLBACK을 부릅니다. 그러면 BEGIN 이후의 모든 변경이 없던 일이 됩니다.
BEGIN; UPDATE books SET stock = stock - 1 WHERE id = 2; -- 어? 재고가 부족하네. 취소하자. ROLLBACK; -- 재고가 원래대로 복구됨
실제로 이게 동작하는지 파이썬으로 확인해 보면(재고 10에서 1 빼다가 오류 → 롤백):
데미안 stock before: 10 롤백됨: 주문 처리 중 오류 발생! 데미안 stock after rollback: 10 ← 되돌아왔다
자동 커밋(autocommit).BEGIN을 명시하지 않고UPDATE한 줄만 실행하면, SQLite는 그 한 문장을 즉시 자동으로 커밋합니다(자동 커밋 모드). 그래서 1부~5부에서BEGIN없이도 변경이 저장됐던 겁니다. 여러 문장을 한 묶음으로 묶고 싶을 때만BEGIN을 씁니다. 파이썬sqlite3는 조금 다르게 동작하는데, 7부에서 다룹니다.
6.3 ACID: 트랜잭션이 약속하는 네 가지
트랜잭션이 보장하는 성질을 네 글자 머리글자 ACID로 부릅니다. 외우기보다 "데이터베이스가 이런 걸 지켜 준다"는 감각으로 읽으세요.
| 글자 | 이름 | 뜻 |
|---|---|---|
| A | 원자성(Atomicity) | 전부 되거나 전부 안 되거나. 절반만 되는 일은 없다 |
| C | 일관성(Consistency) | 트랜잭션 전후로 제약(외래키·CHECK 등)이 항상 지켜진다 |
| I | 격리성(Isolation) | 동시에 도는 트랜잭션끼리 서로의 중간 상태를 안 본다 |
| D | 지속성(Durability) | 커밋된 변경은 정전이 나도 살아남는다 |
앞의 주문 예시가 바로 원자성입니다. "주문 기록 + 재고 감소"가 통째로 되거나 통째로 안 됩니다. 일반적인 관계형 데이터베이스(SQLite 포함)는 이 ACID를 기본으로 보장합니다 — 우리가 신경 쓰지 않아도 데이터가 깨지지 않게 지켜 주는, 데이터베이스를 신뢰할 수 있게 만드는 토대입니다.
6.4 동시성: 여러 사람이 동시에 쓸 때
격리성(Isolation)이 다루는 문제입니다. 두 사람이 같은 책의 마지막 한 권을 동시에 주문하면? 둘 다 "재고 1"을 보고 둘 다 주문을 넣어, 재고가 -1이 되는 사고가 날 수 있습니다. 트랜잭션과 잠금(lock)이 이런 충돌을 조율합니다.
SQLite의 동시성 한계(솔직히). 1부에서 예고했듯, SQLite는 쓰기(write)를 한 번에 하나만 허용합니다. 누군가 쓰는 동안 다른 쓰기는 잠깐 기다립니다(읽기는 여럿이 동시에 가능). 혼자 쓰거나 트래픽이 적은 앱엔 충분하지만, 수많은 사용자가 동시에 데이터를 바꾸는 서비스라면 PostgreSQL·MySQL처럼 더 정교한 동시 쓰기를 지원하는 DB가 필요합니다. 이건 SQLite의 결함이 아니라 설계상의 선택(단순함과 무설치를 얻는 대신 동시 쓰기를 양보)입니다. 7부에서 언제 갈아타야 하는지 다룹니다.
동시성 제어는 깊은 주제지만, 입문 단계의 실천 지침은 단순합니다: 여러 단계로 데이터를 바꿀 땐 트랜잭션으로 묶어라. 그러면 동시 접근 상황에서도 어긋난 중간 상태가 남지 않습니다.
6.5 데이터 무결성: 세 종류
지금까지 흩어져 나온 "데이터가 어긋나지 않게 지키는 장치"들을 무결성(integrity)이라는 한 우산 아래 정리합니다. 무결성은 보통 세 가지로 나눕니다.
flowchart TB
integrity["데이터 무결성"]
integrity --> e["개체 무결성<br/>(Entity)"]
integrity --> r["참조 무결성<br/>(Referential)"]
integrity --> d["도메인 무결성<br/>(Domain)"]
e --> e2["기본키로 각 행을 유일하게 식별<br/>PRIMARY KEY"]
r --> r2["외래키가 가리키는 대상이 실제 존재<br/>FOREIGN KEY"]
d --> d2["각 칸의 값이 허용 범위 안<br/>NOT NULL · CHECK · 타입"]
classDef t fill:#fdf0d5,stroke:#e67e22,color:#000
class integrity,e,r,d,e2,r2,d2 t
- 개체 무결성 — 모든 행은 유일하게 구별돼야 한다. → 기본키(2부)가 보장.
- 참조 무결성 — 외래키는 실제 존재하는 대상만 가리켜야 한다. 존재하지 않는 고객의 주문은 못 만든다. → 외래키(3부 관계)가 보장.
- 도메인 무결성 — 각 칸의 값은 정해진 범위·형식을 지켜야 한다. 가격은 음수 불가, 제목은 필수. →
NOT NULL·CHECK·타입(3부 제약)이 보장.
핵심 메시지: 무결성은 우연히 지켜지는 게 아니라, 설계할 때 제약으로 새겨 두는 것입니다. 애플리케이션 코드의 검사는 빠뜨리거나 우회될 수 있지만, 데이터베이스에 박아 둔 제약은 어떤 경로로 들어와도 마지막 문지기로 작동합니다. 이것이 "데이터는 데이터베이스가 지킨다"는 말의 의미입니다.
6.6 정리
- 트랜잭션은 여러 변경을 "전부 되거나 전부 안 되거나"의 한 단위로 묶는다.
BEGIN으로 시작,COMMIT으로 확정,ROLLBACK으로 취소. - 트랜잭션은 ACID(원자성·일관성·격리성·지속성)를 보장한다 — 데이터베이스를 신뢰할 수 있게 하는 토대.
- 단일 문장은 자동 커밋된다. 여러 문장을 묶을 때만
BEGIN을 쓴다. - SQLite는 쓰기를 한 번에 하나만 허용한다(설계상 선택). 동시 쓰기가 많으면 다른 DB를 고려.
- 무결성은 개체(기본키)·참조(외래키)·도메인(제약·타입) 세 가지. 우연이 아니라 설계 시 제약으로 보장한다.
다음 챕터에서 운영의 나머지 절반, 속도를 다룹니다. 데이터가 많아져 쿼리가 느려질 때 인덱스(index) 로 빠르게 만들고, 복잡한 쿼리를 뷰(view) 로 깔끔하게 재사용하는 법을 배웁니다.
직접 해 보기
BEGIN으로 시작해 책 한 권을INSERT하고, 곧바로ROLLBACK한 뒤SELECT로 그 책이 없는지 확인하세요.- 같은 작업을
COMMIT으로 끝내면 어떻게 다른지 확인하세요. - (생각해 보기) "주문 기록 + 재고 감소"를 트랜잭션으로 안 묶으면, 구체적으로 어떤 나쁜 상태가 생길 수 있나요? 두 가지 시나리오를 적어 보세요.
- (연결 복습) 우리 서점 스키마에서 개체·참조·도메인 무결성을 각각 보장하는 제약을 하나씩 찾아 짝지어 보세요.
← 5부 집계와 GROUP BY · 목차 · 다음: 인덱스와 뷰 →