CHAP08 - DDD-START/ONLINE-STUDY GitHub Wiki

8 ์• ๊ทธ๋ฆฌ๊ฑฐํŠธ ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ

์ด ์žฅ์—์„œ ๋‹ค๋ฃฐ ๋‚ด์šฉ

  • ์• ๊ทธ๋ฆฌ๊ฑฐํŠธ์˜ ํŠธ๋žœ์žญ์…˜
  • ์• ๊ทธ๋ฆฌ๊ฑฐํŠธ ์ž ๊ธˆ ๊ธฐ๋ฒ•

์• ๊ทธ๋ฆฌ๊ฑฐํŠธ์™€ ํŠธ๋žœ์žญ์…˜

ํ•œ ์• ๊ทธ๋ฆฌ๊ฑฐํŠธ(์ฃผ๋ฌธ)๋ฅผ ๋‘ ์‚ฌ์šฉ์ž๊ฐ€ ๊ฑฐ์˜ ๋™์‹œ์— ๋ณ€๊ฒฝํ•  ๋•Œ ํŠธ๋žœ์žญ์…˜์ด ํ•„์š”ํ•˜๋‹ค.

๋ฌธ์ œ์ : ๋ฉ”๋ชจ๋ฆฌ ์บ์‹œ ์‚ฌ์šฉ ์•ˆ ํ•˜๋ฉด, ๋‹ค๋ฅธ ๊ฐ์ฒด๋ฅผ ๊ตฌํ•ด์˜จ๋‹ค.

์šด์˜์ž๋Š” ๋ฐฐ์†ก์ค‘ ์ƒํƒœ๋กœ ๋ณ€๊ฒฝํ–ˆ๋Š”๋ฐ ๊ทธ ์‚ฌ์ด ๊ณ ๊ฐ์€ ๋ฐฐ์†ก์ง€ ์ •๋ณด๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜๋„ ์žˆ๊ฒŒ ๋œ๋‹ค. (์ผ๊ด€์„ฑ x)

image

ํŠธ๋žœ์žญ์…˜์„ ์ด์šฉํ•œ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

  • ์šด์˜์ž๊ฐ€ ๋ณ€๊ฒฝํ•˜๋Š” ๋™์•ˆ ๊ณ ๊ฐ์ด ์• ๊ทธ๋ฆฌ๊ฑฐํŠธ๋ฅผ ์ˆ˜์ • ๋ชปํ•˜๊ฒŒ ํ•˜๋˜๊ฐ€,
  • ์šด์˜์ž๊ฐ€ ์กฐํšŒํ•œ ์ดํ›„ ๊ณ ๊ฐ์ด ์ •๋ณด๋ฅผ ๋ณ€๊ฒฝํ•˜๋ฉด ์šด์˜์ž๊ฐ€ (์• ๊ทธ๋ฆฌ๊ฑฐํŠธ๋ฅผ) ๋‹ค์‹œ ์กฐํšŒํ•œ ๋’ค ์ˆ˜์ •ํ•˜๋„๋ก ํ•œ๋‹ค.

๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๋Š” ํŠธ๋žœ์žญ์…˜ ์ข…๋ฅ˜

  • ์„ ์  (Pessimistic)
  • ๋น„์„ ์  (Optimistic)

์„ ์  ์ž ๊ธˆ

  • ์‚ฌ์šฉ์ด ๋๋‚  ๋•Œ๊นŒ์ง€ ๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ๊ฐ€ ํ•ด๋‹น ์• ๊ทธ๋ฆฌ๊ฑฐํŠธ๋ฅผ ์ˆ˜์ •ํ•˜๋Š” ๊ฒƒ์„ ๋ง‰๋Š” ๋ฐฉ์‹
  • ๋ณดํ†ต DBMS๊ฐ€ ์ œ๊ณตํ•˜๋Š” ํ–‰ (row) ๋‹จ์œ„ ์ž ๊ธˆ์„ ์‚ฌ์šฉํ•œ๋‹ค

์Šค๋ ˆ๋“œ 2๊ฐ€ ๋ธ”๋กœํ‚น ๋˜์—ˆ๋‹ค

image

์•ž์— ์˜ˆ์ œ์— ์ ์šฉ์‹œ

  • ๊ณ ๊ฐ์€ "์ด๋ฏธ ๋ฐฐ์†ก์ด ์‹œ์ž‘๋˜์–ด ๋ฐฐ์†ก์ง€ ๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" ์•ˆ๋‚ด ๋ฌธ๊ตฌ ํ™•์ธ

image

JPA ์—์„œ Pessimistic Lock ์‚ฌ์šฉ์‹œ PESSIMISTIC_WRITE

Order order = entityManager.find(Order.class, orderNo, LockModeType.PESSIMISTIC_WRITE);

์„ ์  ์ž ๊ธˆ๊ณผ ๊ต์ฐฉ ์ƒํƒœ (Deadlock)

์„ ์  ์ž ๊ทผ์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ฐ๋“œ๋ฝ ๋ฐœ์ƒ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๋‹ค (ํŠนํžˆ ์‚ฌ์šฉ์ž ๋งŽ์„๋•Œ).

๋ฐ๋“œ๋ฝ ๋ฐœ์ƒ ์‹œ๋‚˜๋ฆฌ์˜ค

  • ์Šค๋ ˆ๋“œ1: A ์• ๊ทธ๋ฆฌ๊ฑฐํŠธ์— ๋Œ€ํ•œ ์„ ์  ์ž ๊ธˆ ๊ตฌํ•จ
  • ์Šค๋ ˆ๋“œ2: B ์• ๊ทธ๋ฆฌ๊ฑฐํŠธ์— ๋Œ€ํ•œ ์„ ์  ์ž ๊ธˆ ๊ตฌํ•จ
  • ์Šค๋ ˆ๋“œ1: B ์• ๊ทธ๋ฆฌ๊ฑฐํŠธ์— ๋Œ€ํ•œ ์„ ์  ์ž ๊ธˆ ์‹œ๋„
  • ์Šค๋ ˆ๋“œ2: A ์• ๊ทธ๋ฆฌ๊ฑฐํŠธ์— ๋Œ€ํ•œ ์„ ์  ์ž ๊ธˆ ์‹œ๋„

์„œ๋กœ ์ž ๊ทธ๊ณ  ์žˆ๋Š” ์ž์›์„ ๊ธฐ๋‹ค๋ฆฌ๊ณ  ์žˆ๋‹ค.

// ํ•ด๊ฒฐ๋ฐฉ๋ฒ•: Time Out ์„ค์ •
Map<String, Object> hints = new HashMap<>();
hints.put("javax.persistence.lock.timeout", 2000); // 2์ดˆ
Order order = entityManager.find( Order.class, orderNo, LockModeType.PESSIMISTIC_WRITE, hints);

์ฃผ์˜

DBMS์— ๋”ฐ๋ผ ํžŒํŠธ๊ฐ€ ์ ์šฉ๋˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋‹ค. ๋ฒค๋”์— ๋”ฐ๋ผ, ๋Œ€๊ธฐ ์‹œ๊ฐ„์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ• ํ™•์ธ ํ›„ ์‚ฌ์šฉํ•˜๊ธฐ.

๋น„์„ ์  ์ž ๊ธˆ

  • ์ž ๊ธˆ์„ ํ•ด์„œ ๋™์‹œ์— ์ ‘๊ทผํ•˜๋Š” ๊ฒƒ์„ ๋ง‰๋Š” ๋Œ€์‹  ๋ณ€๊ฒฝํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์‹ค์ œ DBMS์— ๋ฐ˜์˜ํ•˜๋Š” ์‹œ์ ์— ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค.

์„ ์  ๋ฐฉ์‹์œผ๋กœ ํ•ด๊ฒฐํ•  ์ˆ˜ ์—†๋Š” ์‹œ๋‚˜๋ฆฌ์˜ค

image

๋ฌธ์ œ์ : ์šด์˜์ž๋Š” ๋‹ค๋ฅธ ๋ฐฐ์†ก์ง€๋กœ ๋ฌผ๊ฑด์„ ๋ฐœ์†กํ•˜๊ฒŒ ๋˜๊ณ , ๋ฐฐ์†ก์€ ์—‰๋šฑํ•œ ๊ณณ์œผ๋กœ~

๊ตฌํ˜„ ๋ฐฉ๋ฒ•

// ๋ฒ„์ „ ์‚ฌ์šฉ: 1์”ฉ ์ฆ๊ฐ€
UPDATE aggtable SET version = version + 1, colx = ?, coly = ?
WHERE aggid = ? and version = ํ˜„์žฌ ๋ฒ„์ „

์„œ๋กœ ๊ฐ™์€ ์• ๊ทธ๋ฆฌ๊ฑฐํŠธ ์ˆ˜์ •์‹œ

image

JPA์˜ @Version ์‚ฌ์šฉ์‹œ

@Entity
@Table(name = "purchase_order")
@Access(AccessType.FIELD)
public class Order {
    @EmbeddedId
    private OrderNo number;
    @Version // UPDATE ์ฟผ๋ฆฌ ์‹คํ–‰ ๋ฒ„์ „ UP
    private long version;
    //...
}

์ฟผ๋ฆฌ

UPDATE purchase_order SET ...์ƒ๋žต, version = version + 1
WHERE number = ? and version = 10

์ปจํŠธ๋กค๋Ÿฌ์™€ ์‘์šฉ ์„œ๋น„์Šค ์ฝ”๋“œ

@Controller
public class OrderController {
    private ChangeShippingService changeShippingService;
    @RequestMapping(value = "/changeshipping", method = RequestMethod.POST)
    public String changeShipping(ChangeShippingRequest changeReq) {
        try {
            ChangeShippingService.changeShipping(changeReq);
            return "changeShippingSuccess";
        } catch(OptimisticLockingFailureException ex) {
            // ํŠธ๋žœ์žญ์…˜ ์ถฉ๋Œ ๋ฉ”์‹œ์ง€
            return "changeShippingTxConflict";
        }
        //...
    }
}

public class ChangeShippingService {
    @Transactional
    public void changeShipping(ChangeShippingRequest changeReq) {
        Order order = orderRepository.findById(new OrderNo(changeReq. getNumber()));
        checkNoOrder(order);
        order.changeShippingInfo(changeReq.getShippingInfo()); // ํŠธ๋žœ์žญ์…˜์ด ์ถฉ๋Œ์‹œ ์—ฌ๊ธฐ์„œ OptimisticLockingFailureException ์˜ˆ์™ธ ๋ฐœ์ƒ (์•„๋ž˜์ฝ”๋“œํ™•์ธ)
    }
    //...
}

์ด์ „ ์‚ฌ๋ก€์— ์ ์šฉ์‹œ ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ

image

๋น„์„ ์  ์ž ๊ธˆ ๋ฐฉ์‹์„ ์—ฌ๋Ÿฌ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ํ™•์žฅํ•˜๋ ค๋ฉด ๋ฒ„์ „ ์ •๋ณด๋„ ํ™”๋ฉด ๋ทฐ์— ์ „๋‹ฌํ•œ๋‹ค.

<!-- ์• ๊ทธ๋ฆฌ๊ฑฐํŠธ ์ •๋ณด๋ฅผ ๋ณด์—ฌ์ค„ ๋•Œ ๋ทฐ ์ฝ”๋“œ๋Š” ๋ฒ„์ „ ๊ฐ’์„ ํ•จ๊ป˜ ์ „์†กํ•œ๋‹ค. ->
<form action="startShipping" method="post">
    < input type="hidden" name="version" value="${orderDto.version}" > // ๋ฒ„์ „ 
    <input type="text" name="orderNumber" value="${orderDto.orderNumber}"
    readonly>
    //....
    <input type="submit" value="๋ฐฐ์†ก ์ƒํƒœ๋กœ ๋ณ€๊ฒฝํ•˜๊ธฐ">
</form>

์‚ฌ์šฉ์ž ์š”์ฒญ ์ฒ˜๋ฆฌ ๋ฐ์ดํ„ฐ ๋ฐ›์„๋•Œ

public class StartShippingRequest {
    private String orderNumber;
    private long version;
    //...์ƒ์„ฑ์ž, getter
}

์šด์˜์ž๊ฐ€ ๋ฐฐ์†ก ์ƒํƒœ ๋ณ€๊ฒฝ ์ปจํŠธ๋กค๋Ÿฌ์™€ ์„œ๋น„์Šค

  • 2 ์ข…๋ฅ˜ ์˜ˆ์™ธ: ๊ฐœ๋ฐœ์ž์—๊ฒŒ ํŠธ๋žœ์žญ์…˜ ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•œ ์‹œ์ ์ด ๋‹ค๋ฅธ ๊ฒƒ์„ ๋ช…ํ™•ํžˆ ์•Œ ์ˆ˜ ์žˆ๊ฒŒ ํ•œ๋‹ค.
    • VersionConflictExcepton = ์ด๋ฏธ ๋ˆ„๊ตฐ๊ฐ€๊ฐ€ ์• ๊ทธ๋ฆฌ๊ฑฐํŠธ๋ฅผ ์ˆ˜์ •ํ–ˆ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธ
    • OptimisticLockingFailureException = ๋ˆ„๊ตฐ๊ฐ€๊ฐ€ ๊ฑฐ์˜ ๋™์‹œ์— ์• ๊ทธ๋ฆฌ๊ฑฐํŠธ๋ฅผ ์ˆ˜์ •ํ–ˆ๋‹ค๋Š” ๊ฒƒ
@Controller
public class OrderAdminController {
    private StartShippingService startShippingService;
    @RequestMapping(value = "/startShipping", method = RequestMethod.POST)
    public String startshipping(StartShippingRequest startReq) {
        try {
            startShippingService.startShipping(startReq);
            return "shippingstarted";
        } catch(OptimisticLockingFailureException | VersionConflictException ex) {
            return "startShippingTxConflict";
        }
    }
    //...
}

public class StartShippingService {
    @PreAuthorize("hasRole('ADMIN')")
    @Transactional
    public void startShipping(StartShippingRequest req) {
        Order order = orderRepository.findById(new OrderNo(req.getOrderNumber()));
        checkOrder(order);
        // ์• ๊ทธ๋ฆฌ๊ฑฐํŠธ์˜ ๋ฒ„์ „๊ณผ ์ผ์น˜ํ•˜๋Š” ๊ฒฝ์šฐ๋งŒ ๋ฐฐ์†ก ์ƒํƒœ (์ถœ๋ฐœ๋กœ) ๋ณ€๊ฒฝ
        if (!order.matchversion(req.getVersion())){
            throw new VersionConflictException(); // ํ‘œํ˜„ ๊ณ„์ธต์œผ๋กœ ๋„˜๊ธด๋‹ค 
        }
        order.startShipping();
    }
    //..
}

๋ฒ„์ „ ์ถฉ๋Œ ์ƒํ™ฉ์— ๋Œ€ํ•œ ํ™•์ธ์ด ๋ช…์‹œ์ ์œผ๋กœ ํ•„์š” ์—†๋‹ค๋ฉด ์‘์šฉ ์„œ๋น„์Šค์—์„œ ํ”„๋ ˆ์ž„์›Œํฌ์šฉ ์ต์…‰์…˜๋งŒ ๋ฐœ์ƒ์‹œํ‚ค๋„๋ก ๊ตฌํ˜„ํ•œ๋‹ค.

public void startShipping(StartShippingRequest req) {
    Order order = oNerRepository.findById(new OrderNo(req.gettrOrderNumber()));
    checkOrder(order);
    if (!order.matchversion(req.getVersion())) {
        // ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๋น„์„ ์  ํŠธ๋žœ์žญ์…˜ ์ถฉ๋Œ ๊ด€๋ จ ์ต์…‰์…˜ ์‚ฌ์šฉ
        throw new OptimisticLockingFailureException("version conflict");
    }
    order.startShipping();
}

๊ฐ•์ œ ๋ฒ„์ „ ์ฆ๊ฐ€

๋ฃจํŠธ๊ฐ€ ์•„๋‹Œ ๋‹ค๋ฅธ ์—”ํ‹ฐํ‹ฐ์˜ ๊ฐ’๋งŒ ๋ณ€๊ฒฝ๋  ๊ฒฝ์šฐ, (๋ฃจํŠธ ์—”ํ‹ฐํ‹ฐ ์ž์ฒด์˜ ๊ฐ’์ด ๋ฐ”๋€Œ๋Š”๊ฒŒ ์—†๊ธฐ์—) JPA๋Š” ๋ฃจํŠธ ์—”ํ‹ฐํ‹ฐ์˜ ๋ฒ„์ „ ๊ฐ’์„ ์ฆ๊ฐ€ํ•˜์ง€ ์•Š๋Š”๋‹ค.

ํ•˜์ง€๋งŒ, ์• ๊ทธ๋ฆฌ๊ฑฐํŠธ์˜ ๊ตฌ์„ฑ์š”์†Œ ์ค‘ ์ผ๋ถ€ ๊ฐ’์ด ๋ฐ”๋€Œ๋ฉด ๋…ผ๋ฆฌ์ ์œผ๋กœ ๋ฐ”๋€๊ฒƒ์ด๋‹ค.

// PTIMISTIC_FORCE_INCREMENT ์‚ฌ์šฉํ•ด ๊ฐ•์ œ๋กœ ์ฆ๊ฐ€ ์ฒ˜๋ฆฌ
// ๋ฃจํŠธ ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์•„๋‹Œ ๋‹ค๋ฅธ ์—”ํ‹ฐํ‹ฐ๋‚˜ ๋ฐธ๋ฅ˜๊ฐ€ ๋ณ€๊ฒฝ๋˜๋”๋ผ๋„ ๋ฒ„์ „ ๊ฐ’์„ ์ฆ๊ฐ€์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.
@Repository
public class JpaOrderRepository implements OrderRepository {
    @PersistenceContext
    private EntityManager entityManager;
    @Override
    public Order findByIdOptimisticLockMode(OrderNo id) {
        return entityManager.find(
            Order.class, id, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
    }
    //...
}

์˜คํ”„๋ผ์ธ ์„ ์  ์ž ๊ธˆ

๋” ์—„๊ฒฉํ•˜๊ฒŒ ๋ฐ์ดํ„ฐ ์ถฉ๋Œ์„ ๋ง‰๊ณ  ์‹ถ๋‹ค๋ฉด, ๋ˆ„๊ตฐ๊ฐ€ ์ˆ˜์ • ํ™”๋ฉด์„ ๋ณด๊ณ  ์žˆ์„ ๋•Œ ์ˆ˜์ • ํ™”๋ฉด ์ž์ฒด๋ฅผ ์‹คํ–‰ํ•˜์ง€ ๋ชปํ•˜๋„๋ก ํ•ด์•ผ ํ•œ๋‹ค. ํ•œ ํŠธ๋žœ์žญ์…˜ ๋ฒ”์œ„์—์„œ๋งŒ ์ ์šฉ๋˜๋Š” ์„ ์  ์ž ๊ธˆ ๋ฐฉ์‹์ด๋‚˜, ๋‚˜์ค‘์— ๋ฒ„์ „ ์ถฉ๋Œ์„ ํ™•์ธํ•˜๋Š” ๋น„์„ ์  ์ž ๊ธˆ ๋ฐฉ์‹์œผ๋กœ๋Š” ์ด๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์—†๋‹ค. ์ด๋•Œ ํ•„์š”ํ•œ ๊ฒƒ์ด ์˜คํ”„๋ผ์ธ ์„ ์  ์ž ๊ธˆ ๋ฐฉ์‹์ด๋‹ค.

โ“ ๊ถ๊ธˆ- ์„ ์ ํ•˜๊ณ  ๋น„์Šทํ•œ๊ฑฐ ์•„๋‹Œ๊ฐ€ โ“

๋‹จ์ผ ํŠธ๋žœ์žญ์…˜์—์„œ ๋™์‹œ ๋ณ€๊ฒฝ์„ ๋ง‰๋Š” ์„ ์  ์ž ๊ธˆ ๋ฐฉ์‹๊ณผ ๋‹ฌ๋ฆฌ ์˜คํ”„๋ผ์ธ ์„ ์  ์ž ๊ธˆ์€ ์—ฌ๋Ÿฌ ํŠธ๋žœ์žญ์…˜์— ๊ฑธ์ณ ๋™์‹œ ๋ณ€๊ฒฝ์„ ๋ง‰๋Š”๋‹ค.

์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ

  • ์‚ฌ์šฉ์ž A๊ฐ€ ์ˆ˜์ •ํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ ๋Œ€๋น„ํ•ด, TimeOut์„ ์„ค์ •ํ•ด์•ผ ํ•œ๋‹ค.

image

์˜คํ”„๋ผ์ธ ์„ ์  ์ž ๊ธˆ์„ ์œ„ํ•œ LockManager ์ธํ„ฐํŽ˜์ด์Šค์™€ ๊ด€๋ จ ํด๋ž˜์Šค

์˜คํ”„๋ผ์ธ ์„ ์  ์ž ๊ธˆ 4๊ฐ€์ง€ ๊ธฐ๋Šฅ

  • ์„ ์  ์‹œ๋„
  • ์ž ๊ธˆ ํ™•์ธ
  • ์ž ๊ธˆ ํ•ด์ œ
  • ๋ฝ ์œ ํšจ ์‹œ๊ฐ„ ์—ฐ์žฅ
public interface LockManager {
    LockId tryLock(String type, String id) throws LockException;
    void checkLock(LockId lockId) throws LockException;
    void releaseLock(LockId lockId) throws LockException;
    void extendLockExpiration(LockId lockId, long inc) throws LockException;
}

public class LockId {
    private String value;
    // ์ƒ์„ฑ์ž, getter
}

์ง€๋ผ์˜ ์ปจํผ๋Ÿฐ์Šค ์‚ฌ๋ก€

  • ํŽ˜์ด์ง€๋ฅผ ์ˆ˜์ •ํ•˜๋Š” ๋™์•ˆ ๋‹ค๋ฅธ ์‚ฌ๋žŒ๋„ ์ˆ˜์ •์ด ๊ฐ€๋Šฅํ•˜๋‹ค. ๋‹ค๋งŒ, Merge ๊ณผ์ •์„ ๊ฑฐ์ณ์ค˜์•ผ ํ•œ๋‹ค.

image

HTML ๋ทฐ์™€ ์ปจํŠธ๋กค๋Ÿฌ

// ๋ฐ์ดํ„ฐ ์ˆ˜์ • ํผ์— ๋™์‹œ์— ์ ‘๊ทผํ•˜๋Š” ๊ฒƒ์„ ์ œ์–ดํ•˜๋Š” ์ฝ”๋“œ์˜ ์˜ˆ 
@RequestMapping("/some/edit/{id}")
public String editForm(@PathVariable("id") Long id, ModelMap model) {
    // 1. ์˜คํ”„๋ผ์ธ ์„ ์  ์ž ๊ธˆ ์‹œ๋„
    LockId lockId = lockManager.tryLock("data", id);
    // 2. ๊ธฐ๋Šฅ ์‹คํ–‰
    Data data = someDao.select(id);
    model.addAttribute("data", data);
    // 3. ์ž ๊ธˆ ํ•ด์ œ์— ์‚ฌ์šฉํ•  LockId๋ฅผ ๋ชจ๋ธ์— ์ถ”๊ฐ€
    model.addAttribute("lockId", lockId);
    return "editForm"
}

์ž ๊ธˆ์„ ํ•ด์ œํ•˜๋Š” ์ฝ”๋“œ

<form action="/some/edit/${data.id}" method="post">
    ...
	<input type="hidden" name="lid" value="${lockId.value}">
    ...
</form>
// ------------------------------------------------
@RequestMapping(value = "/some/edit/{id}", method = RequestMethod.POST)
public String edit(@PathVariable("id") Long id,
                   @ModelAttribute("editReq") EditRequest editReq,
                   @RequestParam("lid") String lockIdValue) {
    editReq.setId(id);
    // 1. ์ž ๊ธˆ ์„ ์  ํ™•์ธ
    LockId lockId = new LockId(lockIdValue);
    lockManager.checkLock(lockId);
    // 2. ๊ธฐ๋Šฅ ์‹คํ–‰
    someEditService.edit(editReq);
    model.addAttribute("data", data);
    // 3. ์ž ๊ธˆ ํ•ด์ œ
    lockManager.releaseLock(lockId);

    return "editSuccess";
}

๊ฐ์ข… Lock ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ RULE๋“ค ์˜ˆ์‹œ

  • ์ž ๊ธˆ์˜ ์œ ํšจ ์‹œ๊ฐ„์ด ์ง€๋‚ฌ์œผ๋ฉด ์ด๋ฏธ ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๊ฐ€ ์ž ๊ธˆ์„ ์„ ์ ํ•œ๋‹ค.
  • ์ž ๊ธˆ์„ ์„ ์ ํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž๊ฐ€ ๊ธฐ๋Šฅ์„ ์‹คํ–‰ํ–ˆ๋‹ค๋ฉด ๊ธฐ๋Šฅ ์‹คํ–‰์„ ๋ง‰์•„์•ผ ํ•œ๋‹ค

DB๋ฅผ ์ด์šฉํ•œ LockManager ๊ตฌํ˜„

์ž ๊ธˆ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ์ €์žฅํ•  ํ…Œ์ด๋ธ”๊ณผ ์ธ๋ฑ์Šค ์ƒ์„ฑ

์ฃผ์š”ํ‚ค ๋ชฉ์ : ๋™์‹œ์— ๋‘์‚ฌ์šฉ์ž๊ฐ€ ํŠน์ • ํƒ€์ž…์˜ ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ์ž ๊ธˆ์„ ๊ตฌํ•˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€

// MySQL ์‚ฌ์šฉ ์˜ˆ์‹œ
create table locks (
    'type' varchar(255), 
    id varchar(255),
    lockid varchar(255),
    expiration_time datetime, // ์ž ๊ธˆ์˜ ์œ ํšจ ์‹œ๊ฐ„
    primary key ('type', id) 
) character set utf8;

create unique index locks_idx ON locks (lockid);

์• ๊ทธ๋ฆฌ๊ฑฐํŠธ์— ๋Œ€ํ•œ ์ž ๊ธˆ์„ ์š”์ฒญ์‹œ

insert into locks values ๏ผˆ'Order','1', '์ƒ์„ฑํ•œ๏ผšlockid', '2016-03-28 09:10:00'๏ผ‰;

LockData ํด๋ž˜์Šค

public class LockData {
    private String type;
    private String id;
    private String lockid;
    private long expirationTime;
   
    public LockData(String type, String id, String lockId, long expirationTime) {
        this.type = type;
        this.id = id;
        this.lockId = lockId;
        this.expirationTime= expirationTime;
    }

    public String getType() {
        return type;
    }

    public String getId() {
        return id;
    }

    public String getLockId() {
        return lockId;
    }

    public long getExpirationTime() {
        return expirationTime;
    }

    // ์œ ํšจ ์‹œ๊ฐ„์ด ์ง€๋‚ฌ๋Š”์ง€ ํ™•์ธ
    public boolean isExpired() {
        return expirationTime < System.currentTimeMillis();
    }
}

์Šคํ”„๋ง JdbcTemplate์„ ์ด์šฉํ•œ SpringLockManager์˜ tryLock() ๊ตฌํ˜„

@Component
public class SpringLockManager implements LockManager {
    private int lockTimeout = 5 * 60 * 1000;
    private DdbcTemplate jdbcTemplate;
    private RowMapper<LockData> lockDataRowMapper = (rs3 rowNum) ->
        new LockData(rs.getString(1), rs.getString(2),
                     rs.getString(3), rs.getTimestamp(4).getTime());
    @Transactional
    @Override
    public LockId tryLock(String type, String id) throws LockException {
        checkAlreadyLocked(type, id);
        LockId lockId = new LockId(UUID.randomUUID().toString());
        locking(type, id, lockId);
        return lockId;
    }

    private void checkAlreadyLocked(String type, String id) {
        List<LockData> locks = jdbcTemplate.query(
            "select * from locks where type = ? and id = ?",
            lockDataRowMapper, type, id);
        Optional<LockData> lockData = handleExpiration(locks);
        if (lockData.isPresent()) throw new AlreadyLockedException();
    }

    private Optional<LockData> handleExpiration(List<LockData> locks) {
        if (locks.isEmptyO) return Optional.empty();
        LockData lockData = locks.get(0);
        if (lockData.isExpired()) {
            jdbcTemplate.update(
                "delete from locks where type = ? and id = ?",
                lockData.getType(), lockData.getId());
            return Optional.empty();
        } else {
            return Optional.of(lockData);
        }
    }

    private void locking(String type. String id, LockId lockId) {
        try {
            int updatedCount = jdbcTemplate.update(
                "insert into locks values (?,?,?,?)",
                type, id, lockId.getValue(), new Timestamp(getExpirationTime()));
            if (updatedCount == 0) throw new LockingFailException();
        } catch (DuplicateKeyException e) {
            throw new LockingFailException(e);
        }
    }

    private long getExpirationTime() {
        return System.currentTimeMillis() + lockTimeout;
    }

    @Override
    public void checkLock(Lockid lockid) throws LockException {
        Optional<LockData> lockData = getLockData(lockid);
        if (!lockData.isPresent()) throw new NoLockException();
    }

    private Optional<LockData> getLockData(Lockid lockid) {
        List<LockData> locks = jdbcTemplate.query(
            "select * from locks where lockid =
            lockDataRowMapper, lockid.getValue());
        return handleExpiration(locks);
    }

    @Transactional
    @Override
    public void extendLockExpiration(Lockid lockId, long inc) throws LockException {
        Optional<LockData> lockDataOpt = getLockData(lockid);
        LockData lockData =
            lockDataOpt.orElseThrow(() -> new NoLockException());
        jdbcTemplate.update(
            "update locks set expiration_time = ? where type = ? AND id = ?",
            new Timestamp(lockData.getTimestamp() + inc),
            lockData.getType(), lockData.getId());
    }

    @Transactional
    @Override
    public void releaseLock(LockId lockId) throws LockException {
        jdbcTemplate.update("delete from locks where lockid = ?", lockId.getValue());
    }

    @Autowired
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
}
โš ๏ธ **GitHub.com Fallback** โš ๏ธ