F-LAB JAVA · 6주차 · Phase 6 · 트랜잭션과 ACID
★ 깊이 파기 — ACID 의 첫 글자, "전부 아니면 전무"
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
Atomicity (원자성) 는 트랜잭션의 모든 연산이 모두 성공해서 commit 되거나 모두 실패해서 rollback 되어 마치 일어나지 않은 것처럼 되어야 한다는 성질로, 자바의 Atomic (단일 변수의 연산 원자성) 과 달리 DB Atomicity 는 여러 작업의 묶음의 원자성이며, 분산 환경에서 부분 성공을 허용하는 시나리오에는 Saga 패턴을 쓴다.
Atomicity (원자성) 는 트랜잭션의 모든 연산이 모두 성공하거나 모두 실패해야 한다 는 ACID 의 첫 성질이다.
계좌 이체 시나리오에서 — A 출금 ✅, B 입금 ❌ (장애) 가 되면 A 의 돈이 사라지는 듯 보이지만 NO — 전체가 rollback 되어 A 의 출금도 취소되고 처음 상태로 돌아간다.
4주차의 자바AtomicLong.incrementAndGet()은 하나의 변수에 대한 연산의 원자성 이지만, DB Atomicity 는 여러 SQL 작업의 묶음 의 원자성이라 단위가 다르다.
원자성이 깨지면 "A 돈은 사라졌는데 B 는 못 받음" 같은 데이터 무결성 사고가 발생하므로, 분산 환경처럼 한 트랜잭션이 불가능한 경우엔 Saga 패턴 (각 단계가 commit + 보상 트랜잭션으로 복구) 으로 부분 성공을 허용한다.
원자성 = 결혼식 서약:
원자성 (전부 또는 전무):
- 신부 "예" + 신랑 "예"
- 둘 다 → 결혼 성립 (commit)
- 한쪽 "아니오" → 결혼 X (rollback)
- "신부만 예" 상태 X (중간)
계좌 이체:
- A 출금 + B 입금
- 둘 다 → 이체 성립
- 입금 실패 → 출금도 취소
- "A 만 출금" X
자바 Atomic vs DB Atomicity:
- 자바: 한 변수의 한 연산 (count++)
- DB: 여러 작업의 묶음 (이체 = 2개 SQL)
- 단위 다름
원자성 깨지면:
- "신부만 결혼 X 신랑은 결혼"
- 누구도 받지 못한 돈
- 데이터 무결성 사고
Saga 패턴 (분산):
- 결혼식 → 신혼여행 → 신혼집
- 각자 commit + 보상 (취소)
- 중간 실패 시 보상으로 복구
DB 가 보장 (undo log):
- 변경 전 기록
- rollback 시 복원
→ Atomicity = 전부 or 전무, 자바 Atomic 과 단위 다름, 분산엔 Saga.
1. Atomicity 정의
2. 전부 성공 / 전부 실패
3. 계좌 이체 시나리오
4. 부분 실패 처리
5. 자바 Atomic 과 차이
6. 원자성 깨지면 사고
7. Saga 패턴 (분산)
8. DB 가 보장 (undo log)
9. Spring 의 원자성
Atomicity (원자성):
"Atom" = 더 이상 쪼갤 수 없는 단위.
트랜잭션:
- 더 이상 쪼개지지 않는 작업 묶음
- 전체가 한 단위
핵심 원칙:
"모두 성공 or 모두 실패"
- 부분 성공 X
- 부분 실패 X
- 전부 또는 전무 (All or Nothing)
트랜잭션 vs SQL:
단일 SQL:
- DB 가 자체 원자성 (한 SQL 자체)
트랜잭션 (여러 SQL):
- 묶어서 한 단위
- 명시적 트랜잭션 필요
Atomicity (ILIC)
ILIC 배송 예약:
- 예약 INSERT
- 배송 INSERT
- 운임 INSERT
→ 한 트랜잭션 (원자적)
- 모두 성공 → 예약 성립
- 하나 실패 → 모두 취소 (rollback)
- "예약은 있는데 배송 없음" X (원자성)
Atomicity (원자성) 의 정의는?
답:
1. Atomicity:
핵심:
단일 SQL:
트랜잭션:
두 결과:
1. 전부 성공:
- 모든 작업 적용
- Commit
2. 전부 실패:
- 어느 것도 적용 X
- Rollback
중간 상태 X:
외부 관점:
- 트랜잭션 진행 중: 안 보임 (격리)
- 끝: 전부 또는 전무
- "절반 적용" 상태 X
→ 외부 일관성
가능한 종료:
- 정상 commit (전부 성공)
- 명시적 rollback
- 예외 발생 → rollback
- 제약 위반 → rollback
- 시스템 다운 → 복구 시 rollback
→ 항상 전부 또는 전무
-- 트랜잭션 흐름
BEGIN;
SQL1; -- 작업 1
SQL2; -- 작업 2
SQL3; -- 작업 3
-- 두 갈래:
-- 정상: COMMIT (모두 적용)
-- 실패: ROLLBACK (모두 취소)
COMMIT; -- 또는 ROLLBACK;
// 전부 성공 / 전부 실패 (ILIC)
@Service
public class BookingService {
@Transactional
public void createBooking(BookingRequest req) {
// 작업 1
Booking booking = bookingDao.add(req.toBooking());
// 작업 2
Shipment shipment = new Shipment();
shipment.setBookingId(booking.getId());
shipmentDao.add(shipment);
// 작업 3
BigDecimal freight = freightCalculator.calculate(shipment);
freightDao.save(shipment.getId(), freight);
// 두 갈래:
// 정상 → commit (3개 모두 적용)
// 예외 → rollback (3개 모두 취소)
// 중간 상태 X (Booking 있는데 Shipment 없음 = 절대 X)
}
BookingDao bookingDao;
ShipmentDao shipmentDao;
FreightCalculator freightCalculator;
FreightDao freightDao;
}
class BookingRequest { Booking toBooking() { return null; } }
class Booking { Long getId() { return null; } }
class Shipment { Long getId() { return null; } void setBookingId(Long id) {} }
class BookingDao { Booking add(Booking b) { return null; } }
class ShipmentDao { void add(Shipment s) {} }
class FreightDao { void save(Long id, java.math.BigDecimal f) {} }
class FreightCalculator { java.math.BigDecimal calculate(Shipment s) { return null; } }
"모두 성공 or 모두 실패" 의 의미는?
답:
1. 두 결과:
중간 X:
종료:
흐름:
계좌 이체:
A → B 10만원:
1. A 출금 -10만
2. B 입금 +10만
원자적:
- 둘 다 성공 → 이체 OK
- 어느 하나 실패 → 전체 취소
-- 정상 흐름
BEGIN;
UPDATE accounts SET balance = balance - 100000 WHERE id = 'A';
-- 성공
UPDATE accounts SET balance = balance + 100000 WHERE id = 'B';
-- 성공
COMMIT;
-- A -10만, B +10만 영구 적용
-- 실패 흐름
BEGIN;
UPDATE accounts SET balance = balance - 100000 WHERE id = 'A';
-- 성공
UPDATE accounts SET balance = balance + 100000 WHERE id = 'B';
-- 실패! (B 계좌 없음 등)
ROLLBACK;
-- A 의 출금도 자동 취소 → 처음 상태
사용자 관점:
성공 시:
- A 잔액: 90만 (확정)
- B 잔액: 110만 (확정)
실패 시:
- A 잔액: 100만 (그대로)
- B 잔액: 100만 (그대로)
- "이체 실패" 메시지
→ 중간 상태 절대 X
// 계좌 이체 (ILIC, 결제 시스템 가정)
@Service
public class PaymentService {
@Transactional
public void transfer(Long from, Long to, BigDecimal amount) {
// 1. 출금
accountDao.decrease(from, amount);
// ↑ 여기까지만 되면 from 의 돈만 사라짐
// 2. 입금
accountDao.increase(to, amount);
// ↑ 여기 실패하면?
// → 전체 rollback
// → from 의 출금도 취소
// → 둘 다 처음 상태
// 정상 종료 → commit (둘 다 적용)
}
AccountDao accountDao;
}
class AccountDao {
void decrease(Long id, java.math.BigDecimal amount) {}
void increase(Long id, java.math.BigDecimal amount) {}
}
계좌 이체 시나리오에서 원자성은?
답:
1. 시나리오:
정상:
실패:
사용자:
부분 실패:
세션1: A 출금 ✅
세션1: B 입금 ❌ (장애)
→ A 의 돈은 사라짐?
→ NO! (원자성)
rollback 동작:
실패 감지:
- DB 가 자동
- 또는 예외 → rollback 호출
복원:
- undo log 사용
- A 의 변경 취소
- B 도 영향 X (이미 실패)
외부 관점:
실패한 트랜잭션:
- 아무 일도 안 일어남
- 시작 전과 동일
- "절대 시도 안 한 것처럼"
→ 마치 없던 일
시스템 장애:
진행 중 DB 다운:
- 자동 rollback (재시작 시)
- undo log 복원
- 원자성 유지
→ 어떤 상황에서도 보장
// 부분 실패 처리 (ILIC)
@Service
public class BookingService {
@Transactional
public void createBooking(Booking b) {
bookingDao.add(b); // ✅ 성공
// 만약 여기서 장애:
// - 시스템 다운
// - 네트워크 끊김
// - 예외 발생
shipmentDao.add(...); // ❌ 미실행 (장애)
// 결과:
// → bookingDao.add 도 rollback
// → Booking 사라짐 (처음 상태)
// → "Booking 만 있고 Shipment 없음" 절대 X
// 원자성 보장 (DB 가)
}
BookingDao bookingDao;
ShipmentDao shipmentDao;
}
class Booking {}
class BookingDao { void add(Booking b) {} }
class ShipmentDao { void add(Object o) {} }
출금만 되고 입금 실패 시 동작은?
답:
1. 부분 실패:
rollback:
외부:
장애 시도:
단위 차이:
자바 Atomic (4주차):
- 하나의 변수
- 하나의 연산
- 예: count.incrementAndGet()
DB Atomicity:
- 여러 작업 (SQL 들)
- 하나의 트랜잭션
- 예: UPDATE A; UPDATE B;
// 4주차 자바 Atomic
AtomicLong count = new AtomicLong();
count.incrementAndGet(); // 단일 변수, 단일 연산 원자적
// CAS (Compare-And-Swap)
// - 한 변수의 read-modify-write
// - 멀티스레드 안전
-- 6주차 DB Atomicity
BEGIN;
UPDATE A SET ... WHERE ...; -- 작업 1
UPDATE B SET ... WHERE ...; -- 작업 2
INSERT INTO C ...; -- 작업 3
COMMIT;
-- 3개 작업이 하나의 단위
| 항목 | 자바 Atomic | DB Atomicity |
|---|---|---|
| 단위 | 한 변수 한 연산 | 여러 SQL 묶음 |
| 적용 | 메모리 | 영구 저장소 |
| 보장 도구 | CAS | 트랜잭션 |
| 멀티 | 스레드 | 세션 |
// 자바 Atomic vs DB Atomicity (ILIC)
// 4주차 자바 Atomic
@Service
public class ShipmentCounter {
private final AtomicLong count = new AtomicLong();
public void increment() {
count.incrementAndGet(); // 메모리 원자성
// 단일 변수, 단일 연산
}
}
// 6주차 DB Atomicity
@Service
public class BookingService {
@Transactional
public void createBooking(Booking b) {
// 여러 SQL 의 원자성
bookingDao.add(b); // 작업 1
shipmentDao.add(...); // 작업 2
freightDao.save(...); // 작업 3
// 3개 모두 한 단위 (DB Atomicity)
}
BookingDao bookingDao;
ShipmentDao shipmentDao;
FreightDao freightDao;
}
// → 같은 "원자성" 이지만 단위가 다름
class Booking {}
class BookingDao { void add(Booking b) {} }
class ShipmentDao { void add(Object o) {} }
class FreightDao { void save(Object o) {} }
4주차 자바 Atomic 과 DB Atomicity 차이는?
답:
1. 자바 Atomic:
DB Atomicity:
단위:
도구:
원자성 깨지면:
계좌 이체:
- A 출금만 (B 입금 X)
- A 의 돈 사라짐
- 누구도 못 받음
→ 데이터 사고
사고 유형:
1. 데이터 무결성 깨짐
- 한쪽만 적용
2. 회계 불일치
- 차변 ≠ 대변
3. 비즈니스 오류
- 주문 있는데 결제 X
- 결제 했는데 주문 X
ILIC 사고 (원자성 깨지면):
배송 예약:
- 예약 INSERT ✅
- 배송 INSERT ❌
→ 예약은 있는데 배송 정보 없음
→ 사용자: 예약했는데 운송 못 함
→ 비즈니스 사고
복구의 어려움:
원자성 깨진 사고:
- 수동 복구 필요
- 어디까지 적용? 추적 어려움
- 시간 오래
- 운영 부담
→ 사전 방지 (트랜잭션) 중요
// 원자성 깨지면 (ILIC, @Transactional 없으면)
@Service
public class BadBookingService {
// ❌ @Transactional 없음
public void createBooking(Booking b) {
bookingDao.add(b); // ✅
// 만약 여기서 예외 (네트워크/DB 등):
shipmentDao.add(...); // ❌ (안 됨)
// 결과:
// - Booking 만 DB 에 있음
// - Shipment 없음
// - 비즈니스 무결성 깨짐
// → 운영자가 수동 정리 (사고)
}
// ✓ @Transactional 있으면
@Transactional
public void createBookingSafe(Booking b) {
bookingDao.add(b);
shipmentDao.add(...);
// 예외 시 → 전체 rollback (원자성 보장)
}
BookingDao bookingDao;
ShipmentDao shipmentDao;
}
class Booking {}
class BookingDao { void add(Booking b) {} }
class ShipmentDao { void add(Object o) {} }
원자성이 깨지면 어떤 사고?
답:
1. 사고:
유형:
ILIC:
복구:
분산 환경 문제:
여러 서비스/DB:
- 한 트랜잭션 어려움
- 2PC 비싸고 느림
→ 부분 성공 허용하면?
→ Saga
Saga 패턴:
여러 로컬 트랜잭션:
- 각자 commit
- 실패 시 보상 트랜잭션
- 단계별 복구
→ 분산 트랜잭션 대안
보상 트랜잭션:
단계 실패 시:
- 이전 단계 취소
- 명시적 보상 SQL
예: 결제 실패 → 주문 취소 (보상)
// Saga 패턴 (ILIC, 분산 시나리오 가정)
@Service
public class DistributedBookingSaga {
public void createBookingSaga(Booking b) {
try {
// 단계 1: 예약 (commit)
bookingService.create(b);
try {
// 단계 2: 외부 통관 등록 (다른 시스템)
customsService.register(b);
try {
// 단계 3: 운임 결제 (다른 시스템)
paymentService.charge(b);
// 모두 성공
} catch (Exception e) {
// 단계 3 실패 → 보상
customsService.cancelRegistration(b); // 보상 2
bookingService.cancel(b); // 보상 1
throw e;
}
} catch (Exception e) {
// 단계 2 실패 → 보상
bookingService.cancel(b); // 보상 1
throw e;
}
} catch (Exception e) {
// 처리
}
}
BookingService bookingService;
CustomsService customsService;
PaymentService paymentService;
}
class Booking {}
class BookingService { void create(Booking b) {} void cancel(Booking b) {} }
class CustomsService { void register(Booking b) {} void cancelRegistration(Booking b) {} }
class PaymentService { void charge(Booking b) {} }
부분 성공 vs Atomicity:
전통 트랜잭션:
- 강한 원자성
- 한 DB 내
- 분산 약함
Saga:
- 약한 일관성 (잠시 불일치)
- 분산 환경
- 보상으로 복구
부분 성공 허용 시나리오 (Saga 패턴) 는?
답:
1. 분산 문제:
Saga:
보상:
트레이드오프:
undo log:
변경 전 데이터 기록:
- rollback 시 복원용
- 트랜잭션 중 보관
- commit 후 정리
undo log 동작:
UPDATE 실행:
1. 변경 전 값 → undo log
2. 데이터 변경
rollback 시:
- undo log 에서 복원
- 원상복구
commit 시:
- undo log 정리 (다른 세션 위한 격리는 유지)
InnoDB (MySQL):
- Undo Log: rollback / MVCC
- Redo Log: 복구 (Durability, 6.5)
- 트랜잭션 로그
→ 원자성/지속성 보장
시스템 다운 후:
DB 재시작:
- undo log 확인
- commit 안 된 트랜잭션 rollback
- 원자성 유지
→ 어떤 상황에서도
-- undo log 동작 (ILIC, 개념)
-- 트랜잭션 시작
BEGIN;
UPDATE shipments SET status = 'SHIPPED' WHERE id = 1;
-- DB 내부:
-- 1. undo log 에 "id=1 의 이전 status='BOOKED'" 기록
-- 2. 데이터 변경 (status='SHIPPED')
-- 만약 여기서 시스템 다운:
-- → 재시작 시 DB 가 undo log 보고
-- → "commit 안 된 변경" 발견
-- → status='BOOKED' 로 복원 (rollback)
-- → 원자성 유지
-- 정상 commit:
COMMIT;
-- undo log 는 더 이상 rollback 용 X (정리)
-- (단, 다른 세션의 일관 읽기 위해 잠시 유지)
DB 가 원자성을 어떻게 보장 (undo log) 하나?
답:
1. undo log:
동작:
InnoDB:
장애 후:
Spring @Transactional:
메서드 단위 원자성:
- 시작 시 BEGIN
- 끝 시 COMMIT (정상) / ROLLBACK (예외)
- 자동
@Service
public class BookingService {
@Transactional
public void createBooking(Booking b) {
bookingDao.add(b);
shipmentDao.add(...);
// Spring 이 보장:
// - 둘 다 성공 → commit
// - 예외 → 전체 rollback
// - 원자성
}
BookingDao bookingDao;
ShipmentDao shipmentDao;
}
class Booking {}
class BookingDao { void add(Booking b) {} }
class ShipmentDao { void add(Object o) {} }
RuntimeException 자동:
Spring 기본 동작:
- RuntimeException → rollback
- Checked Exception → commit (X! 의외)
- rollbackFor 명시로 해결
자체 호출 주의:
@Transactional 메서드:
- 외부 호출만 AOP 적용
- 자체 호출 (this.method()) → 적용 X
→ 다른 빈 통해서
// Spring 원자성 (ILIC)
@Service
public class BookingService {
// @Transactional 메서드 (원자성 자동)
@Transactional
public void createBooking(Booking b) {
bookingDao.add(b);
shipmentDao.add(...);
// 자동 commit (성공) / rollback (예외)
}
// Checked Exception 도 rollback 시키려면
@Transactional(rollbackFor = Exception.class)
public void withCheckedException(Booking b) throws Exception {
bookingDao.add(b);
// Checked Exception 도 rollback
}
// 외부 빈 호출 (AOP 적용)
@Autowired
private NotificationService notificationService;
@Transactional
public void process(Booking b) {
bookingDao.add(b);
notificationService.send(b); // 다른 빈 (AOP OK)
}
BookingDao bookingDao;
ShipmentDao shipmentDao;
}
class Booking {}
class BookingDao { void add(Booking b) {} }
class ShipmentDao { void add(Object o) {} }
class NotificationService { void send(Booking b) {} }
Spring 의 원자성 처리는?
답:
1. @Transactional:
자동:
RuntimeException:
주의:
1. Atomicity (원자성)
2. 자바 Atomic 과 차이
3. 보장과 한계
이번 Unit에서 Atomicity 를 봤다면, 다음은 Consistency (★ 깊이 파기).
💎 Phase 6 — 트랜잭션과 ACID
✅ Unit 6.1 트랜잭션이란
✅ Unit 6.2 Atomicity ★깊이 ← 여기
⏭ Unit 6.3 Consistency ★깊이
⏭ Unit 6.4 Isolation ★깊이
⏭ Unit 6.5 Durability ★깊이
⏭ Unit 6.6 Commit 이전 동시성 ★깊이
🧪 Part A (9 Unit) ✅
💾 Part B — DB 접근의 진화
✅ Phase 3 — JDBC (3)
✅ Phase 4 — Connection Pool (4)
✅ Phase 5 — DataSource (4)
💎 Phase 6 — ACID (2/6)
총: 22/28 Unit
★ 깊이 파기 — Atomicity 완료