ACID: 데이터베이스의 절대 약속
1. 프롤로그 - "100만 원이 허공으로 사라진다면?"
개발을 처음 배울 때, 저는 데이터베이스를 단순히 "데이터를 저장하는 엑셀 같은 것"이라고 생각했습니다. INSERT 하면 들어가고 SELECT 하면 나오는 단순한 창고로만 여겼죠. 하지만 금융 시스템 사례를 공부하면서 그 생각이 얼마나 안일했는지 깨닫게 되었습니다.
상황은 이렇습니다. A가 B에게 100만 원을 이체하는 로직을 짰습니다.
- Step 1: A의 계좌에서 100만 원을 차감한다 (
UPDATE accounts SET balance = balance - 100 WHERE id = 'A'). - Step 2: (여기서 갑자기 서버 전원이 꺼짐! 💥 또는 네트워크 타임아웃 발생!)
- Step 3: B의 계좌에 100만 원을 입금한다 (
UPDATE accounts SET balance = balance + 100 WHERE id = 'B').
서버가 재부팅된 후 로그를 확인해보면, A의 돈은 줄어들었는데 B의 돈은 늘어나지 않았습니다. 100만 원이 디지털 허공으로 사라진 것입니다.
이런 사례를 통해 뼛속 깊이 느끼게 됩니다. "데이터베이스의 진짜 가치는 '저장'이 아니라 '보장'에 있구나." 금융 시스템이 왜 그 느리고 비싸고 확장하기 힘든 RDBMS(관계형 데이터베이스)를 고집하는지, 그 이유가 바로 트랜잭션(Transaction)이라는 "절대 약속" 때문이라는 걸 알게 됩니다.
2. ACID: 트랜잭션의 4가지 절대 원칙
트랜잭션이 안전하게 수행된다는 것을 보장하기 위해, DB는 다음 4가지 성질을 목숨 걸고 지킵니다. 핵심 개념인 ACID입니다.
1) Atomicity (원자성: "All or Nothing")
- 정의: 트랜잭션 내의 모든 작업은 완벽하게 실행되거나, 아니면 아예 실행되지 않아야 합니다. 중간 단계(Partial Update)는 존재할 수 없습니다. 99개가 성공해도 마지막 1개가 실패하면, 앞선 99개도 없던 일로 만들어야 합니다.
- 작동 원리 (Undo Log): DB는 변경 전의 데이터를 Undo Log라는 별도 공간에 기록해둡니다. 만약 중간에 에러가 나거나
ROLLBACK명령이 떨어지면, 이 로그를 보고 원래 상태로 되돌립니다.
2) Consistency (일관성 - "법칙 준수")
- 정의: 트랜잭션이 끝난 후에도 DB는 정의된 모든 규칙(무결성 제약 조건)을 만족해야 합니다.
- 예시: "잔액은 마이너스가 될 수 없다" (
CHECK제약 조건). 트랜잭션 도중에 잔액이 마이너스가 되는 연산이 발생하면, DB는 즉시 트랜잭션을 중단시키고 롤백합니다. 단순히 타입 체크뿐만 아니라 외래키 제약조건(Foreign Key) 등 "약속된 규칙"을 절대 어기지 않습니다.
3) Isolation (격리성 - "간섭 금지")
- 정의: 여러 트랜잭션이 동시에 실행될 때, 서로의 연산에 끼어들거나 중간 데이터를 볼 수 없어야 합니다.
- 현실적인 타협: 완벽한 격리(Serializable)를 보장하려면 한 번에 하나씩 처리해야 하므로 성능이 너무 떨어집니다. 그래서 현실적으로는 격리 수준(Isolation Level)을 조절하여 사용합니다. 이는 개발자가 반드시 설정(Tuning)해야 하는 영역입니다.
4) Durability (지속성 - "불멸의 기록")
- 정의: 사용자가
COMMIT을 했고 DB가 "OK" 응답을 보냈다면, 그 직후에 전원이 뽑혀도 데이터는 살아있어야 합니다. - 작동 원리 (WAL - Write Ahead Logging): DB는 데이터를 실제 디스크에 쓰기 전에, 먼저 로그 파일(Redo Log)에 "나 이거 쓴다!"라고 기록합니다. 전원이 꺼졌다 켜지면 이 로그를 보고 복구(Replay)합니다.
3. 격리 수준 (Transaction Isolation Levels) 심층 분석
DB 성능 최적화의 핵심입니다. 격리를 세게 할수록 데이터는 안전하지만 느려지고, 느슨하게 할수록 빨라지지만 데이터 오류(Anomaly) 가능성이 커집니다. 주니어와 시니어를 가르는 질문이 여기서 나옵니다.
Level 0: Read Uncommitted (커밋되지 않은 읽기)
- 상황: A가 데이터를 수정 중인데 아직 커밋 안 함. B가 그 데이터를 읽음 (Dirty Read).
- 위험: A가 롤백하면 B는 "없는 데이터"를 가지고 로직을 돌린 셈이 됩니다. 절대 사용 금지. 데이터 정합성을 포기하는 것과 같습니다.
Level 1: Read Committed (커밋된 읽기)
- 상황: 오직 커밋이 완료된 데이터만 읽을 수 있습니다. Oracle, PostgreSQL의 기본값입니다. Dirty Read는 막습니다.
- 문제 (Non-Repeatable Read): 하나의 트랜잭션 안에서 똑같은
SELECT를 두 번 날렸는데, 그 사이에 남이 데이터를 바꿔서 결과가 달라질 수 있습니다. "어? 아까는 잔액이 100원이었는데 지금은 200원이네?"
Level 2: Repeatable Read (반복 가능한 읽기)
- 상황: 트랜잭션이 시작된 시점의 스냅샷(Snapshot)을 봅니다. 남들이 데이터를 지지고 볶아도, 나는 내가 시작할 때의 데이터만 봅니다. MySQL (InnoDB)의 기본값입니다.
- 기술 (MVCC): 데이터를 덮어쓰는 게 아니라, 버전(Version)별로 관리합니다. Undo Log를 포인터로 연결해서 과거 버전을 찾아갑니다.
- 문제 (Phantom Read): 없던 데이터(유령)가 갑자기 나타나는 현상. MySQL은 Next-Key Lock으로 이것까지 어느 정도 방어합니다.
Level 3: Serializable (직렬화 가능)
- 상황: 읽기 작업에도 잠금(Shared Lock)을 걸어버립니다.
- 성능: 동시성이 극도로 떨어집니다. 데드락(Deadlock)이 빈번하게 발생합니다. 어지간하면 쓰지 마세요.
4. 확장된 고민 - 분산 락과 CAP 이론
1) 분산 락 (Distributed Lock)
단일 DB에서는 트랜잭션으로 해결되지만, 서버가 여러 대라면? 예를 들어 선착순 쿠폰 이벤트를 한다면, DB 락만으로는 부족할 수 있습니다. 이때 Redis를 이용한 분산 락(Redlock)을 씁니다.
- 원리: Redis에
SET key value NX PX 10000(존재하지 않을 때만 쓰고, 10초 뒤 만료) 명령을 보냅니다. 성공한 서버만 로직을 수행하고, 나머지는 대기합니다.
2) CAP 이론과의 연결
분산 시스템에는 CAP 이론이 있습니다.
- Consistency (일관성): 모든 노드가 같은 데이터를 보여줌.
- Availability (가용성): 항상 응답함.
- Partition Tolerance (분할 내성): 통신이 끊겨도 동작함.
ACID는 C(일관성)를 극한으로 추구하는 모델입니다. 반면 NoSQL(MongoDB 등)은 A(가용성)와 P(분할 내성)를 선택하고 C를 희생(Eventual Consistency)하는 경향이 있습니다. "내가 만드는 서비스가 금융(C)인가, SNS(A)인가?"를 먼저 정의해야 DB를 고를 수 있습니다.
5. 데드락(Deadlock)과 그 해결책
격리 수준을 높이다 보면 필연적으로 데드락을 만납니다.
- 순서 맞추기: 트랜잭션 내에서 자원에 접근하는 순서를 항상 동일하게(
User 1 -> User 2) 맞춥니다. - 타임아웃 설정: 무한 대기하지 않도록 설정합니다.
- 인덱스 최적화: 인덱스가 없으면 DB가 테이블 전체를 잠그는(Table Lock) 멍청한 짓을 하여 데드락 확률이 폭증합니다.
6. 자주 묻는 질문 (FAQ)
Q. 언제 NoSQL을 쓰고 언제 RDBMS를 써야 하나요? A. 데이터의 구조가 자주 바뀌거나(Schema-less), 엄청난 읽기/쓰기 성능이 필요하고 약간의 데이터 불일치를 감수할 수 있다면(좋아요 수 등) NoSQL을 쓰세요. 하지만 돈, 결제, 재고 등 정합성이 생명이라면 무조건 RDBMS입니다.
Q. @Transactional(readOnly = true)는 왜 쓰나요? A. 성능 최적화를 위해서입니다. 읽기 전용 트랜잭션이라고 명시하면, JPA가 변경 감지(Dirty Checking)를 위한 스냅샷을 만들지 않아 메모리를 절약합니다. 또한, Slave DB(Replica)로 요청을 보내 부하를 분산할 수도 있습니다.
7. 마무리 - 주니어와 시니어의 차이
ACID를 공부하고 나서야 비로소 "왜 엔터프라이즈 시스템이 NoSQL의 유혹에도 불구하고 여전히 RDBMS를 메인 저장소로 쓰는지" 이해하게 되었습니다. 데이터의 정합성은 타협할 수 없는 가치이기 때문입니다.
주니어 개발자는 "기능이 돌아가는가"에 집중합니다. 하지만 시니어 개발자는 "많은 트래픽과 동시성 환경에서도 데이터가 깨지지 않는가"를 고민합니다. 여러분도 이제 @Transactional 뒤에 숨겨진 거대한 메커니즘을 이해했으니, 시니어의 시야를 가지게 된 것입니다.