
⚡ 한 줄 요약: 트랜잭션은 데이터의 무결성을 보장하는 최소 작업 단위이며, ACID 원칙과 격리 수준 설정을 통해 시스템의 성능(동시성)과 데이터의 정합성 사이에서 최적의 균형을 찾는 과정입니다.
우리가 쇼핑몰에서 결제 버튼을 누르거나 친구에게 송금할 때, 눈에 보이지 않는 수많은 쿼리가 동시에 움직입니다.
만약 출금은 성공했는데 입금 직전에 서버가 꺼진다면 어떻게 할까요?
이런 끔찍한 상황을 막아주는 최후의 보루가 바로 트랜잭션(Transaction)입니다.
🧐 Why:
🎯 Goal:
트랜잭션은 데이터베이스에서 하나의 논리적 작업 단위를 의미합니다.
우리가 흔히 접하는 '계좌 이체'를 예로 들어볼까요?
이체라는 작업은 사용자 입장에서는 하나의 행위이지만, 실제 DB 내부에서는 여러 개의 쿼리로 나누어 작업이 이루어집니다.
여기서 핵심은 이 두 쿼리가 하나의 논리적인 단위로 묶여야 한다는 것입니다. 그래서 START TRANSACTION 명령어로 이 작업들을 하나로 묶어주는 것이죠.
트랜잭션 내의 모든 작업이 성공해야만 최종적으로 데이터베이스에 반영(COMMIT)되고, 만약 하나라도 실패하면 전체 작업이 취소(ROLLBACK)되어 이전 상태로 돌아갑니다.
이는 데이터의 일관성을 유지하기 위해 필요한 장치입니다.
트랜잭션이 안전하게 수행되기 위해서는 네 가지 핵심 특성을 만족해야 하는데, 각 특성의 앞 글자를 따서 ACID라고 부릅니다.
Atomicity (원자성): 트랜잭션 내의 모든 작업은 '모두 성공'하거나 '모두 실패'해야 합니다. 내 계좌에서 돈만 빠지고 상대방 계좌에는 입금되지 않는 상황이 발생하면 안 되기 때문입니다.
Consistency (일관성): 트랜잭션 실행 전후의 데이터베이스 상태는 항상 일관된 규칙을 유지해야 합니다. A가 B에게 5만 원을 보냈다면, 전체 잔고의 총합은 이체 전후가 동일해야 합니다.
Isolation (고립성): 동시에 여러 트랜잭션이 실행될 때, 서로 영향을 주지 않고 독립적으로 처리되어야 합니다. 같은 상품을 두 사람이 동시에 결제할 때, 재고가 하나뿐이라면 순서를 조절하여 한 사람만 성공하도록 보장해야 하는 것과 같습니다.
Durability (지속성): 트랜잭션이 성공적으로 완료(COMMIT)되었다면, 그 결과는 시스템 장애나 전원 차단이 발생하더라도 영구적으로 보존되어야 합니다.
트랜잭션의 흐름은 작업의 성공 여부에 따라 두 갈래로 나뉩니다.
정상 상황 (COMMIT): 모든 SQL문이 정상적으로 완료되면 COMMIT 명령을 실행합니다. 이때 비로소 작업 내용이 데이터베이스에 확정적으로 반영됩니다.
에러 상황 (ROLLBACK): 만약 홍길동의 계좌에서 돈을 뺐는데, '김철수 계좌가 존재하지 않음'과 같은 서버 오류가 발생하면 어떻게 될까요? 이때는 ROLLBACK 명령을 실행하며 이미 수행된 출금 작업까지 모두 취소하고 트랜잭션 시작 전 상태로 되돌립니다.
💻 참고
실무에서는 단순히
START와COMMIT을 쓰는 것을 넘어, 프레임워크(예: Spring의@Transactional)가 이 트랜잭션을 어떻게 관리하는지, 그리고 서비스의 규모에 따라 어느 정도의 격리 수준을 적용할지 결정하는 역량이 중요합니다.
💡 비유로 이해하기
트랜잭션은 '연필로 써놓은 메모'와 같습니다.
START TRANSACTION이후에 적는 내용들은 언제든 지울 수 있는 연필 메모 상태이고,COMMIT버튼을 눌렀을 때 비로소 그 메모가 '지워지지 않는 잉크'로 변하여 종이에 박히는 것가 같습니다.
트랜잭션은 실행되는 동안 여러 가지 상태를 거치며 진행됩니다.
일반적으로 아래 5가지 상태를 가집니다:
Active (활성 상태)
START TRANSACTION; 명령을 날린 직후의 상태로 이해하면 됩니다.Partially Committed (부분 완료 상태)
COMMIT)되지 않은 상태입니다.Committed (완료 상태)
Failed (실패 상태)
Aborted (중단 상태)
ROLLBACK 연산을 수행해 트랜잭션 시작 전의 상태로 완전히 되돌아간 상태입니다.성공 시나리오 (Committed)
Active
Partially Committed
Committed
COMMIT을 실행하여 최종적으로 성공 상태가 됩니다.실패 시나리오 (Aborted)
Active
Failed
3 Aborted
ROLLBACK을 실행하여 이미 처리된 출금 작업까지 모두 취소되어 중단 상태가 됩니다.실패(Failed)와 중단(Aborted)의 차이
지속성 보장의 시점:
지연 갱신 방식(Deferred Update)은 트랜잭션이 실행되는 동안의 작업 내용을 실제 DB가 아닌 로그(Log)에만 기록하는 방식입니다.
실제 데이터베이스에 반영되는 시점은 오직 COMMIT 명령이 떨어진 이후입니다.
여기서 중요한 포인트가 있습니다.
커밋 명령은 "이 트랜잭션을 확정하겠다"는 선언인데, 이 선언 직후 아주 짧은 찰나에 시스템 장애가 발생할 수 있습니다.
선언은 했지만, 실제로 디스크에 물리적으로 기록되지 못한 상태가 될 수 있는 것이죠.
이런 경우에 대비해 사용하는 것이 바로 Redo(재실행) 연산입니다.
원리
COMMIT은 완료되었지만 실제 DB에 반영되지 못한 내역을 찾아냅니다.작업
장애 대응
반대로 즉시 갱신 방식(Immediate Update)은 쿼리가 실행될 때마다 DB에 바로바로 반영하는 방식입니다.
주로 일부 오래된 시스템에서 사용하는 방식이죠.
이 방식은 장애가 났을 때 처리가 조금 더 복잡합니다.
문제
해결 (Undo)
지연 갱신 방식에서도 Undo가 필요한가요?
로그에는 무엇이 적혀 있나요?
💻 참고
실무에서 로그 파일(Write-Ahead Log, WAL)의 중요성은 아무리 강조해도 지나치지 않습니다. 우리가 흔히 쓰는 MySQL의 InnoDB 엔진도 이 WAL 메커니즘을 사용하여 성능과 안전성을 동시에 잡습니다.
"데이터는 로그에 먼저 써지고, 나중에 DB 파일에 써진다"는 대원칙을 이해하고 있다면, 분산 시스템이나 클라우드 환경의 데이터 복구 로직도 쉽게 파악할 수 있습니다.
하나의 트랜잭션 내에서 작업 중인 데이터에 대해 다른 트랜잭션의 접근 범위를 결정하는 설정입니다.
쉽게 말해, 내가 어떤 데이터를 고치고 있을 때 남이 그 데이터를 어느 정도까지 볼 수 있게 할 것인가를 정하는 기준입니다.
이 설정은 정확성과 동시성 사이의 줄타기와 같습니다.
격리 수준이 낮다
격리 수준이 높다
가장 낮은 격리 수준인 READ UNCOMMITTED부터 가장 높은 수준인 SERIALIZABLE까지 총 4단계가 존재합니다.
표준 SQL에서 정의하는 격리 수준은 아래와 같으며, 실무에서 사용하는 대부분의 DBMS(MySQL, PostgreSQL 등)는 REPEATABLE READ나 READ COMMITTED를 기본값으로 채택하고 있습니다.
| 격리 수준 | 설명 | 문제 발생 가능성 |
|---|---|---|
| READ UNCOMMITTED | 커밋되지 않은 데이터도 읽기 허용 (가장 낮음) | Dirty Read 발생 가능 |
| READ COMMITTED | 커밋된 데이터만 읽기 허용 | Non-repeatable Read 발생 가능 |
| REPEATABLE READ | 트랜잭션 내 동일 데이터 조회 시 항상 같은 값 반환 | Phantom Read 발생 가능 |
| SERIALIZABLE | 트랜잭션을 순차적으로 실행 (가장 높음/엄격) | 성능 저하 우려 |
SERIALIZABLE은 모든 문제를 해결한다?
낮은 격리 수준은 무조건 동시성이 좋은가?
READ UNCOMMITTED가 가장 낮고 SERIALIZABLE이 가장 높은 단계입니다.💡 비유로 이해하기
격리 수준은 도서관 열람실의 개인 공간과 같습니다.
칸막이가 아예 없으면(낮으면) 옆 사람과 대화하며 정보를 빨리 주고받을 수 있지만 방해를 받기 쉽고, 완전히 폐쇄된 1인실(높으면)은 집중도는 완벽하지만 공간 활용 효율(동시성)이 떨어지는 것과 같습니다.
다른 트랜잭션이 커밋하지 않은 데이터를 읽을 수 있게 허용하는 가장 낮은 격리 수준입니다.
이 단계의 핵심 특징은 특정 트랜잭션에서 변경한 내용이 아직 확정(COMMIT)되지 않았더라도 다른 트랜잭션이 그 변경된 값을 바로 볼 수 있다는 점입니다.
구체적인 시나리오를 통해 데이터 정합성이 어떻게 깨지는지 살펴보겠습니다.
트랜잭션 A (수정 중)
UPDATE accounts SET balance = 0 WHERE name = '홍길동';트랜잭션 B (조회 중)
결과
반전(ROLLBACK)
ROLLBACK을 하면, 홍길동의 잔액은 수정 전 원래 금액으로 남게 됩니다.문제 발생
이처럼 다른 트랜잭션이 커밋하지 않은 데이터를 읽는 현상을 더티 리드(Dirty Read)라고 하며, 이는 데이터의 정합성에 심각한 문제를 야기합니다.
쿼리가 실행되면 무조건 DB에 반영되는 것 아닌가요?
COMMIT을 하기 전까지는 '확정'된 상태가 아닙니다.롤백하면 읽어간 데이터도 취소되나요?
커밋 완료된 데이터만 읽을 수 있게 허용하여 Dirty Read를 방지하는 격리 수준입니다.
1단계(READ UNCOMMITTED)의 치명적인 약점이었던 Dirty Read를 해결하기 위해, 오직 커밋된 데이터만 읽도록 제한합니다.
하지만 이 단계에서도 Non-Repeatable Read라는 정합성 문제가 여전히 존재합니다.
아래 시나리오를 통해 트랜잭션 B의 입장에서 어떤 일이 벌어지는지 살펴봅시다.
트랜잭션 B 시작
트랜잭션 A 개입
UPDATE)합니다.트랜잭션 B 재조회(중간 단계)
트랜잭션 A 커밋
트랜잭션 B 최종 재조회
결과적으로 트랜잭션 B 입장에서는 동일한 트랜잭션 내에서 똑같은 쿼리를 실행했는데 조회 결과가 달라지는 현상을 겪게 됩니다.
이를 Non-Repeatable Read라고 부릅니다.
커밋된 것만 읽으니까 안전한 것 아닌가요?
Dirty Read vs Non-Repeatable Read
💻 참고
Oracle이나 PostgreSQL 같은 주요 RDBMS가 이 READ COMMITTED를 기본 격리 수준으로 채택하고 있습니다.
웹 애플리케이션의 일반적인 비즈니스 로직에서는 큰 문제가 되지 않는 경우가 많지만, 데이터의 엄격한 일관성이 필요한 금융권 통계나 정산 로직에서는 이 Non-Repeatable Read을 방어하기 위해 격리 수준을 높이거나 명시적인 락(Lock)을 활용하기도 합니다.
💡 비유로 이해하기
식당 메뉴판을 보고 있는데, 내가 메뉴를 고르는 사이 주방장이 메뉴판의 가격을 수정하고 커밋(확정)한 상황입니다.
처음 메뉴판을 봤을 땐 1만 원이었는데, 주문하려고 다시 보니 1.2만원으로 변해 있는 것과 같습니다.
트랜잭션 내에서 동일한 데이터를 조회했을 때 항상 같은 값이 반환됨을 보장하는 격리 수준입니다.
2단계(READ COMMITTED)에서는 조회 도중 남이 커밋을 하면 값이 바뀌는 현상이 있었죠.
이를 해결하기 위해 3단계인 REPEATABLE READ는 트랜잭션이 시작되기 전에 이미 커밋된 내용만 조회하는 방식을 취합니다.
가장 큰 특징은 트랜잭션 B의 커밋이 트랜잭션 A의 시작보다 나중에 실행되었지만, 트랜잭션 A는 데이터 조회 시 그 커밋 내용을 절대 반영하지 않는다는 점입니다.
즉, 트랜잭션 A는 자신이 시작된 시점의 '데이터 스냅샷'을 끝까지 고수합니다.
기본적으로 데이터베이스는 처음 SELECT 했던 행에 대해 내부적으로 락(Lock)을 걸어둡니다.
이를 통해 내가 읽고 있는 동안 다른 트랜잭션이 그 행을 수정하거나 삭제하지 못하도록 물리적으로 막아버리는 것이죠.
하지만 여기서 치명적인 빈틈이 하나 생깁니다.
바로 새로운 행의 삽입(INSERT)은 막지 못한다는 점입니다.
이유는 단순합니다.
아직 존재하지 않는 행에는 락을 걸 수 없기 때문입니다.
이 때문에 발생하는 현상이 바로 팬텀 리드(Phantom Read)입니다.
기존 데이터는 락 덕분에 변하지 않지만, 새로운 데이터가 삽입되면서 마치 유령(Phantom)처럼 이전에 없던 데이터가 조회 결과에 나타나게 됩니다.
락을 걸면 INSERT도 막히는 거 아닌가요?
트랜잭션이 길어지면 어떻게 되나요?
💻 참고
우리가 실무에서 가장 많이 쓰는 MySQL(InnoDB 엔진)의 기본 격리 수준이 바로 이 REPEATABLE READ입니다.
흥미로운 점은 InnoDB는 Next-Key Lock이라는 기술을 써서 이 단계에서도 팬텀 리드를 어느 정도 방어해 준다는 사실이죠.
모든 정합성 문제를 방지하는 가장 강력한 격리 수준으로, 각 트랜잭션이 마치 하나씩 순서대로 실행되는 것처럼 동작합니다.
4단계인 SERIALIZABLE은 가장 엄격한 수준입니다.
앞선 단계들에서 발생했던 Dirty Read, Non-Repeatable Read, Phantom Read를 모두 완벽하게 방지하죠.
하지만 트랜잭션을 직렬화하여 처리하기 때문에 성능은 가장 느립니다.
안전의 기반, ACID
상태와 복구의 마법
격리 수준의 트레이드오프
READ COMMITTED와 REPEATABLE READ 중 무엇을 선택할지는 서비스의 트래픽과 데이터의 중요도에 따라 결정해야 하는 고도의 엔지니어링 의사결정입니다.현대적 DB의 지혜
SERIALIZABLE이 가장 안전하지만 실무에선 거의 쓰지 않습니다.REPEATABLE READ에서도 기술적(Next-Key Lock)으로 팬텀 리드를 막아주는 엔진의 특성을 이해하는 것이 중요합니다.