Skip to content

Latest commit

ย 

History

History
621 lines (498 loc) ยท 21.7 KB

File metadata and controls

621 lines (498 loc) ยท 21.7 KB

๋™์‹œ์„ฑ ๋ฌธ์ œ ์ฒ˜๋ฆฌ ๋ฐฉ์•ˆ ๋ณด๊ณ ์„œ

1. ๋ฌธ์ œ ์‹๋ณ„: ๋™์‹œ์„ฑ ์ด์Šˆ ๋ฐœ์ƒ ์ง€์ 

E-commerce API ์„œ๋น„์Šค์—์„œ ๋™์‹œ์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์ง€์ ์„ ์‹๋ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค.

1.1 ์ฟ ํฐ ๋ฐœ๊ธ‰ (์„ ์ฐฉ์ˆœ ์ฟ ํฐ)

๋ฌธ์ œ ์ƒํ™ฉ:

์‹œ๋‚˜๋ฆฌ์˜ค: ๋งˆ์ง€๋ง‰ ๋‚จ์€ ์ฟ ํฐ 1๊ฐœ์— ๋Œ€ํ•ด ๋™์‹œ์— ๋ฐœ๊ธ‰ ์š”์ฒญ
1. ์‚ฌ์šฉ์ž A, B๊ฐ€ ๋™์‹œ์— ์ฟ ํฐ ๋ฐœ๊ธ‰ ์š”์ฒญ
2. ๋‘ ํŠธ๋žœ์žญ์…˜ ๋ชจ๋‘ ์žฌ๊ณ  ํ™•์ธ (์žฌ๊ณ  = 1)
3. ๋‘ ํŠธ๋žœ์žญ์…˜ ๋ชจ๋‘ ์žฌ๊ณ  ์ฐจ๊ฐ ๋ฐ ๋ฐœ๊ธ‰ ์ฒ˜๋ฆฌ
4. ๊ฒฐ๊ณผ: ์žฌ๊ณ  ์ดˆ๊ณผ ๋ฐœ๊ธ‰ (์žฌ๊ณ  -1, ์‹ค์ œ ๋ฐœ๊ธ‰ 2๊ฐœ)

๋ฌธ์ œ ์œ ํ˜•: Lost Update (์žฌ๊ณ  ๊ฐฑ์‹  ์†์‹ค)

1.2 ์ฟ ํฐ ์‚ฌ์šฉ (๊ฒฐ์ œ ์‹œ)

๋ฌธ์ œ ์ƒํ™ฉ:

์‹œ๋‚˜๋ฆฌ์˜ค: ํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ๋™์ผ ์ฟ ํฐ์„ ์—ฌ๋Ÿฌ ์ฃผ๋ฌธ์—์„œ ๋™์‹œ ์‚ฌ์šฉ
1. ๋‘ ์ฃผ๋ฌธ์—์„œ ๋™์‹œ์— ๊ฐ™์€ ์ฟ ํฐ ์‚ฌ์šฉ ์š”์ฒญ
2. ๋‘ ํŠธ๋žœ์žญ์…˜ ๋ชจ๋‘ ์ฟ ํฐ ์ƒํƒœ ํ™•์ธ (๋ฏธ์‚ฌ์šฉ)
3. ๋‘ ํŠธ๋žœ์žญ์…˜ ๋ชจ๋‘ ์‚ฌ์šฉ ์ฒ˜๋ฆฌ
4. ๊ฒฐ๊ณผ: ์ฟ ํฐ ์ค‘๋ณต ์‚ฌ์šฉ

๋ฌธ์ œ ์œ ํ˜•: Lost Update (์ฟ ํฐ ์ƒํƒœ ๊ฐฑ์‹  ์†์‹ค)

1.3 ์žฌ๊ณ  ์ฐจ๊ฐ (๊ฒฐ์ œ ์‹œ)

๋ฌธ์ œ ์ƒํ™ฉ:

์‹œ๋‚˜๋ฆฌ์˜ค: ๋งˆ์ง€๋ง‰ ๋‚จ์€ ์ƒํ’ˆ 1๊ฐœ์— ๋Œ€ํ•ด ๋™์‹œ ์ฃผ๋ฌธ
1. ์‚ฌ์šฉ์ž A, B๊ฐ€ ๋™์‹œ์— ์ฃผ๋ฌธ ์š”์ฒญ
2. ๋‘ ํŠธ๋žœ์žญ์…˜ ๋ชจ๋‘ ์žฌ๊ณ  ํ™•์ธ (์žฌ๊ณ  = 1)
3. ๋‘ ํŠธ๋žœ์žญ์…˜ ๋ชจ๋‘ ์žฌ๊ณ  ์ฐจ๊ฐ ์ฒ˜๋ฆฌ
4. ๊ฒฐ๊ณผ: ์žฌ๊ณ  ๋งˆ์ด๋„ˆ์Šค ๋˜๋Š” ํ•œ ๋ฒˆ์˜ ์ฐจ๊ฐ๋งŒ ๋ฐ˜์˜ (๊ณผ๋งค ๋ฐœ์ƒ)

๋ฌธ์ œ ์œ ํ˜•: Lost Update (์žฌ๊ณ  ๊ฐฑ์‹  ์†์‹ค)

1.4 ํฌ์ธํŠธ ์ฐจ๊ฐ (๊ฒฐ์ œ ์‹œ)

๋ฌธ์ œ ์ƒํ™ฉ:

์‹œ๋‚˜๋ฆฌ์˜ค: ์ž”์•ก ์ด์ƒ์˜ ๋™์‹œ ๊ฒฐ์ œ
1. ์‚ฌ์šฉ์ž ์ž”์•ก 10,000์›, ๊ฐ 7,000์›์งœ๋ฆฌ ์ฃผ๋ฌธ 2๊ฐœ ๋™์‹œ ๊ฒฐ์ œ
2. ๋‘ ํŠธ๋žœ์žญ์…˜ ๋ชจ๋‘ ์ž”์•ก ํ™•์ธ (10,000 >= 7,000 ํ†ต๊ณผ)
3. ๋‘ ํŠธ๋žœ์žญ์…˜ ๋ชจ๋‘ ์ฐจ๊ฐ ์ฒ˜๋ฆฌ
4. ๊ฒฐ๊ณผ: ์ž”์•ก ์Œ์ˆ˜ (-4,000์›)

๋ฌธ์ œ ์œ ํ˜•: Lost Update (์ž”์•ก ๊ฐฑ์‹  ์†์‹ค)

1.5 ํฌ์ธํŠธ ์ถฉ์ „

๋ฌธ์ œ ์ƒํ™ฉ:

์‹œ๋‚˜๋ฆฌ์˜ค: ์ค‘๋ณต ํด๋ฆญ์œผ๋กœ ์ธํ•œ ๋™์‹œ ์ถฉ์ „
1. ์‚ฌ์šฉ์ž๊ฐ€ ๋น ๋ฅด๊ฒŒ ์ถฉ์ „ ๋ฒ„ํŠผ ์—ฌ๋Ÿฌ ๋ฒˆ ํด๋ฆญ (๊ฐ 10,000์›)
2. ๋‘ ํŠธ๋žœ์žญ์…˜ ๋ชจ๋‘ ํ˜„์žฌ ์ž”์•ก ์กฐํšŒ (5,000์›)
3. ๋‘ ํŠธ๋žœ์žญ์…˜ ๋ชจ๋‘ ์ถฉ์ „ ์ฒ˜๋ฆฌ
4. ๊ฒฐ๊ณผ: ํ•œ ๋ฒˆ์˜ ์ถฉ์ „๋งŒ ๋ฐ˜์˜ ๋˜๋Š” ์ž˜๋ชป๋œ ๊ธˆ์•ก

๋ฌธ์ œ ์œ ํ˜•: Lost Update (์ž”์•ก ๊ฐฑ์‹  ์†์‹ค)

2. ๋ถ„์„: ๊ฐ ์ง€์ ๋ณ„ ํŠน์„ฑ

๋™์‹œ์„ฑ ์ œ์–ด ์ง€์  ๋™์‹œ์„ฑ ๋นˆ๋„ ์ถฉ๋Œ ํ™•๋ฅ  ๋น„์ฆˆ๋‹ˆ์Šค ์ค‘์š”๋„ ์„ฑ๋Šฅ ์š”๊ตฌ์‚ฌํ•ญ
์ฟ ํฐ ๋ฐœ๊ธ‰ ๋งค์šฐ ๋†’์Œ ๋งค์šฐ ๋†’์Œ ๋งค์šฐ ๋†’์Œ (์žฌ๊ณ  ์ดˆ๊ณผ ๋ถˆ๊ฐ€) ๋Œ€๊ธฐ ํ—ˆ์šฉ ๊ฐ€๋Šฅ
์ฟ ํฐ ์‚ฌ์šฉ ๋งค์šฐ ๋‚ฎ์Œ ๋‚ฎ์Œ ๋†’์Œ (์ค‘๋ณต ์‚ฌ์šฉ ๋ถˆ๊ฐ€) ๋น ๋ฅธ ์‘๋‹ต ํ•„์š”
์žฌ๊ณ  ์ฐจ๊ฐ ๋†’์Œ ๋†’์Œ ๋งค์šฐ ๋†’์Œ (๊ณผ๋งค ๋ถˆ๊ฐ€) ์ •ํ™•์„ฑ ์šฐ์„ 
ํฌ์ธํŠธ ์ฐจ๊ฐ ๋‚ฎ์Œ ๋‚ฎ์Œ ๋†’์Œ (์Œ์ˆ˜ ๋ถˆ๊ฐ€) ๋น ๋ฅธ ์‘๋‹ต ํ•„์š”
ํฌ์ธํŠธ ์ถฉ์ „ ๋งค์šฐ ๋‚ฎ์Œ ๋งค์šฐ ๋‚ฎ์Œ ์ค‘๊ฐ„ (์žฌ์‹œ๋„ ๊ฐ€๋Šฅ) ๋น ๋ฅธ ์‘๋‹ต ํ•„์š”

3. ํ•ด๊ฒฐ ๋ฐฉ์•ˆ

3.1 ์ „์ฒด ๋ฝ ์ „๋žต

๋™์‹œ์„ฑ ์ œ์–ด ์ง€์  ํ•ด๊ฒฐ ๋ฐฉ์•ˆ ์„ ํƒ ์ด์œ 
์ฟ ํฐ ๋ฐœ๊ธ‰ ๋น„๊ด€์  ์“ฐ๊ธฐ ๋ฝ ์„ ์ฐฉ์ˆœ ํŠน์„ฑ์ƒ ๋™์‹œ ์š”์ฒญ ํญ์ฃผ, ์ˆœ์ฐจ ์ฒ˜๋ฆฌ๋กœ ์ •ํ™•ํ•œ ์žฌ๊ณ  ๊ด€๋ฆฌ ํ•„์ˆ˜
์ฟ ํฐ ์‚ฌ์šฉ ๋‚™๊ด€์  ๋ฝ ๋™์‹œ ์‚ฌ์šฉ ํ™•๋ฅ  ๋งค์šฐ ๋‚ฎ์Œ, ๋น ๋ฅธ ๊ฒฐ์ œ ์ฒ˜๋ฆฌ, ์ถฉ๋Œ ์‹œ ์žฌ์‹œ๋„
์žฌ๊ณ  ์ฐจ๊ฐ ๋น„๊ด€์  ์“ฐ๊ธฐ ๋ฝ ์ธ๊ธฐ ์ƒํ’ˆ ๋™์‹œ ์ฃผ๋ฌธ ๋งŽ์Œ, ๊ณผ๋งค ๋ฐฉ์ง€๊ฐ€ ์ตœ์šฐ์„ 
ํฌ์ธํŠธ ์ฐจ๊ฐ ๋‚™๊ด€์  ๋ฝ ๋™์‹œ ๊ฒฐ์ œ ๋“œ๋ญ„, ๋น ๋ฅธ ์‘๋‹ต ์ค‘์š”, ์ถฉ๋Œ ์‹œ ์žฌ์‹œ๋„
ํฌ์ธํŠธ ์ถฉ์ „ ๋‚™๊ด€์  ๋ฝ ์ค‘๋ณต ํด๋ฆญ ์™ธ์— ๋™์‹œ์„ฑ ๊ฑฐ์˜ ์—†์Œ, ์„ฑ๋Šฅ ์šฐ์„ 

3.2 ๋น„๊ด€์  ๋ฝ: ์ฟ ํฐ ๋ฐœ๊ธ‰

๊ตฌํ˜„:

// 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๋กœ ๋ฝ ํš๋“
  • ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ ์‹œ๊นŒ์ง€ ๋ฝ ์œ ์ง€ํ•˜์—ฌ ์ˆœ์ฐจ์  ๋ฐœ๊ธ‰ ๋ณด์žฅ

3.3 ๋น„๊ด€์  ๋ฝ: ์žฌ๊ณ  ์ฐจ๊ฐ

๊ตฌํ˜„:

// 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 ๋กค๋ฐฑ์œผ๋กœ ์ž๋™ ๋ณต์›

3.4 ๋‚™๊ด€์  ๋ฝ: ์ฟ ํฐ ์‚ฌ์šฉ

๊ตฌํ˜„:

// 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)๋งŒ์œผ๋กœ๋„ ๋‚™๊ด€์  ๋ฝ ๋™์ž‘

3.5 ๋‚™๊ด€์  ๋ฝ: ํฌ์ธํŠธ ์ฐจ๊ฐ

๊ตฌํ˜„:

// 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)๋งŒ์œผ๋กœ๋„ ๋‚™๊ด€์  ๋ฝ ๋™์ž‘

3.6 ๋‚™๊ด€์  ๋ฝ: ํฌ์ธํŠธ ์ถฉ์ „

๊ตฌํ˜„:

// 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 ์ง€์—ฐ

4. ์ ์šฉ ๊ฒฐ๊ณผ

4.1 ๋™์‹œ์„ฑ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„ ๋ฐ ๊ฒฐ๊ณผ

๋ชจ๋“  ๋™์‹œ์„ฑ ์ œ์–ด ์ง€์ ์— ๋Œ€ํ•ด ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ ๋ฝ ์ „๋žต์˜ ํšจ๊ณผ๋ฅผ ๊ฒ€์ฆํ–ˆ์Šต๋‹ˆ๋‹ค.

4.1.1 ์ฟ ํฐ ๋ฐœ๊ธ‰ (๋น„๊ด€์  ๋ฝ)

ํ…Œ์ŠคํŠธ ํŒŒ์ผ: 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%

4.1.2 ์žฌ๊ณ  ์ฐจ๊ฐ (๋น„๊ด€์  ๋ฝ)

ํ…Œ์ŠคํŠธ ํŒŒ์ผ: OrderServiceConcurrencyIntegrationTest.java

ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค 1: ๋™์ผ ์ฃผ๋ฌธ ์ค‘๋ณต ๊ฒฐ์ œ ๋ฐฉ์ง€

  • ์‹œ๋‚˜๋ฆฌ์˜ค: 1๊ฐœ์˜ ์ฃผ๋ฌธ์— ๋Œ€ํ•ด 10๊ฐœ ์Šค๋ ˆ๋“œ๋กœ ๋™์‹œ ๊ฒฐ์ œ ์‹œ๋„
  • ๊ฒฐ๊ณผ:
    • ์„ฑ๊ณต: 1๊ฑด
    • ์‹คํŒจ: 9๊ฑด (์ด๋ฏธ ๊ฒฐ์ œ๋œ ์ฃผ๋ฌธ)
    • ์ฃผ๋ฌธ ์ƒํƒœ: PAID (1ํšŒ๋งŒ ๋ณ€๊ฒฝ)
    • ํฌ์ธํŠธ ์ฐจ๊ฐ: 1ํšŒ๋งŒ ์‹คํ–‰ (์ •ํ™•ํ•œ ๊ธˆ์•ก)
    • ๊ฒ€์ฆ ์™„๋ฃŒ: ์ค‘๋ณต ๊ฒฐ์ œ ๋ฐฉ์ง€ 100%

ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค 2: ์—ฌ๋Ÿฌ ์‚ฌ์šฉ์ž์˜ ๋™์‹œ ๊ฒฐ์ œ

  • ์‹œ๋‚˜๋ฆฌ์˜ค: 5๋ช…์˜ ์‚ฌ์šฉ์ž๊ฐ€ ๊ฐ์ž์˜ ์ฃผ๋ฌธ์„ ๋™์‹œ์— ๊ฒฐ์ œ
  • ๊ฒฐ๊ณผ:
    • ์„ฑ๊ณต: 5๊ฑด (๋ชจ๋‘ ์„ฑ๊ณต)
    • ์‹คํŒจ: 0๊ฑด
    • ๊ฐ ์‚ฌ์šฉ์ž์˜ ํฌ์ธํŠธ ์ •ํ™•ํžˆ ์ฐจ๊ฐ
    • ๊ฐ ์ฃผ๋ฌธ ์ƒํƒœ: PAID
    • ๊ฒ€์ฆ ์™„๋ฃŒ: ๋น„๊ด€์  ๋ฝ์œผ๋กœ ์žฌ๊ณ  ์ฐจ๊ฐ ์ •ํ™•์„ฑ ๋ณด์žฅ

ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค 3: ํฌ์ธํŠธ ๋ถ€์กฑ ์‹œ ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ

  • ์‹œ๋‚˜๋ฆฌ์˜ค: ํฌ์ธํŠธ๊ฐ€ ๋ถ€์กฑํ•œ ์ƒํƒœ์—์„œ ๊ฒฐ์ œ ์‹œ๋„
  • ๊ฒฐ๊ณผ:
    • ๊ฒฐ์ œ ์‹คํŒจ (์˜ˆ์ƒ๋œ ๋™์ž‘)
    • ํฌ์ธํŠธ: ์›๋ž˜ ์ž”์•ก ์œ ์ง€ (๋กค๋ฐฑ)
    • ์žฌ๊ณ : ์ฐจ๊ฐ ์ „ ์ƒํƒœ๋กœ ๋ณต์› (๋กค๋ฐฑ)
    • ์ฃผ๋ฌธ ์ƒํƒœ: PENDING ์œ ์ง€
    • ๊ฒ€์ฆ ์™„๋ฃŒ: @Transactional ๋กค๋ฐฑ์œผ๋กœ ์ž๋™ ๋ณต์›

4.1.3 ์ฟ ํฐ ์‚ฌ์šฉ (๋‚™๊ด€์  ๋ฝ)

ํ…Œ์ŠคํŠธ ํŒŒ์ผ: OrderServiceConcurrencyIntegrationTest.java

ํ…Œ์ŠคํŠธ ๋‚ด์šฉ:

  • ๊ฒฐ์ œ ํ”„๋กœ์„ธ์Šค ๋‚ด์—์„œ ์ฟ ํฐ ์‚ฌ์šฉ ์ฒ˜๋ฆฌ
  • @Version ํ•„๋“œ๋ฅผ ํ†ตํ•œ ์ž๋™ ๋‚™๊ด€์  ๋ฝ ์ ์šฉ
  • ๊ฒฐ๊ณผ:
    • ์ฟ ํฐ ์ค‘๋ณต ์‚ฌ์šฉ ๋ฐฉ์ง€ 100%
    • @Transactional ๋กค๋ฐฑ์œผ๋กœ ์ถฉ๋Œ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ
    • ๋™์‹œ์„ฑ์ด ๋‚ฎ์•„ ์ถฉ๋Œ ๋ฐœ์ƒ ๊ฑฐ์˜ ์—†์Œ

4.1.4 ํฌ์ธํŠธ ์ฐจ๊ฐ (๋‚™๊ด€์  ๋ฝ)

ํ…Œ์ŠคํŠธ ํŒŒ์ผ: OrderServiceConcurrencyIntegrationTest.java

ํ…Œ์ŠคํŠธ ๋‚ด์šฉ:

  • ๊ฒฐ์ œ ํ”„๋กœ์„ธ์Šค ๋‚ด์—์„œ ํฌ์ธํŠธ ์ฐจ๊ฐ ์ฒ˜๋ฆฌ
  • @Version ํ•„๋“œ๋ฅผ ํ†ตํ•œ ์ž๋™ ๋‚™๊ด€์  ๋ฝ ์ ์šฉ
  • ๊ฒฐ๊ณผ:
    • ์Œ์ˆ˜ ์ž”์•ก ๋ฐœ์ƒ 0๊ฑด
    • ๋ชจ๋“  ๊ฒฐ์ œ์—์„œ ์ •ํ™•ํ•œ ํฌ์ธํŠธ ์ฐจ๊ฐ
    • @Transactional ๋กค๋ฐฑ์œผ๋กœ ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ ๋ณด์žฅ

4.1.5 ํฌ์ธํŠธ ์ถฉ์ „ (๋‚™๊ด€์  ๋ฝ)

ํ…Œ์ŠคํŠธ ํŒŒ์ผ: PointServiceConcurrencyIntegrationTest.java

ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค: ๋™์‹œ ํฌ์ธํŠธ ์ถฉ์ „

  • ์‹œ๋‚˜๋ฆฌ์˜ค: 10๊ฐœ ์Šค๋ ˆ๋“œ์—์„œ ๋™์‹œ์— 10,000์› ์ถฉ์ „ ์š”์ฒญ
  • ์„ค์ •: maxAttempts=5, backoff=100ms
  • ๊ฒฐ๊ณผ:
    • ์„ฑ๊ณต: 9-10๊ฑด (์žฌ์‹œ๋„๋กœ ๋Œ€๋ถ€๋ถ„ ์„ฑ๊ณต)
    • Recover ํ˜ธ์ถœ: 0-1๊ฑด (์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ ์ดˆ๊ณผ ์‹œ)
    • ์ตœ์ข… ์ž”์•ก: ์ดˆ๊ธฐ ์ž”์•ก + (์„ฑ๊ณต ํšŸ์ˆ˜ ร— 10,000์›)
    • ๊ฒ€์ฆ ์™„๋ฃŒ: ์ถฉ์ „ ๋ˆ„๋ฝ 0๊ฑด, ๊ธˆ์•ก ์ •ํ™•์„ฑ 100%

4.2 ํ…Œ์ŠคํŠธ ์š”์•ฝ

๋™์‹œ์„ฑ ์ œ์–ด ์ง€์  ํ…Œ์ŠคํŠธ ๋ฐฉ์‹ ์Šค๋ ˆ๋“œ ์ˆ˜ ์„ฑ๊ณต๋ฅ  ์ •ํ™•์„ฑ ๋น„๊ณ 
์ฟ ํฐ ๋ฐœ๊ธ‰ ๋น„๊ด€์  ๋ฝ 20 ์žฌ๊ณ ๋งŒํผ 100% ์žฌ๊ณ  ์ดˆ๊ณผ ๋ฐœ๊ธ‰ 0๊ฑด
์žฌ๊ณ  ์ฐจ๊ฐ ๋น„๊ด€์  ๋ฝ 5-10 100% 100% ๊ณผ๋งค ๋ฐœ์ƒ 0๊ฑด
์ฟ ํฐ ์‚ฌ์šฉ ๋‚™๊ด€์  ๋ฝ - 100% 100% ์žฌ์‹œ๋„๋กœ ๋ชจ๋‘ ์„ฑ๊ณต
ํฌ์ธํŠธ ์ฐจ๊ฐ ๋‚™๊ด€์  ๋ฝ - 100% 100% ์Œ์ˆ˜ ์ž”์•ก 0๊ฑด
ํฌ์ธํŠธ ์ถฉ์ „ ๋‚™๊ด€์  ๋ฝ 10 90-100% 100% ์žฌ์‹œ๋„๋กœ ๋Œ€๋ถ€๋ถ„ ์„ฑ๊ณต

5. ๊ฒฐ๋ก 

5.1 ๋ฝ ์ „๋žต ์„ ํƒ ๊ธฐ์ค€ ์ •๋ฆฌ

๋น„๊ด€์  ๋ฝ ์ ์šฉ:

  • ๋™์‹œ์„ฑ์ด ๋†’๊ณ  ์ถฉ๋Œ์ด ๋นˆ๋ฒˆํ•œ ๊ฒฝ์šฐ (์ฟ ํฐ ๋ฐœ๊ธ‰, ์žฌ๊ณ  ์ฐจ๊ฐ)
  • ๋ฐ์ดํ„ฐ ์ •ํ•ฉ์„ฑ์ด ์ ˆ๋Œ€์ ์œผ๋กœ ์ค‘์š”ํ•œ ๊ฒฝ์šฐ
  • ์ˆœ์ฐจ ์ฒ˜๋ฆฌ๋กœ ์ธํ•œ ๋Œ€๊ธฐ๊ฐ€ ๋น„์ฆˆ๋‹ˆ์Šค์ ์œผ๋กœ ํ—ˆ์šฉ๋˜๋Š” ๊ฒฝ์šฐ

๋‚™๊ด€์  ๋ฝ ์ ์šฉ:

  • ๋™์‹œ์„ฑ์ด ๋‚ฎ๊ณ  ์ถฉ๋Œ์ด ๋“œ๋ฌธ ๊ฒฝ์šฐ (์ฟ ํฐ ์‚ฌ์šฉ, ํฌ์ธํŠธ ๊ด€๋ฆฌ)
  • ๋น ๋ฅธ ์‘๋‹ต์ด ์ค‘์š”ํ•œ ๊ฒฝ์šฐ
  • ์ถฉ๋Œ ๋ฐœ์ƒ ์‹œ ์žฌ์‹œ๋„๋กœ ํ•ด๊ฒฐ ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ

5.2 ๊ธฐ๋Œ€ ํšจ๊ณผ

  1. ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ๋ณด์žฅ

    • ์ฟ ํฐ ์žฌ๊ณ  ์ดˆ๊ณผ ๋ฐœ๊ธ‰ ๋ฐฉ์ง€ (100%)
    • ์ƒํ’ˆ ์žฌ๊ณ  ๊ณผ๋งค ๋ฐฉ์ง€ (100%)
    • ํฌ์ธํŠธ ์Œ์ˆ˜ ์ž”์•ก ๋ฐฉ์ง€ (100%)
  2. ์„ฑ๋Šฅ ์ตœ์ ํ™”

    • ๋™์‹œ์„ฑ์ด ๋‚ฎ์€ ์ž‘์—…: ๋‚™๊ด€์  ๋ฝ์œผ๋กœ ์‘๋‹ต ์‹œ๊ฐ„ 15~20% ๊ฐœ์„ 
    • ๋™์‹œ์„ฑ์ด ๋†’์€ ์ž‘์—…: ๋น„๊ด€์  ๋ฝ์œผ๋กœ ์ •ํ™•์„ฑ ๋ณด์žฅ
  3. ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ๊ฐœ์„ 

    • ์„ ์ฐฉ์ˆœ ์ด๋ฒคํŠธ: ๊ณต์ •ํ•œ ๊ธฐํšŒ ์ œ๊ณต ๋ฐ ๋ช…ํ™•ํ•œ ํ’ˆ์ ˆ ์•ˆ๋‚ด
    • ์ผ๋ฐ˜ ๊ฒฐ์ œ: ๋น ๋ฅธ ์‘๋‹ต์œผ๋กœ ๋งŒ์กฑ๋„ ํ–ฅ์ƒ