이 글에서 제시하는 수치는 일반적인 운영 환경에서 관측되는 대략적인 범위입니다. 실제 성능은 하드웨어, 워크로드, 설정에 따라 달라집니다.
PART 1. ACID로 DB 동작 이해하기
1. Application을 넘어 DB 레벨로
2. ACID 개념 정리
PART 2. Durability의 실제 의미
3. 메모리, OS, 디스크: 데이터가 저장되는 계층
4. write()와 fsync()의 차이
5. Redo Log / WAL의 역할
6. Group Commit: fsync 비용 분산
PART 3. MySQL (InnoDB)의 Durability 설계
7. InnoDB Redo Log 구조
8. innodb_flush_log_at_trx_commit 옵션
9. 설정별 성능 차이
10. = 2를 선택할 때의 전제 조건
PART 4. PostgreSQL의 Durability 설계
11. WAL 구조
12. 기본 설정에서의 Durability
13. 구조적 Group Commit
14. 고트래픽에서의 Latency 특성
PART 5. 비교와 선택
15. 설계 철학의 차이
16. 트래픽 증가에 따른 성능 특성
17. 서비스 유형별 선택 경향
18. 정리
평소에는 JPA나 ORM 레벨에서 @Transactional을 붙이고 넘어갑니다. 하지만 성능 튜닝이나 장애 대응을 하다 보면 DB 내부 동작을 알아야 할 때가 있습니다.
ACID는 그 출발점으로 좋습니다. 이 글에서는 ACID를 간단히 훑은 뒤, 그중 Durability를 깊이 살펴봅니다.
트랜잭션 내의 모든 연산이 전부 성공하거나 전부 실패합니다. 단일 DB 내에서는 명확하게 보장됩니다.
분산 환경에서는 Saga 패턴, Outbox 패턴 등으로 해결합니다.
DB가 정의된 제약조건(NOT NULL, UNIQUE, FK 등)을 항상 만족하는 상태를 유지합니다.
분산 시스템의 "eventual consistency"와는 다른 개념입니다.
여러 트랜잭션이 동시에 실행될 때 서로 격리합니다. 완벽한 격리(Serializable)는 동시성을 제한하므로, 대부분 Read Committed나 Repeatable Read를 사용합니다.
MySQL과 PostgreSQL 모두 MVCC를 사용하지만, 구현 방식이 다릅니다.
"COMMIT된 트랜잭션은 영구적으로 저장된다"는 속성입니다.
그런데 여기서 질문이 생깁니다.
이 질문들에 대한 답이 MySQL과 PostgreSQL에서 다르게 나타납니다. 다음 PART에서 구체적으로 살펴봅니다.
애플리케이션이 DB에 데이터를 쓸 때, 데이터는 여러 계층을 거칩니다.
[Application] → [DB Buffer] → [OS Page Cache] → [Disk Controller Cache] → [Disk]
각 계층마다 crash 시 결과가 다릅니다.
Crash 유형별 데이터 손실 범위
시스템 콜 수준에서 write()와 fsync()는 완전히 다른 보장을 제공합니다.
write()
- 데이터를 OS Page Cache에 복사
- 디스크에는 아직 없을 수 있음
fsync()
- OS가 해당 파일의 모든 데이터를 디스크에 기록하도록 강제
- 이 호출이 반환되면 디스크에 있다고 믿을 수 있음
fsync()는 비용이 큽니다.
| 저장 장치 | fsync latency (대략) |
|---|---|
| 7200 RPM HDD | 5~15ms |
| 15000 RPM HDD | 3~8ms |
| SATA SSD | 0.5~2ms |
| NVMe SSD | 0.1~0.5ms |
| 배터리 백업 RAID | 0.1~0.3ms |
매 트랜잭션마다 전체 데이터 페이지를 fsync하는 것은 비효율적입니다. 대신 로그 선행 기록(Write-Ahead Logging) 방식을 사용합니다.
Write-Ahead Logging 동작 방식
순차 쓰기라서 무작위 쓰기보다 빠릅니다.
MySQL에서는 Redo Log, PostgreSQL에서는 WAL(Write-Ahead Log)이라고 부릅니다.
매 트랜잭션마다 fsync를 하면, fsync latency가 곧 commit latency가 됩니다.
NVMe SSD에서 fsync가 0.2ms라면?
→ 단순 계산으로 초당 최대 5,000 TPS
→ 고트래픽 서비스에서는 병목
Group Commit은 여러 트랜잭션의 로그 기록을 모아서 한 번의 fsync로 처리합니다.
Group Commit 동작 방식
트랜잭션 1 → 로그 기록 (대기)
트랜잭션 2 → 로그 기록 (대기)
트랜잭션 3 → 로그 기록 (대기)
↓
한 번의 fsync
↓
트랜잭션 1, 2, 3 모두 COMMIT 완료
fsync 비용이 여러 트랜잭션에 분산되고, 동시 요청이 많을수록 효율이 좋아집니다.
| 시나리오 | fsync/commit | 예상 TPS (NVMe 기준) |
|---|---|---|
| 단건 commit | 1회 | ~3,000~5,000 |
| Group Commit (10건 묶음) | 0.1회 | ~20,000~40,000 |
| Group Commit (50건 묶음) | 0.02회 | ~50,000+ |
InnoDB는 Redo Log를 통해 Durability를 구현합니다.
[트랜잭션 변경] → [Redo Log Buffer] → [OS Page Cache] → [Disk]
COMMIT 시점에 Redo Log가 어디까지 기록되어야 하는지가 Durability 수준을 결정합니다.
MySQL은 innodb_flush_log_at_trx_commit 파라미터로 이 동작을 제어합니다.
COMMIT → Log Buffer → OS Page Cache → fsync() → 완료
COMMIT → Log Buffer → OS Page Cache → 완료
(fsync는 1초마다 별도로)
COMMIT → Log Buffer → 완료
(모든 flush가 1초마다)
설정값별 동작 요약
| 설정 | COMMIT 시 동작 | 손실 가능 범위 |
|---|---|---|
| = 1 | Log Buffer → OS → Disk | 없음 |
| = 2 | Log Buffer → OS | OS crash 시 ~1초 |
| = 0 | Log Buffer | DB crash 시 ~1초 |
| 설정값 | commit latency (NVMe) | 예상 TPS | 보장 수준 |
|---|---|---|---|
| = 1 | 0.3~1ms | 3,000~8,000 | DB + OS crash |
| = 2 | 0.05~0.2ms | 15,000~40,000 | DB crash only |
| = 0 | 0.01~0.05ms | 30,000~60,000 | 없음 |
많은 서비스가 = 2를 선택합니다.
OS crash는 드물다
1초 손실이 치명적이지 않은 경우가 많다
애플리케이션 수준에서 재처리가 가능하다
단, 다음 조건을 확인해야 합니다.
= 2 선택 전 체크리스트
- 비즈니스가 "1초 이내 손실 가능"을 허용하는가?
- 재처리/복구 메커니즘이 갖춰져 있는가?
- OS crash 감지를 위한 모니터링이 있는가?
- 운영팀이 이 설정의 의미를 이해하고 있는가?
결제, 정산, 금융 거래라면 = 1 또는 별도 보장 필요
PostgreSQL도 WAL을 통해 Durability를 구현합니다.
[트랜잭션 변경] → [WAL Buffer] → [WAL 파일 (Disk)]
주요 관련 설정입니다.
| 설정 | 기본값 | 설명 |
|---|---|---|
| synchronous_commit | on | COMMIT 시 WAL fsync 여부 |
| fsync | on | WAL fsync 사용 여부 (끄지 말 것) |
| wal_sync_method | fdatasync | fsync 방식 |
synchronous_commit = on은 MySQL의 innodb_flush_log_at_trx_commit = 1과 유사한 보장을 제공합니다.
MySQL에서 Group Commit은 성능 최적화를 위해 "추가된" 기능입니다. PostgreSQL은 WAL writer 프로세스의 동작 방식 자체가 Group Commit을 자연스럽게 만듭니다.
PostgreSQL의 Group Commit 동작 방식
Backend 1 → WAL Buffer 기록 → "fsync 요청"
Backend 2 → WAL Buffer 기록 → "fsync 요청"
Backend 3 → WAL Buffer 기록 → "fsync 요청"
↓
WAL writer가 모아서 fsync
↓
Backend 1, 2, 3 모두 완료
동시 트랜잭션이 많을수록 하나의 fsync에 더 많이 묶입니다.
| 동시 연결 수 | 평균 묶음 크기 (대략) |
|---|---|
| 10 | 2~5 |
| 50 | 10~20 |
| 200 | 30~100 |
| 500+ | 100~500 |
PostgreSQL의 구조적 Group Commit은 흥미로운 특성을 만듭니다.
저트래픽 (동시 연결 수 적음)
고트래픽 (동시 연결 수 많음)
반직관적이지만, 부하가 높아지면 latency가 오히려 안정되는 구간이 있습니다.
PostgreSQL도 Durability를 완화하는 옵션이 있습니다.
SET synchronous_commit = off;
COMMIT 시 fsync를 기다리지 않습니다. WAL writer가 주기적으로(기본 200ms마다) fsync합니다.
MySQL = 2가 1초마다 fsync하는 것과 비교하면, PostgreSQL이 좀 더 촘촘하게 보호합니다. 단, 둘 다 "비정상 종료 시 일부 손실 가능"이라는 점은 같습니다.
MySQL (InnoDB)
innodb_flush_log_at_trx_commit의 세 가지 값이 예시PostgreSQL
| 측면 | MySQL (InnoDB) | PostgreSQL |
|---|---|---|
| 기본 Durability | Full (= 1) | Full (sync_commit = on) |
| 완화 옵션 | = 2, = 0 | sync_commit = off |
| 손실 가능 범위 | = 2: ~1초 | off: ~수백ms |
| Group Commit | 있음 | 구조적으로 내장 |
| 설정 복잡도 | 명시적 선택 필요 | 기본값으로 충분한 경우 많음 |
저트래픽
고트래픽
MySQL = 2는 모든 구간에서 가장 빠르지만, OS crash 시 데이터 손실 가능성이 있습니다.
| 서비스 유형 | 흔히 관측되는 조합 | 배경 |
|---|---|---|
| 일반 웹 서비스 | MySQL = 2 또는 PostgreSQL 기본 | 성능과 안정성의 균형 |
| 고트래픽 서비스 | MySQL = 2 + 재처리 보장 | 최대 TPS 필요 |
| 금융/정산 | MySQL = 1 또는 PostgreSQL 기본 | 손실 불허 |
| 로그/분석 | MySQL = 0 또는 전용 시스템 | 성능 최우선 |
ACID를 단순히 "지원함 / 지원하지 않음"으로 구분하는 것은
현실의 데이터베이스를 이해하는 데에는 다소 한계가 있습니다.
대부분의 관계형 데이터베이스는 각자의 방식으로 ACID를 충족하고 있으며,
중요한 차이는 어떤 방식으로 이를 구현하고 있는지,
그리고 어떤 선택지를 제공하고 있는지에 있습니다.
흔히 이야기되는 "COMMIT되면 항상 절대적으로 안전하다"는 표현 역시
모든 상황을 그대로 반영한다고 보기는 어렵습니다.
Durability는 하나의 고정된 개념이 아니라 여러 단계로 나뉘며,
각 단계는 서로 다른 성능 비용과 안정성 특성을 가집니다.