PR #168 · Closes #122

지정가 체결 엔진 구조

공유 가상 호가를 소비하는 지정가 부분 체결 엔진과 3초 주기 워커. 접수 경로와 계층 규약은 develop에 이미 있었고, 이번 PR은 비어 있던 “접수 이후” 구간을 채운다.

이번 PR 신규 이번 PR 수정 기존 (develop)

사용자

HTTP
GET /orders/quote/limit 견적 · 체결 미리보기
POST /orders/limit 주문 접수
요청

LimitOrderService

@Transactional(NEVER)

종목 조회 · 상태 갱신 · 세션 · 환율 준비 후 정책 검증. 두 경로가 여기서 갈린다.

quote() → 미리보기
place() → 접수
Stock · OrderTerms · Price · MarketContext
워커 · 확정 건너뜀

LimitOrderPreviewService

호가를 읽어 Planner를 그대로 호출하되 아무것도 저장하지 않는다. 화면 숫자와 실제 체결 숫자가 어긋나지 않는 이유.

→ 계산 계층으로 직행 (워커 · 확정 거치지 않음)

LimitOrderTransactionService

주문 INSERT + 현금·수량 동결. 커밋 후 LimitOrderAcceptedEvent 발행.

여기부터 비동기 · 전용 스케줄러 스레드

워커 계층

상태 보유 · 단일 인스턴스 자세히 보기 →

누구를 먼저 체결할지 정한다. 아래 계층은 “주문 하나”만 알고, 공정성은 전부 여기 책임.

LimitOrderExecutionWorker @Scheduled(fixedDelay 3s) · 그룹 라운드로빈 · tick당 100건 / 그룹당 10건 / 시작 예산 5초
LimitExecutionCandidateRepository (stock_id, side) 그룹 → 가격·시간 keyset 페이지 50건
LimitExecutionProgress 그룹별 커서. 접수 이벤트를 보류했다가 다음 선정 경계에서 반영 — 진행 중 체결을 끊지 않는다.
orderId 1건

준비 계층

@Transactional(NEVER)
LimitOrderExecutionService 외부 I/O를 전부 여기서 끝낸다 — 세션 · 종목 상태 · 환율 · 호가 스냅샷. 느려도 DB 커넥션을 물지 않고, 실패해도 롤백할 게 없다. 최대 1회 재시도.
LimitExecutionBookReader order_book 깊이·정렬·신선도 검증 후 레벨 목록으로 변환
LimitExecutionAttempt ★ — bookVersion · revision · executionCount · context

확정 계층

@Transactional · 외부 I/O 금지
  1. 제한SET LOCAL lock_timeout
  2. — 계좌 → 주문 → 호가 버전 → 레벨 → 보유 (고정 순서)
  3. 재검증 — 들고 온 토큰이 아직 유효한가? 누적 체결·순정산액·동결액 일치?
    아니면 저장 없이 Reason 반환 → 워커가 판단
  4. 저장 — 체결 이력 · 원장 · 주문 누적 · 계좌 · 보유 원가 · 호가 잔량 소비 · revision 증가
숫자만 — 엔티티·DB·시계 없음

계산 계층

순수 함수 자세히 보기 →
LimitOrderExecutionPlanner 호가별 체결 계획. BUY는 지정가 이하 ASK 저가부터, SELL은 지정가 이상 BID 고가부터. 동결액이 모자라면 이진 탐색으로 최대 정수 수량. 중단 사유를 함께 반환.
LimitOrderSettlementCalculator 한 건의 수수료·세금 누적 차액 (기존, 그대로 재사용)

↑ 미리보기도 확정 계층도 같은 이 함수를 호출한다.

OrderBookRefreshScheduler

가상 호가 생성·갱신 → order_book. 워커와 같은 테이블을 두고 경합한다.

LimitOrderExpirationService

만료 주문 정리 + 동결 해제. 체결과 동결 자원을 나눠 쓴다.

위에서 아래로 갈수록 순수해진다. 읽는 순서는 반대 — Planner → 확정 → 준비 → 워커가 편하다.

기존 구조와 닿는 접점

계층 패턴 자체는 develop에 이미 있다(Service/TransactionService 쌍, Propagation.NEVER 10곳). 그래서 리뷰의 질문은 “계층을 잘 나눴나”가 아니라 “기존 규약을 제대로 지켰나”이고, 위험은 아래 세 접점에 몰려 있다.

LimitOrderAcceptedEvent

접수 트랜잭션이 워커에게 말을 거는 유일한 통로. AFTER_COMMIT으로 발행하고, 워커는 즉시 반영하지 않고 보류했다가 다음 후보 선정 경계에서 커서를 되돌린다.

order_book

호가 갱신 스케줄러와 체결 워커가 같은 테이블을 본다. bookVersion + revision을 근거 토큰에 담아 락 이후 재검증으로 방어.

동결 자원

접수가 잠근 현금·수량을 체결과 만료가 각각 푼다. 이번에 Account.settleReservedBuy, Holding.settleReservedSell가 신설됐다.

이번 PR 전후

이전 (develop)이후
접수 주문 INSERT + 동결 동일 + 이벤트 발행 ◇
체결 없음. 영원히 PENDING → 만료 워커가 호가 잔량을 소비 ★
미리보기 UNSUPPORTED 고정 문구 Planner 재사용 ★
주문 종료 만료뿐 만료 또는 체결