topic난이도 · 약 35

인덱스 · 트랜잭션 · ACID · 격리수준

B-Tree 인덱스로 100만 행에서 0.001초, 트랜잭션으로 데이터 일관성 보장.

#인덱스#B-Tree#트랜잭션#ACID#격리수준#FOR UPDATE
왜 배우는가

"사용자가 많아지면서 페이지가 느려졌어요" → 90%는 인덱스 누락. "주문은 생성됐는데 결제는 안 찍혔어요" → 100%는 트랜잭션 누락. 이 토픽 하나로 프로덕션 장애의 절반을 예방할 수 있다.

인덱스는 책의 '찾아보기'다. 500쪽짜리 책에서 "파이썬" 단어를 찾을 때, 처음부터 읽으면 몇 시간이지만 책 뒤 찾아보기(색인)를 보면 "파이썬 ... 237쪽" 한 줄로 끝난다. DB 인덱스도 똑같다.

인덱스 없는 조회 vs 있는 조회 — 풀스캔(n) vs B-Tree 탐색(log n)
사용자 수인덱스 없음 (Full Scan)인덱스 있음 (B-Tree)
1,0001~10ms< 1ms
100,000100ms~1초< 1ms
10,000,00010초+ (타임아웃)< 5ms
원리O(n) 순차O(log n) 이진탐색
sql
-- 인덱스 생성 (가장 흔한 케이스)
CREATE INDEX idx_users_email ON users(email);

-- 복합 인덱스 (컬럼 순서 중요!)
CREATE INDEX idx_posts_user_created ON posts(user_id, created_at DESC);

-- 유니크 인덱스 = 인덱스 + 중복 방지
CREATE UNIQUE INDEX idx_users_email ON users(email);

-- 실행 계획 확인 — 인덱스 쓰는지 검증하는 핵심 명령
EXPLAIN ANALYZE
SELECT * FROM users WHERE email = 'kim@example.com';
-- "Index Scan using idx_users_email"   ← 인덱스 사용 중 ✅
-- "Seq Scan on users"                  ← 풀스캔 중 ❌ (인덱스 없거나 안 먹힘)

`EXPLAIN ANALYZE`는 DB가 쿼리를 어떻게 실행하는지 보여준다. `Seq Scan`이 보이면 인덱스를 검토해야 한다. Claude에게 쿼리 최적화를 시킬 때 이 결과를 함께 붙이면 정확한 조언이 나온다.

이런 쿼리라면이런 인덱스이유
`WHERE email = ?``idx(email)`단일 컬럼 등호
`WHERE user_id=? AND created_at > ?``idx(user_id, created_at)`복합 인덱스, 순서 중요
`ORDER BY created_at DESC``idx(created_at DESC)`정렬 자체가 인덱스 활용
`WHERE LOWER(email) = ?``idx(LOWER(email))`함수 인덱스
`WHERE email LIKE 'kim%'``idx(email)`✅ 전방 일치는 OK
`WHERE email LIKE '%kim%'`인덱스 못 씀 (전체 스캔)

인덱스의 비용. 읽기는 빨라지지만 쓰기(INSERT/UPDATE/DELETE)는 느려진다 — 인덱스도 갱신해야 하니까. 그리고 디스크 용량도 먹는다. 모든 컬럼에 인덱스를 박으면 오히려 느려질 수 있다. 원칙: WHERE·JOIN·ORDER BY에 자주 등장하는 컬럼에만 붙인다.

왜 B-Tree? DB가 쓰는 B-Tree는 디스크 I/O에 최적화된 자료구조다. 한 노드에 수백 개 키를 담아 트리 높이를 3~4단으로 유지 — 10억 행이어도 디스크를 3~4번만 읽으면 원하는 행을 찾는다(ch18-2 이진탐색의 디스크 확장판).

트랜잭션은 '전부 아니면 전혀'다. 계좌 이체를 생각해보자. A의 잔액을 1000원 빼고, B의 잔액에 1000원 더한다. 두 동작 사이에서 서버가 죽으면? 돈이 사라진다. 트랜잭션은 이 두 동작을 묶어서, 둘 다 성공하거나 둘 다 없던 일(롤백)로 만든다.

sql
-- ❌ 위험: 트랜잭션 없이 두 줄
UPDATE accounts SET balance = balance - 1000 WHERE id = 'A';
-- 여기서 서버 크래시 → B는 못 받음, A만 손해
UPDATE accounts SET balance = balance + 1000 WHERE id = 'B';

-- ✅ 안전: 트랜잭션으로 묶기
BEGIN;
  UPDATE accounts SET balance = balance - 1000 WHERE id = 'A';
  UPDATE accounts SET balance = balance + 1000 WHERE id = 'B';
COMMIT;
-- 중간에 에러나면 전체 ROLLBACK

-- 실전: 앱 코드 (Node.js + Postgres)
await client.query('BEGIN');
try {
  await client.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [1000, 'A']);
  await client.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [1000, 'B']);
  await client.query('COMMIT');
} catch (err) {
  await client.query('ROLLBACK');   // ← 필수
  throw err;
}

돈·재고·포인트를 건드리는 로직에는 반드시 트랜잭션. Claude가 만든 코드에 `BEGIN/COMMIT`이 없다면 반드시 지시해서 추가시켜야 한다.

트랜잭션의 4대 보장 — ACID. 암기 필수. 이걸 모르면 DB 설계 리뷰에서 감이 안 잡힌다.

글자이름깨지면?
AAtomicity (원자성)전부 성공 or 전부 롤백돈 절반만 이체
CConsistency (일관성)제약조건·규칙 유지재고 음수
IIsolation (격리성)동시 트랜잭션 서로 간섭 없음레이스 컨디션
DDurability (지속성)커밋 후 전원 꺼져도 유지주문 사라짐

격리 수준(Isolation Level)은 '동시 트랜잭션이 서로 얼마나 보이는가'를 조절하는 다이얼이다. 엄격할수록 안전하지만 느리다. 바이브코더가 기본 이상은 알아야 레이스 컨디션 버그를 피한다.

격리 수준Dirty ReadNon-repeatablePhantom흔한 DB 기본값
Read Uncommitted⚠️⚠️⚠️거의 사용 안 함
Read Committed⚠️⚠️PostgreSQL 기본
Repeatable Read⚠️MySQL 기본
Serializable가장 엄격, 가장 느림
현상예시
Dirty Read아직 커밋 안 된 변경을 다른 트랜잭션이 읽음롤백될 데이터를 보고 결정
Non-repeatable Read같은 쿼리를 두 번 하는데 결과가 다름중간에 다른 트랜잭션이 값 변경
Phantom Read같은 범위 쿼리에 새 행이 등장중간에 INSERT 발생
sql
-- 재고 차감 같은 민감 로직 → 격리 수준 올리기
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

  SELECT stock FROM products WHERE id = 'X';   -- 100개
  -- 다른 트랜잭션이 동시에 재고 차감 시도 시 직렬화 실패로 롤백
  UPDATE products SET stock = stock - 1 WHERE id = 'X';

COMMIT;

-- 또는 더 가벼운 방법: 행 잠금
BEGIN;
  SELECT stock FROM products WHERE id = 'X' FOR UPDATE;  -- ← 이 행 잠금
  UPDATE products SET stock = stock - 1 WHERE id = 'X';
COMMIT;

재고·좌석 예약·포인트 차감처럼 동시 호출이 동시에 같은 행을 건드릴 때 SERIALIZABLE 또는 `FOR UPDATE` 행 잠금. 안 하면 두 사람이 같은 좌석을 예매하는 사고.

Durability는 디스크 fsync로 보장된다. DB는 COMMIT 시점에 변경 내역을 WAL(Write-Ahead Log)에 적고, 그게 디스크에 안전히 기록됐음을 OS에 확인받은 뒤에야 클라이언트에게 성공을 알린다. Supabase·PlanetScale 같은 관리형 DB는 이걸 여러 노드로 복제까지 자동 처리.

Claude에게 이렇게 말하면 정확히 고쳐준다. ① "posts 테이블에서 user_id 조회가 느려. `user_id` 컬럼에 인덱스 추가하고 `EXPLAIN ANALYZE`로 확인해줘." ② "주문 생성 + 결제 기록 로직을 트랜잭션으로 묶어서, 결제가 실패하면 주문도 롤백되게 해줘." ③ "재고 차감 로직에 `FOR UPDATE` 잠금 걸어줘. 동시 예약 레이스 방지."

한 줄 요약: 자주 WHERE에 걸리는 컬럼엔 인덱스, 돈·재고 건드리는 로직은 트랜잭션, 동시성 민감하면 격리 수준/FOR UPDATE. 이 세 줄이면 DB 장애의 80%를 예방.

실기 드릴 5문항
edit실기 드릴 · 단답형

트랜잭션의 4대 보장(ACID)을 풀어서 쓰시오.

check_circle실기 드릴 · OX

모든 컬럼에 인덱스를 만들수록 DB는 항상 빨라진다.

database실기 드릴 · SQL 작성

users 테이블의 email 컬럼에 중복을 허용하지 않는 인덱스를 추가하는 SQL을 작성하시오.

sql
CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT);
check_circle실기 드릴 · OX

PostgreSQL의 기본 격리 수준은 Serializable이다.

edit실기 드릴 · 단답형

동시에 같은 행을 수정하는 레이스 컨디션을 막기 위해 PostgreSQL에서 쓰는 SQL 키워드는?