5장. 전부 아니면 전무 — 원자성

만화로 보는 요약 — 먼저 읽어보세요
결제 버튼 뒤에는 여러 작업이 묶여 있다 → 하나라도 실패하면 부분 성공이 사고가 된다 → 원자성은 전부 되거나 전부 취소되게 만든다.
면접 실전 질문: ① 원자성이 막는 부분 실패 ② ROLLBACK과 undo 로그의 역할 ③ @Transactional 경계가 의미하는 것
배경 — 약속을 네 조각으로 쪼개다
제1부에서 우리는 트랜잭션이 “안전을 보장한다”고 말해 왔습니다. 그런데 “안전”이 정확히 뭘 뜻하는지는 미뤄뒀죠. 이제 그 약속을 열어볼 차례예요.
1983년, 두 연구자(테오 헤르더Härder와 안드레아스 로이터Reuter)가 트랜잭션이 지켜야 할 성질을 네 글자로 정리합니다. ACID. 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability). 이 네 개가 합쳐져 우리가 “안전”이라 부르는 것의 정체예요. 제2부는 이 네 글자를 하나씩 뜯어봅니다. 그 첫 글자, A부터요.
스토리 — 장바구니의 결제 버튼
쇼핑몰에서 결제 버튼을 누르는 순간, 서버에서는 사실 여러 일이 한꺼번에 벌어집니다.
- 재고를 하나 깎고
- 결제를 승인하고
- 사용한 쿠폰을 소진 처리하고
- 주문 레코드를 만든다
자, 3번까지 성공했는데 4번 직전에 서버가 죽으면 어떻게 될까요? 재고는 깎였고, 돈은 빠져나갔고, 쿠폰도 썼는데 — 주문은 없습니다. 고객은 돈을 냈지만 산 게 없어요. 반대로 결제(2번)만 실패했는데 재고(1번)는 이미 깎였다면? 팔리지도 않은 물건의 재고가 증발합니다.
이게 1장 회계사의 “반만 적힌 장부”, 2장의 “반쯤 부서진 파일”과 정확히 같은 문제예요. 작업의 일부만 반영되는 부분 실패. 규모만 커졌을 뿐, 싸우는 상대는 500년째 같습니다.
원자성은 이 문제에 칼같이 답합니다. “네 개의 작업을 하나로 묶는다. 전부 되거나, 하나도 안 되거나. 중간은 없다.” 원자(atom)가 더 못 쪼개는 단위이듯, 트랜잭션도 쪼개지지 않는 하나의 덩어리로 취급하는 거죠.
핵심 — “안 되면 없던 일로” 하는 힘
원자성을 코드로 보면 익숙합니다.
START TRANSACTION;
UPDATE product SET stock = stock - 1 WHERE id = 42; -- 1. 재고 차감
INSERT INTO payment(order_no, amount) VALUES (1001, 20000); -- 2. 결제
UPDATE coupon SET used = true WHERE id = 55; -- 3. 쿠폰 소진
INSERT INTO orders(order_no, user_id) VALUES (1001, 7); -- 4. 주문 생성
COMMIT; -- 네 개가 전부 확정되거나,
-- 도중 실패를 감지하면 앱/프레임워크가 ROLLBACK을 불러 넷 다 없던 일로핵심은 마지막 주석입니다. 3번에서 문제가 터지면, DB는 이미 실행한 1·2번까지
되감아 트랜잭션 시작 전 상태로 돌려놓습니다. 이게 ROLLBACK이에요. 그럼
DB는 이걸 어떻게 되감을까요? 실행하기 전에 “원래 값이 뭐였는지”를 따로 기록(undo
로그) 해두기 때문입니다. 이렇게 “되돌릴 정보를 로그에 먼저 적어두는” 원리(undo와
그 짝인 redo)는 15장에서 제대로 파헤칩니다. 지금은 “되돌릴 수 있는 이유는, 되돌릴
정보를 미리 적어두기 때문”만 기억하세요.
실무에서는 이 경계를 SQL로 직접 긋기보다 프레임워크에 맡깁니다. Spring이라면
@Transactional 한 줄이죠.
@Transactional
public void checkout(Order order) {
productRepository.decreaseStock(order.getProductId()); // 1
paymentClient.approve(order.getPayment()); // 2
couponRepository.use(order.getCouponId()); // 3
orderRepository.save(order); // 4
}이 메서드 안에서 예외가 터지면, Spring이 지금까지의 작업을 통째로 롤백합니다. 개발자는 “이 메서드는 하나의 단위다”라고 선언만 하고, “어떻게 되감을지”는 DB와 프레임워크가 알아서 하는 거예요. 1장 회계사가 손으로 지키던 규율을, 이제 애노테이션 한 줄이 대신 지킵니다.
다만 여기엔 함정이 하나 숨어 있어요. @Transactional은 아무 예외에나 롤백하는 게
아닙니다. 기본값은 unchecked 예외(RuntimeException)에만 롤백하고, checked 예외는
오히려 그냥 커밋해 버립니다. 롤백될 줄 알고 checked 예외를 던졌다가 데이터가 반쯤
저장되는 것 — 주니어가 가장 자주 밟는 지뢰죠. 이 함정은 17장에서 제대로 다룹니다.
⚠️ 함정 미리보기: 위 코드의 2번
paymentClient.approve()는 외부 결제사 호출 입니다. 트랜잭션 안에서 외부 API를 부르는 건 사실 위험해요(커넥션을 오래 잡고, 롤백해도 외부 결제는 안 돌아오죠). 이 문제는 17장에서 정면으로 다룹니다.
정리
- 트랜잭션의 “안전” = ACID 네 글자(헤르더·로이터, 1983). 제2부는 이걸 하나씩 뜯는다.
- 원자성(Atomicity) = “전부 되거나, 하나도 안 되거나.” 여러 작업을 쪼개지지 않는 하나의 단위로 묶어 부분 실패를 원천 차단한다.
- 되돌리는 힘의 비결은 미리 적어둔 undo 정보 → 실패 시
ROLLBACK으로 시작 전 상태로. (“되돌릴 정보를 로그에 먼저 적는” 원리 = 15장) - 실무에선 Spring
@Transactional한 줄로 이 경계를 선언한다. 단, 트랜잭션 안 외부 호출은 위험(17장 복선).
생각해볼 질문: 원자성은 “전부 아니면 전무”를 보장합니다. 그런데 네 작업이 전부 성공했다고 쳐도, 그 결과가 말이 안 되는 상태(예: 재고가 음수)라면요? “전부 됐다”와 “올바르다”는 같은 걸까요? (6장에서 이 미묘한 차이를 팝니다.)