From 70d1fea7599b41c5be7056c5b5d6ff5c0b460469 Mon Sep 17 00:00:00 2001 From: ohhalim Date: Mon, 15 Jun 2026 09:59:27 +0900 Subject: [PATCH 1/4] =?UTF-8?q?refactor:=20=EB=A7=88=EC=BC=93=20=ED=81=90?= =?UTF-8?q?=EC=97=90=20marketId/symbol=20=EA=B8=B0=EB=B0=98=20submit=20?= =?UTF-8?q?=EC=98=A4=EB=B2=84=EB=A1=9C=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#1?= =?UTF-8?q?17)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 취소 등 Market 객체 없이 marketId/symbol만 가진 경로도 동일 큐로 제출할 수 있도록 오버로드를 추가한다. --- .../order/service/command/MarketOrderCommandQueue.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/coinflow/order/service/command/MarketOrderCommandQueue.java b/src/main/java/com/coinflow/order/service/command/MarketOrderCommandQueue.java index f833e85..645195b 100644 --- a/src/main/java/com/coinflow/order/service/command/MarketOrderCommandQueue.java +++ b/src/main/java/com/coinflow/order/service/command/MarketOrderCommandQueue.java @@ -29,11 +29,15 @@ public MarketOrderCommandQueue(OrderCreateStageRecorder stageRecorder, MeterRegi } public T submit(Market market, OrderSide side, Supplier command) { + return submit(market.getId(), market.getSymbol(), side, command); + } + + public T submit(Long marketId, String marketSymbol, OrderSide side, Supplier command) { MarketWorker worker = workers.computeIfAbsent( - market.getId(), - ignored -> new MarketWorker(market.getSymbol()) + marketId, + ignored -> new MarketWorker(marketSymbol) ); - QueuedCommand queuedCommand = new QueuedCommand<>(market.getSymbol(), side, command); + QueuedCommand queuedCommand = new QueuedCommand<>(marketSymbol, side, command); worker.submit(queuedCommand); return queuedCommand.await(); } From cf0fe2551acf9b22bf8034b700d0a209a28f43f7 Mon Sep 17 00:00:00 2001 From: ohhalim Date: Mon, 15 Jun 2026 09:59:27 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20=EC=A3=BC=EB=AC=B8=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EC=84=B8=EC=84=9C=EC=97=90=EC=84=9C=20=EB=A7=88?= =?UTF-8?q?=EC=BC=93=20=EB=9D=BD=20=EC=A0=9C=EA=B1=B0=20(#117)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 마켓당 워커가 1개이므로 큐가 이미 직렬화를 보장한다. 중복이던 ReentrantLock 획득/해제와 releaseRegistered + afterCommit/afterCompletion 이중 해제 로직을 제거한다. --- .../processor/AcceptedOrderProcessor.java | 88 +++++++------------ .../service/processor/SyncOrderProcessor.java | 20 ----- 2 files changed, 33 insertions(+), 75 deletions(-) diff --git a/src/main/java/com/coinflow/order/service/processor/AcceptedOrderProcessor.java b/src/main/java/com/coinflow/order/service/processor/AcceptedOrderProcessor.java index 92b5470..9ca3c31 100644 --- a/src/main/java/com/coinflow/order/service/processor/AcceptedOrderProcessor.java +++ b/src/main/java/com/coinflow/order/service/processor/AcceptedOrderProcessor.java @@ -11,8 +11,6 @@ import com.coinflow.order.matching.MatchingEngine; import com.coinflow.order.matching.OrderBookRecoveryService; import com.coinflow.order.repository.OrderRepository; -import com.coinflow.order.service.lock.MarketOrderLockScope; -import com.coinflow.order.service.lock.MarketOrderLockService; import com.coinflow.order.service.metrics.OrderCreateStageRecorder; import com.coinflow.order.service.settlement.OrderSettlementService; import com.coinflow.trade.domain.Trade; @@ -28,7 +26,6 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; @Slf4j @Service @@ -39,7 +36,6 @@ public class AcceptedOrderProcessor { private final MatchingEngine matchingEngine; private final OrderBookRecoveryService orderBookRecoveryService; private final DomainEventRecorder eventRecorder; - private final MarketOrderLockService marketOrderLockService; private final OrderSettlementService orderSettlementService; private final OrderCreateStageRecorder stageRecorder; private final TransactionTemplate transactionTemplate; @@ -50,7 +46,6 @@ public AcceptedOrderProcessor( MatchingEngine matchingEngine, OrderBookRecoveryService orderBookRecoveryService, DomainEventRecorder eventRecorder, - MarketOrderLockService marketOrderLockService, OrderSettlementService orderSettlementService, OrderCreateStageRecorder stageRecorder, PlatformTransactionManager transactionManager @@ -60,7 +55,6 @@ public AcceptedOrderProcessor( this.matchingEngine = matchingEngine; this.orderBookRecoveryService = orderBookRecoveryService; this.eventRecorder = eventRecorder; - this.marketOrderLockService = marketOrderLockService; this.orderSettlementService = orderSettlementService; this.stageRecorder = stageRecorder; this.transactionTemplate = new TransactionTemplate(transactionManager); @@ -76,59 +70,43 @@ public void processAcceptedOrder(Market market, OrderSide side, Long orderId) { } private void processAcceptedOrderInternal(Market market, OrderSide side, Long orderId) { - MarketOrderLockScope marketLockScope = marketOrderLockService.acquire(market, side); - AtomicBoolean releaseRegistered = new AtomicBoolean(false); - - try { - transactionTemplate.execute(status -> { - Order order = orderRepository.findByIdWithLock(orderId) - .orElseThrow(() -> new ApiException(ErrorCode.ORDER_NOT_FOUND)); - if (order.getStatus() != OrderStatus.ACCEPTED) { - return null; - } - - order.open(); - List plan = stageRecorder.record( - market.getSymbol(), side, "matching_plan", - () -> matchingEngine.planMatchRejectingSelfTrade(market, order) - ); - List autoCanceledMakers = new ArrayList<>(); - List trades = stageRecorder.record( - market.getSymbol(), side, "settlement", - () -> orderSettlementService.settle(market, order, plan, autoCanceledMakers, null, false) - ); + transactionTemplate.execute(status -> { + Order order = orderRepository.findByIdWithLock(orderId) + .orElseThrow(() -> new ApiException(ErrorCode.ORDER_NOT_FOUND)); + if (order.getStatus() != OrderStatus.ACCEPTED) { + return null; + } - TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { - @Override - public void afterCommit() { - long orderBookApplyStartedAt = System.nanoTime(); - try { - matchingEngine.applyMatchPlan(market, order, plan); - autoCanceledMakers.forEach(canceledMaker -> - matchingEngine.cancelOrder(market.getSymbol(), canceledMaker)); - } catch (Exception e) { - log.error("오더북 applyMatchPlan 실패: orderId={}, DB 체결 내역 기반 재빌드 시도", order.getId(), e); - orderBookRecoveryService.rebuildAfterApplyFailure(market.getId()); - } finally { - stageRecorder.record(market.getSymbol(), side, "orderbook_after_commit", - System.nanoTime() - orderBookApplyStartedAt); - marketLockScope.release(); - } - } + order.open(); + List plan = stageRecorder.record( + market.getSymbol(), side, "matching_plan", + () -> matchingEngine.planMatchRejectingSelfTrade(market, order) + ); + List autoCanceledMakers = new ArrayList<>(); + List trades = stageRecorder.record( + market.getSymbol(), side, "settlement", + () -> orderSettlementService.settle(market, order, plan, autoCanceledMakers, null, false) + ); - @Override - public void afterCompletion(int status) { - marketLockScope.release(); + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + long orderBookApplyStartedAt = System.nanoTime(); + try { + matchingEngine.applyMatchPlan(market, order, plan); + autoCanceledMakers.forEach(canceledMaker -> + matchingEngine.cancelOrder(market.getSymbol(), canceledMaker)); + } catch (Exception e) { + log.error("오더북 applyMatchPlan 실패: orderId={}, DB 체결 내역 기반 재빌드 시도", order.getId(), e); + orderBookRecoveryService.rebuildAfterApplyFailure(market.getId()); + } finally { + stageRecorder.record(market.getSymbol(), side, "orderbook_after_commit", + System.nanoTime() - orderBookApplyStartedAt); } - }); - releaseRegistered.set(true); - return trades; + } }); - } finally { - if (!releaseRegistered.get()) { - marketLockScope.release(); - } - } + return trades; + }); } private void rejectAcceptedOrder(Long orderId, String reason) { diff --git a/src/main/java/com/coinflow/order/service/processor/SyncOrderProcessor.java b/src/main/java/com/coinflow/order/service/processor/SyncOrderProcessor.java index f181512..e74aff0 100644 --- a/src/main/java/com/coinflow/order/service/processor/SyncOrderProcessor.java +++ b/src/main/java/com/coinflow/order/service/processor/SyncOrderProcessor.java @@ -13,8 +13,6 @@ import com.coinflow.order.matching.OrderBookRecoveryService; import com.coinflow.order.repository.OrderRepository; import com.coinflow.order.service.command.CreateOrderCommand; -import com.coinflow.order.service.lock.MarketOrderLockScope; -import com.coinflow.order.service.lock.MarketOrderLockService; import com.coinflow.order.service.lock.OrderAssetLockService; import com.coinflow.order.service.metrics.OrderCreateStageRecorder; import com.coinflow.order.service.metrics.OrderTransactionLifecycleRecorder; @@ -34,7 +32,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; @Slf4j @Service @@ -48,7 +45,6 @@ public class SyncOrderProcessor { private final OrderSettlementService orderSettlementService; private final MarketSequenceAllocator marketSequenceAllocator; private final OrderCreateStageRecorder stageRecorder; - private final MarketOrderLockService marketOrderLockService; private final OrderTransactionLifecycleRecorder transactionLifecycleRecorder; private final ClientOrderIdService clientOrderIdService; private final TransactionTemplate transactionTemplate; @@ -62,7 +58,6 @@ public SyncOrderProcessor( OrderSettlementService orderSettlementService, MarketSequenceAllocator marketSequenceAllocator, OrderCreateStageRecorder stageRecorder, - MarketOrderLockService marketOrderLockService, OrderTransactionLifecycleRecorder transactionLifecycleRecorder, ClientOrderIdService clientOrderIdService, PlatformTransactionManager transactionManager @@ -75,7 +70,6 @@ public SyncOrderProcessor( this.orderSettlementService = orderSettlementService; this.marketSequenceAllocator = marketSequenceAllocator; this.stageRecorder = stageRecorder; - this.marketOrderLockService = marketOrderLockService; this.transactionLifecycleRecorder = transactionLifecycleRecorder; this.clientOrderIdService = clientOrderIdService; this.transactionTemplate = new TransactionTemplate(transactionManager); @@ -89,8 +83,6 @@ public CreateOrderResponse process( long createStartedAt ) { OrderSide side = command.side(); - MarketOrderLockScope marketLockScope = marketOrderLockService.acquire(market, side); - AtomicBoolean releaseRegistered = new AtomicBoolean(false); long transactionTemplateStartedAt = System.nanoTime(); OrderTransactionLifecycleRecorder.Metrics transactionMetrics = transactionLifecycleRecorder.start( @@ -103,8 +95,6 @@ public CreateOrderResponse process( market, command, side, - marketLockScope, - releaseRegistered, transactionMetrics )); } catch (DataIntegrityViolationException e) { @@ -114,9 +104,6 @@ public CreateOrderResponse process( throw e; } } finally { - if (!releaseRegistered.get()) { - marketLockScope.release(); - } long transactionTemplateFinishedAt = System.nanoTime(); stageRecorder.record(market.getSymbol(), side, "transaction_template", transactionTemplateFinishedAt - transactionTemplateStartedAt); @@ -132,8 +119,6 @@ private CreateOrderResponse executeInTransaction( Market market, CreateOrderCommand command, OrderSide side, - MarketOrderLockScope marketLockScope, - AtomicBoolean releaseRegistered, OrderTransactionLifecycleRecorder.Metrics transactionMetrics ) { transactionMetrics.recordCallbackStarted(); @@ -166,10 +151,8 @@ private CreateOrderResponse executeInTransaction( order, plan, autoCanceledMakers, - marketLockScope, transactionMetrics ); - releaseRegistered.set(true); return CreateOrderResponse.of(order, trades); } finally { @@ -218,7 +201,6 @@ private void registerOrderBookSynchronization( Order order, List plan, List autoCanceledMakers, - MarketOrderLockScope marketLockScope, OrderTransactionLifecycleRecorder.Metrics transactionMetrics ) { TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { @@ -246,7 +228,6 @@ public void afterCommit() { } finally { stageRecorder.record(market.getSymbol(), side, "orderbook_after_commit", System.nanoTime() - orderBookApplyStartedAt); - marketLockScope.release(); transactionMetrics.recordAfterCommitFinished(); } } @@ -254,7 +235,6 @@ public void afterCommit() { @Override public void afterCompletion(int status) { transactionMetrics.recordAfterCompletionStarted(); - marketLockScope.release(); transactionMetrics.recordAfterCompletionFinished(); } }); From 4489e7f72302f16ba34db432a25a2b118de3fac2 Mon Sep 17 00:00:00 2001 From: ohhalim Date: Mon, 15 Jun 2026 09:59:27 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20=EC=A3=BC=EB=AC=B8=20=EC=B7=A8?= =?UTF-8?q?=EC=86=8C=EB=A5=BC=20=EB=A7=88=EC=BC=93=20=ED=81=90=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=EB=A1=9C=20=EC=A0=84=ED=99=98=20(#117)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 취소가 락 대신 마켓 큐를 거치도록 하여 오더북 변경 주체를 워커 스레드로 단일화한다. 사용자 응답은 submit 블로킹으로 기존과 동일하게 즉시 반환한다. --- .../service/cancel/OrderCancelService.java | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/coinflow/order/service/cancel/OrderCancelService.java b/src/main/java/com/coinflow/order/service/cancel/OrderCancelService.java index fcebb56..b8379f4 100644 --- a/src/main/java/com/coinflow/order/service/cancel/OrderCancelService.java +++ b/src/main/java/com/coinflow/order/service/cancel/OrderCancelService.java @@ -7,8 +7,7 @@ import com.coinflow.order.dto.CancelOrderResponse; import com.coinflow.order.matching.MatchingEngine; import com.coinflow.order.repository.OrderRepository; -import com.coinflow.order.service.lock.MarketOrderLockScope; -import com.coinflow.order.service.lock.MarketOrderLockService; +import com.coinflow.order.service.command.MarketOrderCommandQueue; import com.coinflow.wallet.domain.LedgerType; import com.coinflow.wallet.service.WalletOrderOperationService; import lombok.extern.slf4j.Slf4j; @@ -28,7 +27,7 @@ public class OrderCancelService { private final WalletOrderOperationService walletOrderOperationService; private final MatchingEngine matchingEngine; private final DomainEventRecorder eventRecorder; - private final MarketOrderLockService marketOrderLockService; + private final MarketOrderCommandQueue marketOrderCommandQueue; private final TransactionTemplate transactionTemplate; public OrderCancelService( @@ -36,14 +35,14 @@ public OrderCancelService( WalletOrderOperationService walletOrderOperationService, MatchingEngine matchingEngine, DomainEventRecorder eventRecorder, - MarketOrderLockService marketOrderLockService, + MarketOrderCommandQueue marketOrderCommandQueue, PlatformTransactionManager transactionManager ) { this.orderRepository = orderRepository; this.walletOrderOperationService = walletOrderOperationService; this.matchingEngine = matchingEngine; this.eventRecorder = eventRecorder; - this.marketOrderLockService = marketOrderLockService; + this.marketOrderCommandQueue = marketOrderCommandQueue; this.transactionTemplate = new TransactionTemplate(transactionManager); } @@ -52,16 +51,12 @@ public CancelOrderResponse cancelOrder(Long currentUserId, Long orderId) { .orElseThrow(() -> new ApiException(ErrorCode.ORDER_NOT_FOUND)); if (!order.isCancelable()) throw new ApiException(ErrorCode.ORDER_NOT_CANCELABLE); - MarketOrderLockScope marketLockScope = marketOrderLockService.acquire( + return marketOrderCommandQueue.submit( order.getMarketId(), order.getMarketSymbol(), - order.getSide() + order.getSide(), + () -> transactionTemplate.execute(status -> cancelInTransaction(currentUserId, orderId)) ); - try { - return transactionTemplate.execute(status -> cancelInTransaction(currentUserId, orderId)); - } finally { - marketLockScope.release(); - } } private CancelOrderResponse cancelInTransaction(Long currentUserId, Long orderId) { From e2b31c0566b3163658c1e34e6652688ba0b017fa Mon Sep 17 00:00:00 2001 From: ohhalim Date: Mon, 15 Jun 2026 09:59:27 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EB=A7=88=EC=BC=93=20=EB=9D=BD=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20(#117)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MarketOrderLockManager/Service/Scope를 삭제한다. 직렬화는 마켓 큐가 전담한다. --- .../service/lock/MarketOrderLockManager.java | 17 -------- .../service/lock/MarketOrderLockScope.java | 39 ----------------- .../service/lock/MarketOrderLockService.java | 43 ------------------- 3 files changed, 99 deletions(-) delete mode 100644 src/main/java/com/coinflow/order/service/lock/MarketOrderLockManager.java delete mode 100644 src/main/java/com/coinflow/order/service/lock/MarketOrderLockScope.java delete mode 100644 src/main/java/com/coinflow/order/service/lock/MarketOrderLockService.java diff --git a/src/main/java/com/coinflow/order/service/lock/MarketOrderLockManager.java b/src/main/java/com/coinflow/order/service/lock/MarketOrderLockManager.java deleted file mode 100644 index 3a0e4dd..0000000 --- a/src/main/java/com/coinflow/order/service/lock/MarketOrderLockManager.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.coinflow.order.service.lock; - -import org.springframework.stereotype.Component; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.ReentrantLock; - -@Component -public class MarketOrderLockManager { - - private final Map marketLocks = new ConcurrentHashMap<>(); - - public ReentrantLock getLock(Long marketId) { - return marketLocks.computeIfAbsent(marketId, key -> new ReentrantLock()); - } -} diff --git a/src/main/java/com/coinflow/order/service/lock/MarketOrderLockScope.java b/src/main/java/com/coinflow/order/service/lock/MarketOrderLockScope.java deleted file mode 100644 index c5e4647..0000000 --- a/src/main/java/com/coinflow/order/service/lock/MarketOrderLockScope.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.coinflow.order.service.lock; - -import com.coinflow.order.domain.OrderSide; -import com.coinflow.order.service.metrics.OrderCreateStageRecorder; - -import java.util.concurrent.locks.ReentrantLock; - -public final class MarketOrderLockScope { - - private final String marketSymbol; - private final OrderSide side; - private final ReentrantLock lock; - private final long acquiredAt; - private final OrderCreateStageRecorder stageRecorder; - private boolean released; - - MarketOrderLockScope( - String marketSymbol, - OrderSide side, - ReentrantLock lock, - long acquiredAt, - OrderCreateStageRecorder stageRecorder - ) { - this.marketSymbol = marketSymbol; - this.side = side; - this.lock = lock; - this.acquiredAt = acquiredAt; - this.stageRecorder = stageRecorder; - } - - public void release() { - if (released) { - return; - } - released = true; - stageRecorder.record(marketSymbol, side, "market_lock_hold", System.nanoTime() - acquiredAt); - lock.unlock(); - } -} diff --git a/src/main/java/com/coinflow/order/service/lock/MarketOrderLockService.java b/src/main/java/com/coinflow/order/service/lock/MarketOrderLockService.java deleted file mode 100644 index eaee891..0000000 --- a/src/main/java/com/coinflow/order/service/lock/MarketOrderLockService.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.coinflow.order.service.lock; - -import com.coinflow.market.domain.Market; -import com.coinflow.order.domain.OrderSide; -import com.coinflow.order.service.metrics.OrderCreateStageRecorder; -import org.springframework.stereotype.Service; - -import java.util.concurrent.locks.ReentrantLock; - -@Service -public class MarketOrderLockService { - - private final MarketOrderLockManager marketOrderLockManager; - private final OrderCreateStageRecorder stageRecorder; - - public MarketOrderLockService( - MarketOrderLockManager marketOrderLockManager, - OrderCreateStageRecorder stageRecorder - ) { - this.marketOrderLockManager = marketOrderLockManager; - this.stageRecorder = stageRecorder; - } - - public MarketOrderLockScope acquire(Market market, OrderSide side) { - return acquire(market.getId(), market.getSymbol(), side); - } - - public MarketOrderLockScope acquire(Long marketId, String marketSymbol, OrderSide side) { - ReentrantLock marketLock = marketOrderLockManager.getLock(marketId); - long marketLockWaitStartedAt = System.nanoTime(); - marketLock.lock(); - long marketLockAcquiredAt = System.nanoTime(); - stageRecorder.record(marketSymbol, side, "market_lock_wait", - marketLockAcquiredAt - marketLockWaitStartedAt); - return new MarketOrderLockScope( - marketSymbol, - side, - marketLock, - marketLockAcquiredAt, - stageRecorder - ); - } -}