RDBMS Atomicity: 여러 변경을 하나의 결과로 처리하기
TOC
Overview
앞선 글에서는 ACID가 데이터베이스 트랜잭션의 보장 범위를 설명하는 기준이라는 점을 살펴봤다.
이번 글에서는 ACID의 첫 번째 속성인 Atomicity를 다룬다. Atomicity는 여러 데이터 변경을 하나의 작업 단위로 묶어 하나의 결과로 처리하는 성질이다.
Atomicity란 무엇인가
Atomicity는 트랜잭션 안의 변경을 부분적으로 남기지 않는 성질이다.
트랜잭션 시작
├─ 변경 1
├─ 변경 2
└─ 변경 3
트랜잭션 종료
├─ COMMIT → 변경 1, 2, 3 확정
└─ ROLLBACK → 변경 1, 2, 3 취소
변경 3에서 오류가 발생했다고 DB가 자동으로 전체 롤백하는 것은 아니다. 트랜잭션이 계속 유지된다면 클라이언트는 앞선 변경을 COMMIT할 수도 있고, 전체를 ROLLBACK할 수도 있다.
Atomicity는 SQL 하나가 항상 성공하거나 실패한다는 의미가 아니다. 여러 SQL을 하나의 트랜잭션으로 묶고, 클라이언트가 선택한 종료 결과를 그 변경 전체에 적용한다는 의미다.
업무적으로 하나의 작업으로 묶은 변경이라면, 애플리케이션은 일부만 남기지 않도록 오류를 판단한 뒤 ROLLBACK을 요청해야 한다.
트랜잭션 경계와 Atomicity
Atomicity를 얻으려면 서로 성공 여부가 연결된 변경을 같은 트랜잭션에 포함해야 한다.
입력 검증
↓
트랜잭션 시작
├─ 대출 잔액 차감
└─ 상환 이력 저장
트랜잭션 종료
↓
외부 시스템 처리
대출 잔액 차감과 상환 이력 저장은 상환이라는 하나의 업무를 구성한다. 따라서 둘 중 하나만 성공하면 안 되고 같은 트랜잭션으로 처리해야 한다.
반대로 입력 검증이나 외부 시스템 호출을 반드시 같은 트랜잭션에 넣을 필요는 없다. 사용자 입력을 기다리거나 외부 API를 호출하는 동안 트랜잭션을 열어두면 커넥션과 락을 불필요하게 점유할 수 있다.
예시: 대출 상환이 중간에 실패한다면
각 변경을 별도의 트랜잭션으로 처리하면 다음과 같은 문제가 발생할 수 있다.
1. 대출 잔액 10,000원 차감 성공
2. 상환 이력 저장 실패
3. 잔액은 줄었지만 상환 근거가 없음
잔액과 이력을 각각 별도의 트랜잭션으로 처리했기 때문에 일부 변경만 커밋된 것이다.
반면 두 변경을 하나의 트랜잭션으로 묶으면 이력 저장이 실패할 때 잔액 차감도 취소된다.
START TRANSACTION;
UPDATE loan
SET remaining_amount = remaining_amount - 10000
WHERE loan_id = 1;
INSERT INTO repayment_history(loan_id, amount)
VALUES (1, 10000);
COMMIT;
두 번째 SQL에서 오류가 발생하고 전체 상환을 실패로 판단하면 COMMIT 대신 ROLLBACK을 수행한다.
ROLLBACK;
그러면 대출 잔액은 차감 전 상태로 돌아간다. 상환 처리가 실패했는데 잔액만 변경된 상태를 방지할 수 있다.
트랜잭션 밖의 작업
Atomicity는 하나의 데이터베이스 트랜잭션 안에서만 적용된다.
DB 변경과 외부 API 호출을 하나의 트랜잭션처럼 묶을 수는 없다. DB를 롤백하더라도 이미 외부 시스템에 전달된 요청까지 자동으로 되돌아가지는 않는다.
DB 저장과 메시지 발행을 함께 보장해야 하는 경우에는 Outbox나 재시도 같은 별도 설계가 필요하다.
Conclusion
Atomicity는 하나의 업무를 구성하는 데이터베이스 변경을 모두 성공시키거나 모두 취소하는 성질이다.
- 서로 성공 여부가 연결된 변경은 같은 트랜잭션에 포함한다.
- 일부 변경만 커밋되면 업무의 정합성이 깨질 수 있다.
- 외부 API와 메시지 발행은 DB 트랜잭션만으로 함께 롤백할 수 없다.
Atomicity는 트랜잭션 안의 변경을 하나의 결과로 처리하지만, 트랜잭션 밖의 작업까지 원자적으로 만들어주지는 않는다.