F-LAB JAVA · 6주차 · Phase 6 · 트랜잭션과 ACID
★ 깊이 파기 — ACID 의 두 번째, "제약조건은 항상"
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
Consistency (일관성) 는 트랜잭션 완료 후 DB 의 모든 제약조건 (NOT NULL/UNIQUE/CHECK/FOREIGN KEY) 이 항상 지켜져 일관된 상태가 되어야 한다는 성질로, 제약을 깨는 트랜잭션은 DB 가 자동으로 rollback 하며 비즈니스 규칙 같은 더 큰 일관성은 보통 애플리케이션이 강제한다.
Consistency (일관성) 는 트랜잭션이 완료된 후 DB 가 항상 일관된 상태 를 유지해야 한다는 성질이다.
일관성은 주로 제약조건 (Constraint) 으로 표현되며 —NOT NULL(필수),UNIQUE(중복 X),CHECK(값 범위),FOREIGN KEY(참조 무결성), 그리고 도메인의 비즈니스 규칙 (예: 잔액 음수 X) 등이 있다.
트랜잭션 중 제약을 깨는 시도가 감지되면 DB 가 자동으로 전체 트랜잭션을 rollback 하여 일관된 상태를 보장한다.
다만 일관성은 데이터의 정합성(Integrity) 과 유사하지만 좀 더 넓은 개념 — DB 제약은 DB 가, 비즈니스 규칙은 보통 애플리케이션 이 강제하며 (예: Spring 의@PrePersist, 도메인 메서드), JPA 도 엔티티 제약 (@NotNull,@Size,@AssertTrue) 으로 일관성을 도와준다.
Consistency = 회계 장부 균형:
장부 규칙 (제약조건):
- 차변 합 = 대변 합 (항상)
- 잔액 ≥ 0 (음수 X)
- 계정 코드 존재 (FK)
- 거래 일자 필수 (NOT NULL)
트랜잭션 후:
- 모든 규칙 만족
- 깨지면 → 거래 무효 (rollback)
DB 제약 (자동 감지):
- NOT NULL: 빠진 값 X
- UNIQUE: 중복 X
- CHECK: 값 범위
- FK: 참조 유효
비즈니스 규칙 (애플리케이션):
- "잔액 < 출금액이면 거부"
- 도메인 메서드
- DB 제약 외
일관성 vs 무결성:
- 무결성: 데이터 자체 (제약)
- 일관성: 트랜잭션 후 상태 (더 넓음)
DB 의 감지:
- 제약 위반 → 즉시 거부 (예외)
- 전체 트랜잭션 rollback
→ Consistency = 트랜잭션 후 제약조건 + 비즈니스 규칙 항상 만족, 깨지면 rollback.
1. Consistency 정의
2. 제약조건 종류
3. 잔액 음수 불가 같은 제약
4. 자동 rollback
5. 일관성 vs 무결성
6. DB 의 감지 방식
7. 비즈니스 규칙
8. Spring/JPA 의 일관성
9. 면접 + 자기 점검
Consistency (일관성):
"트랜잭션 완료 후
DB 제약조건이 항상 지켜져야"
- 일관된 상태 유지
- 모든 규칙 만족
시작/끝 모두 일관:
트랜잭션 전: 일관 상태
트랜잭션 후: 일관 상태
(중간은 격리 — 외부 X)
→ 일관 → 일관 보장
"Valid State":
유효한 상태:
- 모든 제약 만족
- 비즈니스 규칙 만족
- 의미 있는 데이터
→ 일관성 = 유효성
Consistency (ILIC)
ILIC 일관성 예:
- 배송 (Shipment) 은 예약 (Booking) 참조 필수 (FK)
- 운임 (freight) ≥ 0 (CHECK)
- BL 번호 (bl_no) 유일 (UNIQUE)
- 상태 (status) 는 정해진 값만 (CHECK)
트랜잭션 후:
- 위 모두 만족
- 깨지면 → rollback
→ DB 가 유효성 보장
Consistency (일관성) 의 정의는?
답:
1. 정의:
시작/끝:
Valid State:
목적:
DB 제약조건 (5종):
1. NOT NULL: 필수 값
2. UNIQUE: 중복 X
3. PRIMARY KEY: NOT NULL + UNIQUE
4. CHECK: 값 검사
5. FOREIGN KEY: 참조 무결성
-- NOT NULL
CREATE TABLE shipments (
id BIGINT PRIMARY KEY,
bl_no VARCHAR(50) NOT NULL, -- 필수
weight DECIMAL(10,2)
);
INSERT INTO shipments (id, weight) VALUES (1, 100);
-- ❌ bl_no 빠짐 → 제약 위반 → rollback
-- UNIQUE
CREATE TABLE shipments (
id BIGINT PRIMARY KEY,
bl_no VARCHAR(50) UNIQUE -- 중복 X
);
INSERT INTO shipments VALUES (1, 'BL001');
INSERT INTO shipments VALUES (2, 'BL001');
-- ❌ 중복 → 제약 위반 → rollback
-- CHECK (값 검사)
CREATE TABLE accounts (
id BIGINT PRIMARY KEY,
balance DECIMAL(15,2) CHECK (balance >= 0) -- 음수 X
);
UPDATE accounts SET balance = -100 WHERE id = 1;
-- ❌ 음수 → CHECK 위반 → rollback
-- FOREIGN KEY (참조 무결성)
CREATE TABLE shipments (
id BIGINT PRIMARY KEY,
booking_id BIGINT,
FOREIGN KEY (booking_id) REFERENCES bookings(id)
);
INSERT INTO shipments VALUES (1, 999);
-- ❌ bookings.id=999 없음 → FK 위반 → rollback
-- ILIC 의 제약조건 (102 테이블)
CREATE TABLE bookings (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
bl_no VARCHAR(50) NOT NULL UNIQUE, -- 필수, 유일
status VARCHAR(20) NOT NULL
CHECK (status IN ('BOOKED','CONFIRMED','SHIPPED','CANCELLED')),
weight DECIMAL(10,2) CHECK (weight > 0), -- 양수만
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE shipments (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
booking_id BIGINT NOT NULL,
FOREIGN KEY (booking_id) REFERENCES bookings(id) -- 참조 무결성
);
-- 제약 위반 시도:
INSERT INTO shipments (booking_id) VALUES (999);
-- ❌ booking.id=999 없음 → FK 위반 → rollback
INSERT INTO bookings (bl_no, status, weight) VALUES ('BL001', 'INVALID', 100);
-- ❌ status='INVALID' → CHECK 위반 → rollback
제약조건 종류 (NOT NULL/UNIQUE/CHECK/FK) 는?
답:
1. 5종:
NOT NULL:
CHECK:
FK:
도메인 제약:
비즈니스 규칙:
- "계좌 잔액은 음수 X"
- "재고는 음수 X"
- "주문 수량 ≥ 1"
→ 도메인 의미
-- DB 레벨 (CHECK)
CREATE TABLE accounts (
id BIGINT PRIMARY KEY,
balance DECIMAL(15,2) CHECK (balance >= 0)
);
-- 이체 시도
BEGIN;
UPDATE accounts SET balance = balance - 100000 WHERE id = 'A';
-- A 잔액 50000 이라면? balance = -50000
-- ❌ CHECK 위반 → 자동 rollback (트랜잭션 전체)
COMMIT; -- 도달 못 함
// 애플리케이션 (사전 검증)
@Service
public class TransferService {
@Transactional
public void transfer(Long from, Long to, BigDecimal amount) {
Account fromAcc = accountDao.get(from);
// 비즈니스 검증
if (fromAcc.getBalance().compareTo(amount) < 0) {
throw new InsufficientBalanceException();
// → rollback
}
accountDao.decrease(from, amount);
accountDao.increase(to, amount);
}
AccountDao accountDao;
}
class Account { java.math.BigDecimal getBalance() { return null; } }
class InsufficientBalanceException extends RuntimeException {}
class AccountDao {
Account get(Long id) { return null; }
void decrease(Long id, java.math.BigDecimal a) {}
void increase(Long id, java.math.BigDecimal a) {}
}
2중 방어:
애플리케이션 검증:
- 빠른 거부
- 사용자 피드백
DB CHECK:
- 최종 방어
- 우회 X
→ 둘 다 (안전)
-- ILIC 비즈니스 제약 (DB 레벨)
CREATE TABLE warehouse_items (
id BIGINT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
stock INT NOT NULL CHECK (stock >= 0), -- 재고 음수 X
price DECIMAL(15,2) CHECK (price > 0) -- 가격 양수
);
CREATE TABLE freights (
id BIGINT PRIMARY KEY,
shipment_id BIGINT NOT NULL,
amount DECIMAL(15,2) CHECK (amount >= 0) -- 운임 음수 X
);
-- 시도
UPDATE warehouse_items SET stock = stock - 100 WHERE id = 1;
-- 재고 50 이라면 → -50 시도 → CHECK 위반 → rollback
// 애플리케이션 검증 (ILIC)
@Service
public class WarehouseService {
@Transactional
public void decreaseStock(Long itemId, int amount) {
WarehouseItem item = warehouseDao.get(itemId);
// 비즈니스 검증 (DB CHECK 전에)
if (item.getStock() < amount) {
throw new InsufficientStockException(); // 사용자 친화
}
warehouseDao.decrease(itemId, amount);
// 만약 검증 빠뜨려도 DB CHECK 가 막음
}
WarehouseDao warehouseDao;
}
class WarehouseItem { int getStock() { return 0; } }
class InsufficientStockException extends RuntimeException {}
class WarehouseDao {
WarehouseItem get(Long id) { return null; }
void decrease(Long id, int n) {}
}
잔액 음수 불가 같은 제약은?
답:
1. 도메인 제약:
DB CHECK:
애플리케이션:
2중 방어:
제약 위반 시:
DB 자동 동작:
1. 제약 위반 감지
2. SQL 실행 거부
3. 예외 발생 (SQLException)
4. 트랜잭션 전체 rollback
→ 일관성 보장
예외 종류 (JDBC):
- SQLIntegrityConstraintViolationException
- UNIQUE 위반
- CHECK 위반
- NOT NULL 위반
- FK 위반
→ 모두 IntegrityConstraintViolation
트랜잭션 전체 rollback:
한 SQL 위반 → 전체 트랜잭션 무효:
- 이미 실행된 SQL 도 취소
- 원자성과 함께
- 일관 상태로 복원
→ Atomicity 와 협력
// Spring 자동 rollback
@Transactional
public void process() {
// SQL 1: INSERT (성공)
dao.add(...);
// SQL 2: UPDATE — CHECK 위반!
accountDao.set(id, BigDecimal.valueOf(-100));
// → DataIntegrityViolationException (Spring 변환)
// → 자동 rollback (SQL 1 도 취소)
}
// 자동 rollback (ILIC)
@Service
public class BookingService {
@Transactional
public void createBooking(Booking b) {
try {
// SQL 1: Booking (성공)
bookingDao.add(b);
// SQL 2: Shipment (booking_id 잘못된 값 → FK 위반)
Shipment s = new Shipment();
s.setBookingId(999L); // 존재 X
shipmentDao.add(s);
// ❌ FK 위반 → DataIntegrityViolationException
// SQL 3: 도달 못 함
} catch (DataIntegrityViolationException e) {
// Spring 이 자동 rollback (Booking 도 취소)
throw e;
}
// → DB 일관성 유지 (Booking 있는데 Shipment 없음 X)
}
BookingDao bookingDao;
ShipmentDao shipmentDao;
}
class Booking {}
class Shipment { void setBookingId(Long id) {} }
class BookingDao { void add(Booking b) {} }
class ShipmentDao { void add(Shipment s) {} }
class DataIntegrityViolationException extends RuntimeException {}
깨지는 트랜잭션은 자동 rollback 동작은?
답:
1. 자동 동작:
예외:
전체:
Spring:
| 항목 | 무결성 (Integrity) | 일관성 (Consistency) |
|---|---|---|
| 범위 | 데이터 자체 | 트랜잭션 후 상태 |
| 표현 | DB 제약 (CHECK/FK 등) | 모든 규칙 (제약 + 비즈니스) |
| 책임 | DB | DB + 애플리케이션 |
무결성 (Integrity):
데이터 정합성:
- 개체 (Entity): PK
- 참조 (Referential): FK
- 도메인: CHECK/TYPE
→ DB 가 강제
일관성 (Consistency):
더 넓은 개념:
- 무결성 +
- 비즈니스 규칙
- 의미 있는 상태
→ DB + 애플리케이션
ACID 의 Consistency:
트랜잭션 후:
- 무결성 보장 (DB)
- 비즈니스 규칙 (애플리케이션 + DB CHECK)
- 종합적 유효 상태
→ 무결성 ⊆ 일관성
무결성 vs 일관성 (ILIC)
무결성 (DB 자동):
- bookings.bl_no UNIQUE (개체 무결성)
- shipments.booking_id REFERENCES (참조 무결성)
- weight > 0 (도메인 무결성)
일관성 (DB + 애플리케이션):
- 무결성 +
- "예약 상태가 CANCELLED 면 배송 진행 X" (비즈니스)
- "운임은 weight × rate" (계산 일관)
- 도메인 규칙
→ DB 가 일부, 애플리케이션이 나머지
→ 둘 다 협력
일관성 vs 무결성 (Integrity) 차이는?
답:
1. 무결성:
일관성:
책임:
포함:
DB 의 감지 시점:
1. SQL 실행 시 (즉시)
- NOT NULL: INSERT 시
- UNIQUE: INSERT 시
- CHECK: 값 변경 시
- FK: 참조 변경 시
2. 트랜잭션 commit 시 (지연)
- DEFERRABLE 옵션
-- 즉시 감지 (기본)
BEGIN;
INSERT INTO bookings (bl_no) VALUES ('BL001');
INSERT INTO bookings (bl_no) VALUES ('BL001');
-- ↑ UNIQUE 위반 → 즉시 예외
-- 트랜잭션 rollback 표시
COMMIT; -- 실패
인덱스 활용:
UNIQUE: 유니크 인덱스
PK: 클러스터 인덱스
→ 빠른 감지
CHECK: 표현식 평가
FK: 참조 테이블 조회
트리거 (Trigger):
특정 이벤트 시 자동 실행:
- BEFORE INSERT
- 추가 검증
- 일관성 강화
→ 제약 보완
-- 감지 동작 (ILIC MySQL)
-- 1. NOT NULL 즉시 감지
INSERT INTO bookings (status) VALUES ('BOOKED');
-- ❌ bl_no NULL → "Column 'bl_no' cannot be null"
-- 2. UNIQUE 즉시 감지
INSERT INTO bookings (bl_no, status) VALUES ('BL001', 'BOOKED');
INSERT INTO bookings (bl_no, status) VALUES ('BL001', 'CONFIRMED');
-- ❌ "Duplicate entry 'BL001'"
-- 3. CHECK 감지
INSERT INTO accounts (id, balance) VALUES (1, -100);
-- ❌ "CHECK constraint 'accounts_chk' is violated"
-- 4. FK 감지
INSERT INTO shipments (booking_id) VALUES (999);
-- ❌ "Cannot add or update: foreign key constraint fails"
-- 모두 SQLException → Spring DataIntegrityViolationException
DB 가 일관성을 감지하는 방식은?
답:
1. 시점:
즉시:
활용:
트리거:
비즈니스 규칙:
도메인 의미:
- "예약은 결제 후"
- "회원만 주문"
- "재고 ≤ 주문 거부"
→ DB 제약 외
어디서 강제:
애플리케이션 (주로):
- 도메인 로직
- Service 메서드
- 명확한 메시지
DB (보조):
- CHECK
- TRIGGER
// 도메인 메서드로 일관성
public class Account {
private BigDecimal balance;
public void withdraw(BigDecimal amount) {
// 도메인 규칙
if (balance.compareTo(amount) < 0) {
throw new InsufficientBalanceException();
}
balance = balance.subtract(amount);
}
// 잔액 음수 불가 도메인이 보장
}
class InsufficientBalanceException extends RuntimeException {}
DB 만으로 부족:
복잡한 규칙:
- "월 한도 초과 X"
- "VIP 만 할인"
- "거래 시간 외 X"
→ 애플리케이션 (도메인 로직)
// 비즈니스 규칙 (ILIC)
@Service
public class BookingService {
@Transactional
public void createBooking(BookingRequest req) {
// 비즈니스 규칙 1: 회원만 예약
Customer customer = customerDao.get(req.getCustomerId());
if (!customer.isActive()) {
throw new IllegalStateException("비활성 회원");
}
// 비즈니스 규칙 2: 거래 한도
BigDecimal monthlyTotal = bookingDao.sumMonthly(customer.getId());
if (monthlyTotal.add(req.getAmount())
.compareTo(customer.getMonthlyLimit()) > 0) {
throw new IllegalStateException("월 한도 초과");
}
// 비즈니스 규칙 3: 상품 재고
if (warehouseDao.getStock(req.getItemId()) < req.getQuantity()) {
throw new IllegalStateException("재고 부족");
}
// 예약 진행
bookingDao.add(req.toBooking());
warehouseDao.decreaseStock(req.getItemId(), req.getQuantity());
// DB CHECK 도 추가 방어
}
CustomerDao customerDao;
BookingDao bookingDao;
WarehouseDao warehouseDao;
}
class BookingRequest {
Long getCustomerId() { return null; }
Long getItemId() { return null; }
int getQuantity() { return 0; }
java.math.BigDecimal getAmount() { return null; }
Booking toBooking() { return null; }
}
class Booking {}
class Customer {
Long getId() { return null; }
boolean isActive() { return false; }
java.math.BigDecimal getMonthlyLimit() { return null; }
}
class CustomerDao { Customer get(Long id) { return null; } }
class BookingDao {
void add(Booking b) {}
java.math.BigDecimal sumMonthly(Long id) { return null; }
}
class WarehouseDao {
int getStock(Long id) { return 0; }
void decreaseStock(Long id, int n) {}
}
비즈니스 규칙은 어디서 강제하나?
답:
1. 비즈니스 규칙:
애플리케이션:
DB:
도메인 메서드:
JPA 의 도움:
엔티티 검증:
- @NotNull
- @Size
- @Min/@Max
- @AssertTrue
- 등 Bean Validation
→ 애플리케이션 일관성
// JPA + Bean Validation
@Entity
@Table(name = "bookings")
public class Booking {
@Id
@GeneratedValue
private Long id;
@NotNull
@Size(min = 1, max = 50)
private String blNo;
@NotNull
@Min(0)
private BigDecimal weight;
@NotNull
@Pattern(regexp = "BOOKED|CONFIRMED|SHIPPED|CANCELLED")
private String status;
// 저장 시 검증
}
// Controller 검증
@RestController
public class BookingController {
@PostMapping("/api/bookings")
public Booking create(@Valid @RequestBody BookingRequest req) {
// @Valid → Bean Validation 실행
// 위반 시 → MethodArgumentNotValidException
return null;
}
}
class BookingRequest {}
// JPA 라이프사이클
@Entity
public class Booking {
@PrePersist
void prePersist() {
// 저장 전 검증
if (status == null) {
status = "BOOKED"; // 기본값
}
// 추가 비즈니스 일관성
}
@PreUpdate
void preUpdate() {
// 수정 전 검증
}
String status;
}
// Spring/JPA 일관성 (ILIC)
// 엔티티 (Bean Validation)
@Entity
@Table(name = "shipments")
public class Shipment {
@Id @GeneratedValue
private Long id;
@NotNull
@Size(min = 1, max = 50)
private String blNo;
@NotNull @DecimalMin("0.0")
private BigDecimal weight;
@NotNull @ManyToOne
private Booking booking; // FK
@PrePersist
void onPrePersist() {
// 비즈니스 일관성
if (this.status == null) {
this.status = "BOOKED";
}
}
private String status;
}
// Service
@Service
public class ShipmentService {
@Transactional
public void create(@Valid Shipment s) { // Bean Validation
// 검증 통과 후 저장
shipmentRepository.save(s);
// JPA → SQL → DB CHECK
// 다중 방어
}
ShipmentRepository shipmentRepository;
}
interface ShipmentRepository { void save(Shipment s); }
class Booking {}
Spring/JPA 의 일관성 처리는?
답:
1. JPA:
@NotNull/@Size:
@Valid:
@PrePersist:
| Q | 핵심 답변 |
|---|---|
| Consistency? | 제약조건 항상 |
| 제약조건? | NOT NULL/UNIQUE/CHECK/FK |
| 잔액 음수? | CHECK 또는 도메인 |
| 자동 rollback? | 제약 위반 시 |
| vs 무결성? | 일관성 더 넓음 |
| DB 감지? | SQL 실행 시 |
| 비즈니스 규칙? | 애플리케이션 |
| Spring? | Bean Validation |
| JPA? | @NotNull 등 |
| 다중 방어? | DB + 앱 |
답:
답:
답:
답:
답:
1. Consistency (일관성)
2. 자동 rollback
3. 책임 분담
이번 Unit에서 Consistency 를 봤다면, 다음은 Isolation (★ 깊이 파기).
💎 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 (3/6)
총: 23/28 Unit
★ 깊이 파기 — Consistency 완료