0. 트랜잭션 + 경계
데이터베이스도 선을 긋는 게 중요하다. 어디서부터 트랜잭션이 시작되고, 어디서 끝나는 지 알아야 한다. 그래서
트랜잭션이란
- 데이터베이스에서 여러 SQL 문장을 하나의 묶음(단위)으로 취급하는 것이다.
트랜잭션 경계란
- 트랜잭션이 시작되고 끝나는 구간을 의미한다.
BEGIN/START → 여러 SQL 실행 → 성공 시 COMMIT, 실패 시 ROLLBACK으로 마무리한다.
세부 개념
- Auto-commit: 일부 DB/클라이언트는 기본으로 각 문장을 자동 커밋한다. 다중 작업을 묶으려면 명시적 시작이 필요하다.
- DDL 자동 커밋: 많은 DB에서 스키마 변경(DDL)은 트랜잭션을 암묵 커밋 또는 종료시킬 수 있다.
- SAVEPOINT: 트랜잭션 내부의 부분 롤백 지점을 의미한다.
1. ACID란
DB가 약속하는 기본 네 가지의 원칙이 있는데, 이름은 간단하지만 실제로는 많은 내용을 담고 있다.
원자성 (Atomicity)
- “모두 아니면 아무 것도 아니다.” 부분 성공은 금지.
- Undo/Redo 로그, WAL 기반 복구를 통해 보장한다.
- 깨지면 부분적 반영이 발생한다.
- 체크리스트
- 실패 지점에서 롤백 경로가 설계되어 있는가?
- 외부 시스템에 동시에 쓰면 보상 트랜잭션 전략이 있는가?
일관성 (Consistency)
- 트랜잭션 전후로 비즈니스 불변식과 DB 제약이 항상 만족되어야 한다.
- PK/FK/UNIQUE, CHECK, 트리거, 애플리케이션 레벨 불변식으로 보장할 수 있다.
- 일관성은 DB만의 책임이 아니며 애플리케이션도 함께 보장해야 한다.
- 체크리스트
- 핵심 불변식을 문서화 및 테스트했는가?
- DB 제약으로 끌어내릴 수 있는가?
격리성 (Isolation)
- 동시에 실행돼도 서로 간섭 없이 순차 실행처럼 보이게 한다.
- 이상현상은 다음과 같다.
- Dirty Read
-> 다른 트랜잭션이 아직 커밋하지 않은 데이터를 읽어버리는 현상
- Non-repeatable Read
-> 같은 조건으로 두번 조회했는데 결과가 다르게 나오는 현상
- Phantom Read
-> 같은 조건으로 조회했는데 새 레코드가 끼어드는 현상
- Lost Update, Write Skew
-> 둘 이상의 트랜잭션이 동시에 갱신하면서 서로의 변경이 덮여 사라지는 현상
- 체크리스트
- 필요한 격리 수준을 업무 단위별로 정의했는가?
- 읽기 일관성이 크면 MVCC 이해가 필요한 것이다.
지속성 (Durability)
- 커밋된 결과는 장애 후에도 영구적으로 보존된다.
- WAL, fsync/그룹 커밋, 복제 및 백업을 통해 보장된다.
- 설정에 따라 강도가 달라질 수 있다.
- 체크리스트
- 장애 또는 재시작 시 복구 시나리오가 검증되었는가?
- 백업 및 복구 목표(RPO/RTO)가 수립되었는가?
2. 격리 수준 (Isolation Level)
동시에 여러 요청이 몰리면 꼬일 수 있다. 그래서 다음과 같은 격리 수준이 필수이다.
| 수준 | Dirty Read | Non-repeatable | Phantom | 비고/기본값 예시 |
|---|
| READ UNCOMMITTED | 가능 | 가능 | 가능 | 일부 DB는 미지원(PostgreSQL) |
| READ COMMITTED | 불가 | 가능 | 가능 | 기본값: Oracle, PostgreSQL |
| REPEATABLE READ | 불가 | 불가 | (표준상) 가능 | MySQL InnoDB 기본. InnoDB는 Next-Key Lock으로 팬텀도 상당수 방지 |
| SERIALIZABLE | 불가 | 불가 | 불가 | 가장 안전(성능 비용 큼). 일부 DB는 충돌 시 에러로 종료 |
3. 동시성 제어 기법
누가 먼저 쓸지, 어떻게 충돌을 막을 지 정하는 방법으로 동시성 제어 기법을 사용하는데, 그 예시로는 다음과 같다.
비관적 잠금 (Pessimistic Locking)
- 공유/배타 잠금, 의도 잠금, 갭락 및 넥스트키락을 사용한다.
- 충돌이 잦을 때 안전하다.
- 교착상태, 대기 및 타임아웃, 처리량 저하가 발생할 수 있다.
- 업데이트 충돌이 빈번하고 비용이 큰 경우 사용된다.
낙관적 잠금 (Optimistic Locking)
- 버전 또는 타임스탬프 필드, CAS 검증 후 재시도를 사용한다.
- 동시성이 높고 잠금 점유가 적다.
- 충돌 시 재시도 비용과 Starvation이 발생할 수 있다.
- 충돌이 드문 읽기 환경에서 사용된다.
MVCC (Multi-Version Concurrency Control)
- 쓰기는 새 버전을 생성하고 읽기는 시점 일관성을 확보한다.
- Readers don’t block writers 특성을 가진다(DB별 차이가 있다).
- 읽기 성능이 높아진다.
- 장수 트랜잭션은 블로트 문제(불필요한 데이터의 공간 낭비)를 발생시킬 수 있다.
4) 교착상태 (Deadlock)
동시성 프로그램이다보니, 교착상태가 발생하기도 한다. 그래서
발생 원인
- 두 트랜잭션이 서로가 가진 리소스를 기다리며 순환 대기를 형성할 때 발생한다.
예방 방법
- 일관된 잠금 순서를 준수해야 한다.
- 짧은 트랜잭션을 유지하고 배치를 쪼개야 한다.
- 적절한 인덱스로 불필요한 범위 잠금을 줄여야 한다.
- 타임아웃 및 재시도 정책을 수립하고 재시도 시 idempotency를 고려해야 한다.
5. 저장점 (Savepoint)
전체 롤백이 아니라 중간 지점까지 되돌리고 싶은 경우는 SAVEPOINT(저장점) 을 사용하는데, 그래서
SAVEPOINT란
- 트랜잭션 내부 체크포인트로 부분 롤백이 가능하다.
활용 예시
- 대량 처리 중 일부 레코드만 취소하고 나머지를 진행할 수 있다.
6. 분산 트랜잭션
DB가 하나만 있으면 좋은데, 여러 시스템이 얽혀 있기 때문에 분산 트랜잭션이 등장하였다.
2PC (Two-Phase Commit)
- 코디네이터가 참여자에게 Prepare와 Commit을 합의하는 방식이다.
- 원자성을 확보할 수 있다.
- 복잡성과 지연이 증가하고 장애 시 블로킹이 발생한다.
대안 패턴
- Saga: 로컬 트랜잭션과 보상 트랜잭션으로 처리하는 방식이다.
- Outbox/Inbox: 이벤트를 기록하여 안전하게 비동기 전파하는 방식이다.
- Idempotency Key: 재시도 시 중복 효과를 방지하는 방식이다.