---
type: knowledge
domain: backend
status: active
last-reviewed: 2026-07-27
---

# DB 접근과 트랜잭션 실전

> 한 줄 정의
> 트랜잭션 경계·락·마이그레이션은 "코드가 아니라 데이터가 깨지는" 지점이다. SQL 튜닝·인덱스는 [[SQL 성능과 인덱스 실전]], 모델링·저장소 선택은 [[데이터 원리]] — 여기는 애플리케이션이 DB를 만지는 방식의 실전 규율이다.

## 트랜잭션 경계 = 유스케이스 단위

- 경계는 **"전부 아니면 전무"인 업무 단위**에 긋는다 — 주문 생성(주문+재고+이력)이 한 트랜잭션, 화면 하나가 아니다.
- **레포지토리 메서드가 각자 트랜잭션을 여는 구조 금지** — save() 3번 = 커밋 3번 = 부분 성공이라는 유령 상태. 트랜잭션은 유스케이스(서비스) 계층이 열고, 레포지토리는 참여만 한다.
- 서비스가 서비스를 호출하며 트랜잭션이 중첩·전파되기 시작하면 경계 재설계 신호다 → [[백엔드 원리]] 도메인 설계.

## 트랜잭션 안에서 외부 호출 금지

트랜잭션 = 커넥션 점유 + 락 유지. 그 안의 외부 API 호출(메일·결제·LLM)은 상대 지연만큼 락을 들고 있는 것이다.

| 순서 | 규칙 |
|------|------|
| 조회·검증 | 형식 검증·불변 데이터 조회는 밖에서. 단 재고·잔액·현재 상태 같은 **쓰기의 전제조건은 트랜잭션 안에서** 조건부 갱신·제약으로 재검증한다 — 밖에서 본 값은 커밋 시점에 이미 낡았을 수 있다 |
| DB 변경 | 짧은 트랜잭션으로 커밋 |
| 외부 호출·알림 | 커밋 **후**. 원자성이 필요하면 outbox → [[분산 신뢰성 패턴]] |

"커밋했는데 메일 발송 실패"는 재시도로 풀 수 있지만, "외부 호출 대기 중 락 적체"는 시스템 전체를 세운다.

## N+1 — 코드 리뷰가 아니라 쿼리 카운트로 잡는다

- 목록 화면·루프 안 연관 조회 = 즉시 의심. 탐지·처방(eager loading·IN 배치)은 [[SQL 성능과 인덱스 실전]]에 위임.
- 실전 장치 하나만 추가: **개발 환경에서 요청당 쿼리 수 로그·경고**(예: 요청 1건에 쿼리 30개 초과 시 경고). 눈으로는 못 본다.

## 락 — 낙관이 기본값

| 방식 | 채택 조건 | 함정 |
|------|----------|------|
| 낙관적 락 (버전 필드) — **기본값** | 충돌이 드물다 (일반 CRUD 대부분) | 충돌 시 재시도·사용자 안내 경로를 안 만들면 그냥 500 |
| 비관적 락 (SELECT FOR UPDATE) | 충돌이 잦고 재시도 비용이 크다 (재고 차감·순번 채번) | 락 범위·순서 불일치 = 데드락. 잡는 순서를 통일한다 |
| 원자적 갱신 (`UPDATE SET cnt = cnt - 1 WHERE cnt > 0`) | 카운터·재고류 경합 | 락 없이 읽고-계산하고-쓰기로 풀면 진다 (같은 트랜잭션의 FOR UPDATE 후 재검증은 예외) |

분산 환경의 락·멱등 우선순위는 [[분산 신뢰성 패턴]] 5절 — 분산 락은 최후 수단이다.

## 마이그레이션 규율

- **버전 관리되는 마이그레이션 파일만**이 스키마 변경 경로다. 콘솔에서 직접 ALTER = 환경 간 스키마 표류의 시작.
- 롤백 판단: 구조 변경(컬럼 추가 등)은 down 스크립트를 준비하되 새 컬럼에 데이터가 쓰인 뒤의 down은 그 자체로 파괴다 — 기본은 roll-forward. **데이터를 파괴하는 변경(컬럼 삭제·타입 축소)은 롤백이 원리적으로 불가** — 이런 건 순방향 전용으로 선언하고 expand-contract로 단계 분리. 5단계 분해의 원리 정본은 [[마이그레이션·전환 설계]], 배포 순서 실무는 [[배포 전략 실전]].
- **마이그레이션과 앱 배포를 한 몸으로 묶지 않는다**: 이전 버전 앱이 새 스키마에서도 돌아가는 순서(스키마 먼저·호환 유지)로. 대형 테이블 ALTER는 락을 잡는다 — 온라인 DDL 확인 후 배포 창구에서.
- 외주 지점: 스키마 변경 이력은 납품물이고 데이터 소유권은 발주사다 — 발주사 DB에 스크립트 없는 수동 변경을 남기면 이관 시점에 재현 불가 스키마가 된다. 운영 DB 직접 변경은 발주사 합의 창구로만.

## 대량 처리 — 청크가 기본

- 100만 행을 한 트랜잭션으로 = 락 장시간 유지 + 롤백 폭탄 + 메모리. **청크(수백~수천 행) 단위로 나누고, 청크마다 커밋**.
- 재개 가능하게: 마지막 처리 키를 기록해 중단 지점부터 재시작 — "처음부터 다시"는 멱등이 보장될 때만 허용.
- 청크 순회는 OFFSET이 아니라 keyset(마지막 ID 기준) → [[SQL 성능과 인덱스 실전]] 페이지네이션.
- 실행 중 지표(처리 건수·실패 건수)를 로그로 남긴다 — 몇 시간짜리 배치가 무성으로 도는 것 자체가 결함이다.

## 안티패턴

- **트랜잭션을 "혹시 몰라서" 넓게** — 컨트롤러 진입부터 응답까지 하나의 트랜잭션은 안전이 아니라 락 적체다.
- **ORM 기본 동작 방치** — lazy/eager 기본값, 더티 체킹 범위를 모르고 쓰면 쿼리가 어디서 나가는지 아무도 모른다.
- **재시도 없는 낙관적 락** — 버전 충돌 예외를 그대로 사용자에게 던지면 락을 안 쓴 것보다 UX가 나쁘다.
- **운영 장애를 데이터 수동 UPDATE로 봉합** — 원인 코드는 그대로라 재발하고, 수정 이력은 아무 데도 없다.

## 관련 문서

- [[00_백엔드 허브]] — 06_백엔드 진입점
- [[백엔드 원리]] — 도메인 설계·신뢰성 채택 기준
- [[SQL 성능과 인덱스 실전]] — 실행 계획·N+1 상세·페이지네이션
- [[데이터 원리]] — 트랜잭션·동시성 원칙, 모델링
- [[분산 신뢰성 패턴]] — outbox·saga·분산 락 우선순위
- [[마이그레이션·전환 설계]] — Expand–Contract 5단계·듀얼 런 검증의 원리 정본
- [[배포 전략 실전]] — 마이그레이션과 배포 순서 실무
