E-commerce API ์๋น์ค์์ ๋์์ฑ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์๋ ์ง์ ์ ์๋ณํ์ต๋๋ค.
๋ฌธ์ ์ํฉ:
์๋๋ฆฌ์ค: ๋ง์ง๋ง ๋จ์ ์ฟ ํฐ 1๊ฐ์ ๋ํด ๋์์ ๋ฐ๊ธ ์์ฒญ
1. ์ฌ์ฉ์ A, B๊ฐ ๋์์ ์ฟ ํฐ ๋ฐ๊ธ ์์ฒญ
2. ๋ ํธ๋์ญ์
๋ชจ๋ ์ฌ๊ณ ํ์ธ (์ฌ๊ณ = 1)
3. ๋ ํธ๋์ญ์
๋ชจ๋ ์ฌ๊ณ ์ฐจ๊ฐ ๋ฐ ๋ฐ๊ธ ์ฒ๋ฆฌ
4. ๊ฒฐ๊ณผ: ์ฌ๊ณ ์ด๊ณผ ๋ฐ๊ธ (์ฌ๊ณ -1, ์ค์ ๋ฐ๊ธ 2๊ฐ)
๋ฌธ์ ์ ํ: Lost Update (์ฌ๊ณ ๊ฐฑ์ ์์ค)
๋ฌธ์ ์ํฉ:
์๋๋ฆฌ์ค: ํ ์ฌ์ฉ์๊ฐ ๋์ผ ์ฟ ํฐ์ ์ฌ๋ฌ ์ฃผ๋ฌธ์์ ๋์ ์ฌ์ฉ
1. ๋ ์ฃผ๋ฌธ์์ ๋์์ ๊ฐ์ ์ฟ ํฐ ์ฌ์ฉ ์์ฒญ
2. ๋ ํธ๋์ญ์
๋ชจ๋ ์ฟ ํฐ ์ํ ํ์ธ (๋ฏธ์ฌ์ฉ)
3. ๋ ํธ๋์ญ์
๋ชจ๋ ์ฌ์ฉ ์ฒ๋ฆฌ
4. ๊ฒฐ๊ณผ: ์ฟ ํฐ ์ค๋ณต ์ฌ์ฉ
๋ฌธ์ ์ ํ: Lost Update (์ฟ ํฐ ์ํ ๊ฐฑ์ ์์ค)
๋ฌธ์ ์ํฉ:
์๋๋ฆฌ์ค: ๋ง์ง๋ง ๋จ์ ์ํ 1๊ฐ์ ๋ํด ๋์ ์ฃผ๋ฌธ
1. ์ฌ์ฉ์ A, B๊ฐ ๋์์ ์ฃผ๋ฌธ ์์ฒญ
2. ๋ ํธ๋์ญ์
๋ชจ๋ ์ฌ๊ณ ํ์ธ (์ฌ๊ณ = 1)
3. ๋ ํธ๋์ญ์
๋ชจ๋ ์ฌ๊ณ ์ฐจ๊ฐ ์ฒ๋ฆฌ
4. ๊ฒฐ๊ณผ: ์ฌ๊ณ ๋ง์ด๋์ค ๋๋ ํ ๋ฒ์ ์ฐจ๊ฐ๋ง ๋ฐ์ (๊ณผ๋งค ๋ฐ์)
๋ฌธ์ ์ ํ: Lost Update (์ฌ๊ณ ๊ฐฑ์ ์์ค)
๋ฌธ์ ์ํฉ:
์๋๋ฆฌ์ค: ์์ก ์ด์์ ๋์ ๊ฒฐ์
1. ์ฌ์ฉ์ ์์ก 10,000์, ๊ฐ 7,000์์ง๋ฆฌ ์ฃผ๋ฌธ 2๊ฐ ๋์ ๊ฒฐ์
2. ๋ ํธ๋์ญ์
๋ชจ๋ ์์ก ํ์ธ (10,000 >= 7,000 ํต๊ณผ)
3. ๋ ํธ๋์ญ์
๋ชจ๋ ์ฐจ๊ฐ ์ฒ๋ฆฌ
4. ๊ฒฐ๊ณผ: ์์ก ์์ (-4,000์)
๋ฌธ์ ์ ํ: Lost Update (์์ก ๊ฐฑ์ ์์ค)
๋ฌธ์ ์ํฉ:
์๋๋ฆฌ์ค: ์ค๋ณต ํด๋ฆญ์ผ๋ก ์ธํ ๋์ ์ถฉ์
1. ์ฌ์ฉ์๊ฐ ๋น ๋ฅด๊ฒ ์ถฉ์ ๋ฒํผ ์ฌ๋ฌ ๋ฒ ํด๋ฆญ (๊ฐ 10,000์)
2. ๋ ํธ๋์ญ์
๋ชจ๋ ํ์ฌ ์์ก ์กฐํ (5,000์)
3. ๋ ํธ๋์ญ์
๋ชจ๋ ์ถฉ์ ์ฒ๋ฆฌ
4. ๊ฒฐ๊ณผ: ํ ๋ฒ์ ์ถฉ์ ๋ง ๋ฐ์ ๋๋ ์๋ชป๋ ๊ธ์ก
๋ฌธ์ ์ ํ: Lost Update (์์ก ๊ฐฑ์ ์์ค)
| ๋์์ฑ ์ ์ด ์ง์ | ๋์์ฑ ๋น๋ | ์ถฉ๋ ํ๋ฅ | ๋น์ฆ๋์ค ์ค์๋ | ์ฑ๋ฅ ์๊ตฌ์ฌํญ |
|---|---|---|---|---|
| ์ฟ ํฐ ๋ฐ๊ธ | ๋งค์ฐ ๋์ | ๋งค์ฐ ๋์ | ๋งค์ฐ ๋์ (์ฌ๊ณ ์ด๊ณผ ๋ถ๊ฐ) | ๋๊ธฐ ํ์ฉ ๊ฐ๋ฅ |
| ์ฟ ํฐ ์ฌ์ฉ | ๋งค์ฐ ๋ฎ์ | ๋ฎ์ | ๋์ (์ค๋ณต ์ฌ์ฉ ๋ถ๊ฐ) | ๋น ๋ฅธ ์๋ต ํ์ |
| ์ฌ๊ณ ์ฐจ๊ฐ | ๋์ | ๋์ | ๋งค์ฐ ๋์ (๊ณผ๋งค ๋ถ๊ฐ) | ์ ํ์ฑ ์ฐ์ |
| ํฌ์ธํธ ์ฐจ๊ฐ | ๋ฎ์ | ๋ฎ์ | ๋์ (์์ ๋ถ๊ฐ) | ๋น ๋ฅธ ์๋ต ํ์ |
| ํฌ์ธํธ ์ถฉ์ | ๋งค์ฐ ๋ฎ์ | ๋งค์ฐ ๋ฎ์ | ์ค๊ฐ (์ฌ์๋ ๊ฐ๋ฅ) | ๋น ๋ฅธ ์๋ต ํ์ |
| ๋์์ฑ ์ ์ด ์ง์ | ํด๊ฒฐ ๋ฐฉ์ | ์ ํ ์ด์ |
|---|---|---|
| ์ฟ ํฐ ๋ฐ๊ธ | ๋น๊ด์ ์ฐ๊ธฐ ๋ฝ | ์ ์ฐฉ์ ํน์ฑ์ ๋์ ์์ฒญ ํญ์ฃผ, ์์ฐจ ์ฒ๋ฆฌ๋ก ์ ํํ ์ฌ๊ณ ๊ด๋ฆฌ ํ์ |
| ์ฟ ํฐ ์ฌ์ฉ | ๋๊ด์ ๋ฝ | ๋์ ์ฌ์ฉ ํ๋ฅ ๋งค์ฐ ๋ฎ์, ๋น ๋ฅธ ๊ฒฐ์ ์ฒ๋ฆฌ, ์ถฉ๋ ์ ์ฌ์๋ |
| ์ฌ๊ณ ์ฐจ๊ฐ | ๋น๊ด์ ์ฐ๊ธฐ ๋ฝ | ์ธ๊ธฐ ์ํ ๋์ ์ฃผ๋ฌธ ๋ง์, ๊ณผ๋งค ๋ฐฉ์ง๊ฐ ์ต์ฐ์ |
| ํฌ์ธํธ ์ฐจ๊ฐ | ๋๊ด์ ๋ฝ | ๋์ ๊ฒฐ์ ๋๋ญ, ๋น ๋ฅธ ์๋ต ์ค์, ์ถฉ๋ ์ ์ฌ์๋ |
| ํฌ์ธํธ ์ถฉ์ | ๋๊ด์ ๋ฝ | ์ค๋ณต ํด๋ฆญ ์ธ์ ๋์์ฑ ๊ฑฐ์ ์์, ์ฑ๋ฅ ์ฐ์ |
๊ตฌํ:
// CouponRepository.java
public interface CouponRepository {
/**
* ๋น๊ด์ ๋ฝ์ ์ฌ์ฉํ์ฌ ์ฟ ํฐ ์กฐํ
* ์ ์ฐฉ์ ์ฟ ํฐ ๋ฐ๊ธ ์ ๋์์ฑ ์ ์ด๋ฅผ ์ํด ์ฌ์ฉ
*/
Optional<Coupon> findByIdWithPessimisticLock(Integer couponId);
}
// CouponService.java
@Service
@RequiredArgsConstructor
public class CouponService {
@Transactional
public IssueCouponResult issueCoupon(IssueCouponCommand command) {
// 1. ํ์ ์กด์ฌ ๊ฒ์ฆ
User user = userValidator.validateAndGetUser(command.userId());
// 2. ์ฟ ํฐ ์กด์ฌ ๊ฒ์ฆ ๋ฐ ๋น๊ด์ ๋ฝ ํ๋
Coupon coupon = couponRepository.findByIdWithPessimisticLock(command.couponId())
.orElseThrow(() -> new CouponException(ErrorCode.COUPON_NOT_FOUND));
// 3. ์ค๋ณต ๋ฐ๊ธ ๊ฒ์ฆ (๋น๊ด์ ๋ฝ ์ฌ์ฉ)
Optional<CouponUser> existingCouponUser = couponUserRepository
.findByCouponIdAndUserIdWithPessimisticLock(command.couponId(), command.userId());
if (existingCouponUser.isPresent()) {
throw new CouponException(ErrorCode.COUPON_ALREADY_ISSUED);
}
// 4. ์ฟ ํฐ ๋ฐ๊ธ ๊ฐ๋ฅ ์ฌ๋ถ ๊ฒ์ฆ (์๋, ๋ง๋ฃ์ผ)
// ๋๋ฉ์ธ ์ํฐํฐ์ issueCoupon() ๋ฉ์๋๊ฐ ๊ฒ์ฆ์ ์ํํ๊ณ ๋ฐ๊ธ ์๋์ ์ฆ๊ฐ์ํด
coupon.issueCoupon();
// 5. ์ฟ ํฐ ์
๋ฐ์ดํธ (๋ฐ๊ธ ์๋ ์ฆ๊ฐ)
couponRepository.save(coupon);
// 6. ์ฟ ํฐ ๋ฐ๊ธ ์ด๋ ฅ ์์ฑ
CouponUser couponUser = CouponUser.createIssuedCouponUser(coupon, user);
couponUser = couponUserRepository.save(couponUser);
// 7. ๊ฒฐ๊ณผ ๋ฐํ
return IssueCouponResult.from(couponUser);
}
}์ ํ ์ด์ :
- ์ ์ฐฉ์ ์ด๋ฒคํธ๋ ๋์ ์์ฒญ์ด ํญ์ฃผํ๋ฉฐ ์ถฉ๋์ด ๋งค์ฐ ๋น๋ฒํจ
- ๋๊ด์ ๋ฝ ์ฌ์ฉ ์ ์ถฉ๋ ์ฆ์ ์คํจ โ ์ฌ์ฉ์๊ฐ ๋ฐ๋ณต ์ฌ์๋ (๋์ UX)
- ๋น๊ด์ ๋ฝ์ ์์ฐจ ์ฒ๋ฆฌ๋ก ๋๊ธฐํ์ง๋ง ์ ์ฐฉ์ ๊ธฐํ ๋ณด์ฅ ๋ฐ ์ ํํ ํ์ ์๋ด ๊ฐ๋ฅ
- ์ฌ๊ณ ์ด๊ณผ ๋ฐ๊ธ์ ์ ๋ ํ์ฉ ๋ถ๊ฐ
๊ตฌํ ๋ฐฉ์:
@Lock(LockModeType.PESSIMISTIC_WRITE)๋ฅผ ์ฌ์ฉํ ๋น๊ด์ ์ฐ๊ธฐ ๋ฝ- ์ฟ ํฐ ์กฐํ ์
findByIdWithPessimisticLock๋ฉ์๋๋ก ๋ฝ ํ๋ - ์ค๋ณต ๋ฐ๊ธ ํ์ธ ์์๋
findByCouponIdAndUserIdWithPessimisticLock๋ก ๋ฝ ํ๋ - ํธ๋์ญ์ ์ปค๋ฐ ์๊น์ง ๋ฝ ์ ์งํ์ฌ ์์ฐจ์ ๋ฐ๊ธ ๋ณด์ฅ
๊ตฌํ:
// ProductRepository.java
public interface ProductRepository {
/**
* ID๋ก ์ํ ์กฐํ (๋น๊ด์ ๋ฝ - PESSIMISTIC_WRITE)
* ๋์์ฑ ์ ์ด๊ฐ ํ์ํ ์ฌ๊ณ ์ฐจ๊ฐ ๋ฑ์ ์์
์ ์ฌ์ฉ
*/
Product findByIdWithLock(Integer productId);
}
// OrderService.java
@Service
@RequiredArgsConstructor
public class OrderService {
@Transactional
public PaymentResult processPayment(Integer orderId, Integer userId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderException(ErrorCode.ORDER_NOT_FOUND));
order.validatePaymentAvailable();
try {
// 1. ํฌ์ธํธ ์ฐจ๊ฐ
Integer paymentAmount = order.getFinalPaymentAmount();
User user = deductPointsWithOptimisticLock(userId, paymentAmount);
// 2. ํฌ์ธํธ ์ฌ์ฉ ์ด๋ ฅ ์ ์ฅ
Point point = Point.createUseHistory(user, paymentAmount);
pointRepository.save(point);
// 3. ์ํ ์ฌ๊ณ ์ฐจ๊ฐ (๋น๊ด์ ๋ฝ ์ฌ์ฉ)
List<OrderItem> items = orderItemRepository.findByOrderId(orderId);
for (OrderItem item : items) {
Product product = productRepository.findByIdWithLock(item.getProduct().getProductId());
product.decreaseStock(item.getOrderQuantity());
productRepository.save(product);
}
// 4. ์ฟ ํฐ ์ฌ์ฉ ์ฒ๋ฆฌ
if (order.hasCoupon()) {
useCouponWithOptimisticLock(order.getCoupon().getCouponId(), userId);
}
// 5. ์ฅ๋ฐ๊ตฌ๋ ์ญ์
cartItemRepository.deleteByUserId(userId);
// 6. ์ฃผ๋ฌธ ์ํ ๋ณ๊ฒฝ
order.completePayment();
orderRepository.save(order);
return PaymentResult.from(order, user.getPointBalance());
}
catch (Exception e) {
log.error("๊ฒฐ์ ์ฒ๋ฆฌ ์ค ์ค๋ฅ ๋ฐ์. ์ฃผ๋ฌธID: {}, ์ฌ์ฉ์ID: {}", orderId, userId, e);
// ์๋ ์์ธ๊ฐ ๋น์ฆ๋์ค ์์ธ๋ฉด ๊ทธ๋๋ก ๋์ง๊ธฐ
if (e instanceof BusinessException) {
throw e;
}
// ์์์น ๋ชปํ ์์คํ
์์ธ๋ OrderException์ผ๋ก ๊ฐ์ธ๊ธฐ
throw new OrderException(ErrorCode.ORDER_PAY_FAILED, e);
}
}
}์ ํ ์ด์ :
- ์ธ๊ธฐ ์ํ์ ๋์ ์ฃผ๋ฌธ์ด ๋ง์ ๊ฒฝ์์ด ๋์
- ๊ณผ๋งค ๋ฐ์ ์ ๋น์ฆ๋์ค ๋ฆฌ์คํฌ๊ฐ ํผ (๊ณ ๊ฐ ๋ถ๋ง, ํ๋ถ ์ฒ๋ฆฌ ๋ฑ)
- ๋๊ด์ ๋ฝ ์ฌ์ฉ ์ ์ถฉ๋ ๋น๋ฒํ์ฌ ์ฌ์๋ ๊ณผ๋ค ๋ฐ์ โ ์์คํ ๋ถํ
- ์ฌ๊ณ ๊ด๋ฆฌ ์ ํ์ฑ์ด ์ฑ๋ฅ๋ณด๋ค ์ค์
๊ตฌํ ๋ฐฉ์:
@Lock(LockModeType.PESSIMISTIC_WRITE)๋ฅผ ์ฌ์ฉํ ๋น๊ด์ ์ฐ๊ธฐ ๋ฝ- ์ฌ๊ณ ์ฐจ๊ฐ ์
findByIdWithLock๋ฉ์๋๋ก ๋ฝ ํ๋ - ํธ๋์ญ์ ์ปค๋ฐ ์๊น์ง ๋ฝ ์ ์ง
- ์คํจ ์
@Transactional๋กค๋ฐฑ์ผ๋ก ์๋ ๋ณต์
๊ตฌํ:
// CouponUser.java
@Entity
@Table(name = "coupon_user")
public class CouponUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "coupon_user_id")
private Integer couponUserId;
@Column(nullable = false)
private Boolean used;
@Version
@Column(name = "version")
private Integer version;
public void validateUsable() {
if (Boolean.TRUE.equals(this.used)) {
throw new CouponException(ErrorCode.COUPON_ALREADY_USED);
}
}
public void markAsUsed() {
this.used = true;
this.usedAt = LocalDateTime.now();
}
}
// OrderService.java
@Service
@RequiredArgsConstructor
public class OrderService {
/**
* ์ฟ ํฐ ์ฌ์ฉ ์ฒ๋ฆฌ
*/
@Transactional
public CouponUser useCouponWithOptimisticLock(Integer couponId, Integer userId) {
CouponUser couponUser = couponUserRepository
.findByCouponIdAndUserId(couponId, userId)
.orElseThrow(() -> new CouponException(ErrorCode.COUPON_NOT_ISSUED));
couponUser.markAsUsed();
return couponUserRepository.save(couponUser);
}
@Transactional
public PaymentResult processPayment(Integer orderId, Integer userId) {
// ...
// 4. ์ฟ ํฐ ์ฌ์ฉ ์ฒ๋ฆฌ
if (order.hasCoupon()) {
useCouponWithOptimisticLock(order.getCoupon().getCouponId(), userId);
}
// ...
}
}์ ํ ์ด์ :
- ์ฌ์ฉ์๊ฐ ์์ ์ ์ฟ ํฐ์ ๋์์ ์ฌ๋ฌ ์ฃผ๋ฌธ์์ ์ฌ์ฉํ ํ๋ฅ ๋งค์ฐ ๋ฎ์
- ๋น ๋ฅธ ๊ฒฐ์ ์ฒ๋ฆฌ๊ฐ ์ฌ์ฉ์ ๊ฒฝํ์ ์ค์
@Versionํ๋๋ฅผ ํตํ ์๋ ๋๊ด์ ๋ฝ์ผ๋ก ๋ฐ์ดํฐ ์ ํฉ์ฑ ๋ณด์ฅ- ์ถฉ๋ ๋ฐ์ ์ ํธ๋์ญ์ ๋กค๋ฐฑ์ผ๋ก ์์ ํ๊ฒ ์ฒ๋ฆฌ
- DB ๋ฝ ๋ฏธ์ฌ์ฉ์ผ๋ก ์ฑ๋ฅ ์ด์
๊ตฌํ ๋ฐฉ์:
@Versionํ๋๋ฅผ ํ์ฉํ์ฌ JPA๊ฐ ์๋์ผ๋ก ๋๊ด์ ๋ฝ ์ ์ฉ- ๋ช
์์ ์ธ
@Lock(LockModeType.OPTIMISTIC)๋ถํ์ - ์ผ๋ฐ ์กฐํ ๋ฉ์๋(
findByCouponIdAndUserId)๋ง์ผ๋ก๋ ๋๊ด์ ๋ฝ ๋์
๊ตฌํ:
// User.java
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Integer userId;
@Column(name = "point_balance", nullable = false)
private Integer pointBalance;
@Version
@Column(name = "version")
private Integer version;
/**
* ํฌ์ธํธ๋ฅผ ์ฌ์ฉํฉ๋๋ค.
* @param amount ์ฌ์ฉํ ๊ธ์ก
* @throws PointException ์์ก์ด ๋ถ์กฑํ ๊ฒฝ์ฐ
*/
public void usePoints(Integer amount) {
if (this.pointBalance < amount) {
throw new PointException(ErrorCode.POINT_INSUFFICIENT_BALANCE);
}
this.pointBalance -= amount;
}
}
// OrderService.java
@Service
@RequiredArgsConstructor
public class OrderService {
/**
* ํฌ์ธํธ ์ฐจ๊ฐ ์ฒ๋ฆฌ
*/
@Transactional
public User deductPointsWithOptimisticLock(Integer userId, Integer amount) {
User user = userRepository.findById(userId);
if (user == null) {
throw new UserException(ErrorCode.USER_NOT_FOUND);
}
user.usePoints(amount);
return userRepository.save(user);
}
@Transactional
public PaymentResult processPayment(Integer orderId, Integer userId) {
// ...
// 1. ํฌ์ธํธ ์ฐจ๊ฐ
Integer paymentAmount = order.getFinalPaymentAmount();
User user = deductPointsWithOptimisticLock(userId, paymentAmount);
// 2. ํฌ์ธํธ ์ฌ์ฉ ์ด๋ ฅ ์ ์ฅ
Point point = Point.createUseHistory(user, paymentAmount);
pointRepository.save(point);
// ...
}
}์ ํ ์ด์ :
- ํ ์ฌ์ฉ์๊ฐ ๋์์ ์ฌ๋ฌ ๊ฒฐ์ ์งํํ๋ ๊ฒฝ์ฐ ๋๋ญ
- ๊ฒฐ์ ํ๋ก์ธ์ค์์ ๋น ๋ฅธ ์๋ต์ด ์ค์
@Versionํ๋๋ฅผ ํตํ ์๋ ๋๊ด์ ๋ฝ์ผ๋ก ๋ฐ์ดํฐ ์ ํฉ์ฑ ๋ณด์ฅ- ์ถฉ๋ ๋ฐ์ ์ ํธ๋์ญ์ ๋กค๋ฐฑ์ผ๋ก ์์ ํ๊ฒ ์ฒ๋ฆฌ
- ๋น๊ด์ ๋ฝ ์ฌ์ฉ ์ ๋ถํ์ํ ๋๊ธฐ ์๊ฐ ๋ฐ์
๊ตฌํ ๋ฐฉ์:
@Versionํ๋๋ฅผ ํ์ฉํ์ฌ JPA๊ฐ ์๋์ผ๋ก ๋๊ด์ ๋ฝ ์ ์ฉ- ๋ช
์์ ์ธ
@Lock(LockModeType.OPTIMISTIC)๋ถํ์ - ์ผ๋ฐ ์กฐํ ๋ฉ์๋(
findById)๋ง์ผ๋ก๋ ๋๊ด์ ๋ฝ ๋์
๊ตฌํ:
// User.java
@Entity
@Table(name = "user")
public class User {
@Version
@Column(name = "version")
private Integer version;
/**
* ํฌ์ธํธ๋ฅผ ์ถฉ์ ํฉ๋๋ค.
* @param amount ์ถฉ์ ํ ๊ธ์ก
*/
public void chargePoints(Integer amount) {
this.pointBalance += amount;
}
}
// PointService.java
@Service
@Transactional
public class PointService {
@Transactional
public PointResult chargePoint(Integer userId, Integer amount) {
// 1. ๊ธ์ก ์ ํจ์ฑ ๊ฒ์ฆ
Point.validatePointAmount(amount, minAmount, maxAmount);
// 2. ์ฌ์ฉ์ ์กฐํ
User user = userRepository.findById(userId);
if (user == null) {
throw new UserException(ErrorCode.USER_NOT_FOUND);
}
// 3. ์์ฌ ํฌ์ธํธ ๋ฐ์ (๋๊ด์ ๋ฝ ์ ์ฉ ๋ถ๋ถ๋ง ์ฌ์๋)
chargePointsWithRetry(user, amount);
// 4. Point ์ด๋ ฅ ์ ์ฅ
Point point = Point.createChargeHistory(user, amount);
Point savedPoint = pointRepository.save(point);
// 5. DTO๋ก ๋ณํํ์ฌ ๋ฐํ
return PointResult.from(savedPoint, user.getPointBalance());
}
@Retryable(
retryFor = {ObjectOptimisticLockingFailureException.class,
jakarta.persistence.OptimisticLockException.class},
exclude = {UserException.class},
maxAttempts = 5,
backoff = @Backoff(delay = 100)
)
@Transactional
public void chargePointsWithRetry(User user, Integer amount) {
user.chargePoints(amount);
userRepository.save(user);
}
@Recover
public PointResult recoverOptimistic(ObjectOptimisticLockingFailureException e,
Integer userId,
Integer amount) {
log.debug(">> ์ฌ์๋ ์คํจ, userId: {}, amount: {}", userId, amount);
throw new PointException(ErrorCode.POINT_RACE_CONDITION);
}
}์ ํ ์ด์ :
- ์ค๋ณต ํด๋ฆญ์ ์ ์ธํ๋ฉด ๋์์ฑ ๊ฑฐ์ ๋ฐ์ํ์ง ์์
- ์ถฉ์ ์คํจ ์ ์ฌ์ฉ์๊ฐ ์ฌ์๋ ๊ฐ๋ฅํ ์์
@Versionํ๋๋ฅผ ํตํ ์๋ ๋๊ด์ ๋ฝ์ผ๋ก ๋ฐ์ดํฐ ์ ํฉ์ฑ ๋ณด์ฅ- ๋น๊ด์ ๋ฝ์ ์ฑ๋ฅ์ ์ค๋ฒํค๋
- ๋น ๋ฅธ ์๋ต์ด ์ฌ์ฉ์ ๊ฒฝํ ํฅ์
๊ตฌํ ๋ฐฉ์:
@Retryable์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํ์ฌ ์ถฉ๋ ์ ์๋ ์ฌ์๋@Versionํ๋๋ฅผ ํ์ฉํ์ฌ JPA๊ฐ ์๋์ผ๋ก ๋๊ด์ ๋ฝ ์ ์ฉ- ์ถฉ์ ์์
๋ง ๋ณ๋ ๋ฉ์๋(
chargePointsWithRetry)๋ก ๋ถ๋ฆฌํ์ฌ ์ฌ์๋ ๋ฒ์ ์ต์ํ - ์ต๋ 5ํ ์ฌ์๋, 100ms ์ง์ฐ
๋ชจ๋ ๋์์ฑ ์ ์ด ์ง์ ์ ๋ํด ํตํฉ ํ ์คํธ๋ฅผ ๊ตฌํํ์ฌ ๋ฝ ์ ๋ต์ ํจ๊ณผ๋ฅผ ๊ฒ์ฆํ์ต๋๋ค.
ํ ์คํธ ํ์ผ: CouponServiceConcurrencyIntegrationTest.java
ํ ์คํธ ์ผ์ด์ค 1: ๋์ผ ์ฌ์ฉ์ ์ค๋ณต ๋ฐ๊ธ ๋ฐฉ์ง
- ์๋๋ฆฌ์ค: ๋์ผ ์ฌ์ฉ์๊ฐ 20๊ฐ ์ค๋ ๋๋ก ๋์์ ๊ฐ์ ์ฟ ํฐ ๋ฐ๊ธ ์์ฒญ
- ๊ฒฐ๊ณผ:
- ์ฑ๊ณต: 1๊ฑด
- ์คํจ: 19๊ฑด (์ค๋ณต ๋ฐ๊ธ ์์ธ)
- ์ต์ข ๋ฐ๊ธ ์๋: 1๊ฐ
- ๊ฒ์ฆ ์๋ฃ: ์ค๋ณต ๋ฐ๊ธ ๋ฐฉ์ง 100%
ํ ์คํธ ์ผ์ด์ค 2: ๋ง์ง๋ง 1๊ฐ ์ฟ ํฐ ์ ์ฐฉ์
- ์๋๋ฆฌ์ค: 20๋ช ์ ์ฌ์ฉ์๊ฐ ๋ง์ง๋ง ๋จ์ 1๊ฐ ์ฟ ํฐ์ ๋์์ ์์ฒญ
- ๊ฒฐ๊ณผ:
- ์ฑ๊ณต: 1๋ช
- ์คํจ: 19๋ช (ํ์ )
- ์ต์ข ์ฌ๊ณ : 0๊ฐ
- ๊ฒ์ฆ ์๋ฃ: ์ฌ๊ณ ์ด๊ณผ ๋ฐ๊ธ 0๊ฑด
ํ ์คํธ ์ผ์ด์ค 3: ์ ์ฐฉ์ 10๊ฐ ์ฟ ํฐ, 20๋ช ๊ฒฝ์
- ์๋๋ฆฌ์ค: 20๋ช ์ด ๋์์ 10๊ฐ ์ฌ๊ณ ์ฟ ํฐ ๋ฐ๊ธ ์์ฒญ
- ๊ฒฐ๊ณผ:
- ์ฑ๊ณต: 10๋ช
- ์คํจ: 10๋ช (ํ์ )
- ์ต์ข ๋ฐ๊ธ ์๋: 10๊ฐ (์ ํํ ์ด ์๋๋งํผ)
- ๋ฐ๊ธ ์ด๋ ฅ: 10๊ฑด
- ๊ฒ์ฆ ์๋ฃ: ์ฌ๊ณ ์ด๊ณผ ๋ฐ๊ธ 0%, ๋ฐ๊ธ ์ ํ๋ 100%
ํ ์คํธ ํ์ผ: OrderServiceConcurrencyIntegrationTest.java
ํ ์คํธ ์ผ์ด์ค 1: ๋์ผ ์ฃผ๋ฌธ ์ค๋ณต ๊ฒฐ์ ๋ฐฉ์ง
- ์๋๋ฆฌ์ค: 1๊ฐ์ ์ฃผ๋ฌธ์ ๋ํด 10๊ฐ ์ค๋ ๋๋ก ๋์ ๊ฒฐ์ ์๋
- ๊ฒฐ๊ณผ:
- ์ฑ๊ณต: 1๊ฑด
- ์คํจ: 9๊ฑด (์ด๋ฏธ ๊ฒฐ์ ๋ ์ฃผ๋ฌธ)
- ์ฃผ๋ฌธ ์ํ: PAID (1ํ๋ง ๋ณ๊ฒฝ)
- ํฌ์ธํธ ์ฐจ๊ฐ: 1ํ๋ง ์คํ (์ ํํ ๊ธ์ก)
- ๊ฒ์ฆ ์๋ฃ: ์ค๋ณต ๊ฒฐ์ ๋ฐฉ์ง 100%
ํ ์คํธ ์ผ์ด์ค 2: ์ฌ๋ฌ ์ฌ์ฉ์์ ๋์ ๊ฒฐ์
- ์๋๋ฆฌ์ค: 5๋ช ์ ์ฌ์ฉ์๊ฐ ๊ฐ์์ ์ฃผ๋ฌธ์ ๋์์ ๊ฒฐ์
- ๊ฒฐ๊ณผ:
- ์ฑ๊ณต: 5๊ฑด (๋ชจ๋ ์ฑ๊ณต)
- ์คํจ: 0๊ฑด
- ๊ฐ ์ฌ์ฉ์์ ํฌ์ธํธ ์ ํํ ์ฐจ๊ฐ
- ๊ฐ ์ฃผ๋ฌธ ์ํ: PAID
- ๊ฒ์ฆ ์๋ฃ: ๋น๊ด์ ๋ฝ์ผ๋ก ์ฌ๊ณ ์ฐจ๊ฐ ์ ํ์ฑ ๋ณด์ฅ
ํ ์คํธ ์ผ์ด์ค 3: ํฌ์ธํธ ๋ถ์กฑ ์ ํธ๋์ญ์ ๋กค๋ฐฑ
- ์๋๋ฆฌ์ค: ํฌ์ธํธ๊ฐ ๋ถ์กฑํ ์ํ์์ ๊ฒฐ์ ์๋
- ๊ฒฐ๊ณผ:
- ๊ฒฐ์ ์คํจ (์์๋ ๋์)
- ํฌ์ธํธ: ์๋ ์์ก ์ ์ง (๋กค๋ฐฑ)
- ์ฌ๊ณ : ์ฐจ๊ฐ ์ ์ํ๋ก ๋ณต์ (๋กค๋ฐฑ)
- ์ฃผ๋ฌธ ์ํ: PENDING ์ ์ง
- ๊ฒ์ฆ ์๋ฃ:
@Transactional๋กค๋ฐฑ์ผ๋ก ์๋ ๋ณต์
ํ ์คํธ ํ์ผ: OrderServiceConcurrencyIntegrationTest.java
ํ ์คํธ ๋ด์ฉ:
- ๊ฒฐ์ ํ๋ก์ธ์ค ๋ด์์ ์ฟ ํฐ ์ฌ์ฉ ์ฒ๋ฆฌ
@Versionํ๋๋ฅผ ํตํ ์๋ ๋๊ด์ ๋ฝ ์ ์ฉ- ๊ฒฐ๊ณผ:
- ์ฟ ํฐ ์ค๋ณต ์ฌ์ฉ ๋ฐฉ์ง 100%
@Transactional๋กค๋ฐฑ์ผ๋ก ์ถฉ๋ ์์ ํ๊ฒ ์ฒ๋ฆฌ- ๋์์ฑ์ด ๋ฎ์ ์ถฉ๋ ๋ฐ์ ๊ฑฐ์ ์์
ํ ์คํธ ํ์ผ: OrderServiceConcurrencyIntegrationTest.java
ํ ์คํธ ๋ด์ฉ:
- ๊ฒฐ์ ํ๋ก์ธ์ค ๋ด์์ ํฌ์ธํธ ์ฐจ๊ฐ ์ฒ๋ฆฌ
@Versionํ๋๋ฅผ ํตํ ์๋ ๋๊ด์ ๋ฝ ์ ์ฉ- ๊ฒฐ๊ณผ:
- ์์ ์์ก ๋ฐ์ 0๊ฑด
- ๋ชจ๋ ๊ฒฐ์ ์์ ์ ํํ ํฌ์ธํธ ์ฐจ๊ฐ
@Transactional๋กค๋ฐฑ์ผ๋ก ๋ฐ์ดํฐ ์ ํฉ์ฑ ๋ณด์ฅ
ํ ์คํธ ํ์ผ: PointServiceConcurrencyIntegrationTest.java
ํ ์คํธ ์ผ์ด์ค: ๋์ ํฌ์ธํธ ์ถฉ์
- ์๋๋ฆฌ์ค: 10๊ฐ ์ค๋ ๋์์ ๋์์ 10,000์ ์ถฉ์ ์์ฒญ
- ์ค์ : maxAttempts=5, backoff=100ms
- ๊ฒฐ๊ณผ:
- ์ฑ๊ณต: 9-10๊ฑด (์ฌ์๋๋ก ๋๋ถ๋ถ ์ฑ๊ณต)
- Recover ํธ์ถ: 0-1๊ฑด (์ต๋ ์ฌ์๋ ํ์ ์ด๊ณผ ์)
- ์ต์ข ์์ก: ์ด๊ธฐ ์์ก + (์ฑ๊ณต ํ์ ร 10,000์)
- ๊ฒ์ฆ ์๋ฃ: ์ถฉ์ ๋๋ฝ 0๊ฑด, ๊ธ์ก ์ ํ์ฑ 100%
| ๋์์ฑ ์ ์ด ์ง์ | ํ ์คํธ ๋ฐฉ์ | ์ค๋ ๋ ์ | ์ฑ๊ณต๋ฅ | ์ ํ์ฑ | ๋น๊ณ |
|---|---|---|---|---|---|
| ์ฟ ํฐ ๋ฐ๊ธ | ๋น๊ด์ ๋ฝ | 20 | ์ฌ๊ณ ๋งํผ | 100% | ์ฌ๊ณ ์ด๊ณผ ๋ฐ๊ธ 0๊ฑด |
| ์ฌ๊ณ ์ฐจ๊ฐ | ๋น๊ด์ ๋ฝ | 5-10 | 100% | 100% | ๊ณผ๋งค ๋ฐ์ 0๊ฑด |
| ์ฟ ํฐ ์ฌ์ฉ | ๋๊ด์ ๋ฝ | - | 100% | 100% | ์ฌ์๋๋ก ๋ชจ๋ ์ฑ๊ณต |
| ํฌ์ธํธ ์ฐจ๊ฐ | ๋๊ด์ ๋ฝ | - | 100% | 100% | ์์ ์์ก 0๊ฑด |
| ํฌ์ธํธ ์ถฉ์ | ๋๊ด์ ๋ฝ | 10 | 90-100% | 100% | ์ฌ์๋๋ก ๋๋ถ๋ถ ์ฑ๊ณต |
๋น๊ด์ ๋ฝ ์ ์ฉ:
- ๋์์ฑ์ด ๋๊ณ ์ถฉ๋์ด ๋น๋ฒํ ๊ฒฝ์ฐ (์ฟ ํฐ ๋ฐ๊ธ, ์ฌ๊ณ ์ฐจ๊ฐ)
- ๋ฐ์ดํฐ ์ ํฉ์ฑ์ด ์ ๋์ ์ผ๋ก ์ค์ํ ๊ฒฝ์ฐ
- ์์ฐจ ์ฒ๋ฆฌ๋ก ์ธํ ๋๊ธฐ๊ฐ ๋น์ฆ๋์ค์ ์ผ๋ก ํ์ฉ๋๋ ๊ฒฝ์ฐ
๋๊ด์ ๋ฝ ์ ์ฉ:
- ๋์์ฑ์ด ๋ฎ๊ณ ์ถฉ๋์ด ๋๋ฌธ ๊ฒฝ์ฐ (์ฟ ํฐ ์ฌ์ฉ, ํฌ์ธํธ ๊ด๋ฆฌ)
- ๋น ๋ฅธ ์๋ต์ด ์ค์ํ ๊ฒฝ์ฐ
- ์ถฉ๋ ๋ฐ์ ์ ์ฌ์๋๋ก ํด๊ฒฐ ๊ฐ๋ฅํ ๊ฒฝ์ฐ
-
๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ ๋ณด์ฅ
- ์ฟ ํฐ ์ฌ๊ณ ์ด๊ณผ ๋ฐ๊ธ ๋ฐฉ์ง (100%)
- ์ํ ์ฌ๊ณ ๊ณผ๋งค ๋ฐฉ์ง (100%)
- ํฌ์ธํธ ์์ ์์ก ๋ฐฉ์ง (100%)
-
์ฑ๋ฅ ์ต์ ํ
- ๋์์ฑ์ด ๋ฎ์ ์์ : ๋๊ด์ ๋ฝ์ผ๋ก ์๋ต ์๊ฐ 15~20% ๊ฐ์
- ๋์์ฑ์ด ๋์ ์์ : ๋น๊ด์ ๋ฝ์ผ๋ก ์ ํ์ฑ ๋ณด์ฅ
-
์ฌ์ฉ์ ๊ฒฝํ ๊ฐ์
- ์ ์ฐฉ์ ์ด๋ฒคํธ: ๊ณต์ ํ ๊ธฐํ ์ ๊ณต ๋ฐ ๋ช ํํ ํ์ ์๋ด
- ์ผ๋ฐ ๊ฒฐ์ : ๋น ๋ฅธ ์๋ต์ผ๋ก ๋ง์กฑ๋ ํฅ์