728x90
반응형


🎯 핵심 개념
"같은 트랜잭션 안에서는 몇 번을 조회해도 항상 같은 값이 나온다"
📝 시나리오 1: 상품 재고 관리
상황 설정
sql
-- 초기 데이터
products 테이블
+----+--------+-------+
| id | name | stock |
+----+--------+-------+
| 1 | 아이폰 | 100 |
+----+--------+-------+
Entity 클래스
java
@Entity
@Table(name = "products")
@Getter
@NoArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer stock;
public void decreaseStock(int quantity) {
if (this.stock < quantity) {
throw new IllegalStateException("재고 부족!");
}
this.stock -= quantity;
}
}
🔴 문제 상황: READ COMMITTED 사용 시
java
@Service
@RequiredArgsConstructor
@Slf4j
public class ProductService {
private final ProductRepository productRepository;
// ❌ READ COMMITTED 사용 (문제 발생!)
@Transactional(isolation = Isolation.READ_COMMITTED)
public void orderProduct(Long productId, int quantity) {
// 1️⃣ 첫 번째 조회
Product product = productRepository.findById(productId).orElseThrow();
log.info("🔍 첫 번째 조회 - 재고: {}", product.getStock()); // 100개
// 재고 검증
if (product.getStock() < quantity) {
throw new IllegalStateException("재고 부족!");
}
try {
// 🕐 업무 로직 처리 중... (5초 소요)
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 2️⃣ 두 번째 조회 (같은 트랜잭션 내)
product = productRepository.findById(productId).orElseThrow();
log.info("🔍 두 번째 조회 - 재고: {}", product.getStock()); // ⚠️ 50개로 변경됨!
// 3️⃣ 재고 차감
product.decreaseStock(quantity);
productRepository.save(product);
log.info("✅ 주문 완료 - 최종 재고: {}", product.getStock());
}
}
동시 실행되는 다른 트랜잭션
java
@Service
@RequiredArgsConstructor
@Slf4j
public class AdminService {
private final ProductRepository productRepository;
@Transactional
public void adjustStock(Long productId, int newStock) {
Product product = productRepository.findById(productId).orElseThrow();
product.setStock(newStock);
productRepository.save(product);
log.info("⚙️ 관리자가 재고를 {}개로 변경", newStock);
}
}
실행 시나리오
java
@SpringBootTest
@Slf4j
public class IsolationTest {
@Autowired
private ProductService productService;
@Autowired
private AdminService adminService;
@Test
void readCommittedProblem() throws InterruptedException {
Long productId = 1L;
// 트랜잭션 A: 고객 주문 (백그라운드 실행)
new Thread(() -> {
productService.orderProduct(productId, 10);
}).start();
// 2초 대기
Thread.sleep(2000);
// 트랜잭션 B: 관리자가 재고 변경
adminService.adjustStock(productId, 50);
Thread.sleep(10000);
}
}
```
### 실행 결과 (READ COMMITTED)
```
[트랜잭션 A] 🔍 첫 번째 조회 - 재고: 100
[트랜잭션 A] 🕐 업무 로직 처리 중...
[트랜잭션 B] ⚙️ 관리자가 재고를 50개로 변경 ← COMMIT!
[트랜잭션 A] 🔍 두 번째 조회 - 재고: 50 ← ⚠️ 값이 바뀜! (Non-Repeatable Read)
[트랜잭션 A] ✅ 주문 완료 - 최종 재고: 40
문제점:
- 처음엔 100개였는데 갑자기 50개로 변경됨
- 같은 트랜잭션 내에서 일관성이 깨짐
- 비즈니스 로직이 예상과 다르게 동작할 수 있음
✅ 해결: REPEATABLE READ 사용
java
@Service
@RequiredArgsConstructor
@Slf4j
public class ProductService {
private final ProductRepository productRepository;
// ✅ REPEATABLE READ 사용 (MySQL 기본값)
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void orderProduct(Long productId, int quantity) {
// 1️⃣ 첫 번째 조회
Product product = productRepository.findById(productId).orElseThrow();
log.info("🔍 첫 번째 조회 - 재고: {}", product.getStock()); // 100개
// 재고 검증
if (product.getStock() < quantity) {
throw new IllegalStateException("재고 부족!");
}
try {
// 🕐 업무 로직 처리 중... (5초 소요)
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 2️⃣ 두 번째 조회 (같은 트랜잭션 내)
product = productRepository.findById(productId).orElseThrow();
log.info("🔍 두 번째 조회 - 재고: {}", product.getStock()); // ✅ 여전히 100개!
// 3️⃣ 재고 차감
product.decreaseStock(quantity);
productRepository.save(product);
log.info("✅ 주문 완료 - 최종 재고: {}", product.getStock());
}
}
```
### 실행 결과 (REPEATABLE READ)
```
[트랜잭션 A] 🔍 첫 번째 조회 - 재고: 100
[트랜잭션 A] 🕐 업무 로직 처리 중...
[트랜잭션 B] ⚙️ 관리자가 재고를 50개로 변경 ← COMMIT!
(하지만 A는 여전히 100개로 보임!)
[트랜잭션 A] 🔍 두 번째 조회 - 재고: 100 ← ✅ 값이 그대로! (일관성 보장)
[트랜잭션 A] ✅ 주문 완료 - 최종 재고: 90
[트랜잭션 A] COMMIT!
[최종 DB 상태] 재고: 50 (B가 변경한 값)
MySQL의 MVCC 동작:
- 트랜잭션 A 시작 시점의 스냅샷을 유지
- B가 데이터를 변경하더라도 A는 자신의 스냅샷을 계속 봄
- A가 커밋할 때 충돌 감지 및 처리
728x90
반응형
'Spring > Spring-detail' 카테고리의 다른 글
| Spring Boot로 구현하는 이벤트 기반 비동기 처리 (입문) (0) | 2025.04.03 |
|---|---|
| 동시성 문제 해결 - Synchronized, Pessimistic Lock, Optimistic Lock, Redis (0) | 2024.04.08 |
| @Transactional 안에 @Transactional 테스트 (0) | 2024.03.04 |
| @Conditional 을 이용해 특정 조건일 때만 사용 (0) | 2024.02.22 |
| @ConfigurationProperties / @ConfigurationPropertiesBean (0) | 2023.08.23 |
댓글