1. 지정가 매도 주문 수량 검증 누락 (trade-service)
Problem
보유 주식 1주로 지정가 매도 주문을 횟수 제한 없이 반복 제출할 수 있었다. 예를 들어 삼성전자(005930) 1주를 보유한 상태에서 지정가 매도 주문을 100번 넣으면 100개의 PENDING 주문이 그대로 DB에 쌓였다. 결국 체결 시점에 1건만 성공하고 나머지 99건은 전부 실패(FAIL) 처리되는 좀비 주문이 발생했다.
로그 예시:
[주문] 지정가 미체결 저장 — tradeId=9efac9b7 SELL 005930 1주 지정가=273000.0 현재가=271500.0
[주문] 지정가 미체결 저장 — tradeId=932ba018 SELL 005930 1주 지정가=273000.0 현재가=271500.0
...
[주문] 보유 부족 거부 — accountId=63950d2e stockCode=005930 보유=1 요청=3
Analysis
validateSufficientFundsOrHoldings() 메서드가 매도 검증 시 asset-service에서 실제 보유 수량만 조회했다. PENDING 상태로 이미 잠긴 지정가 매도 주문 수량을 전혀 고려하지 않았기 때문에, 아직 체결되지 않은 주문이 쌓여 있어도 보유 수량 체크를 계속 통과했다.
기존 검증 로직:
held(asset-service 조회) >= 요청 수량 → 통과
문제:
보유 1주 / PENDING 매도 1건 상태에서 새 매도 1주 요청
→ held=1, 요청=1 → 1 >= 1 → 통과 (잘못된 결과)
영향 범위: TradeServiceImpl.validateSufficientFundsOrHoldings() — SELL 분기
Action
매도 검증 시 실제 보유 수량에서 이미 PENDING 상태인 지정가 매도 주문의 수량 합계를 차감해 가용 수량 기준으로 비교하도록 수정했다.
수정 파일 4개:
TradeJpaRepository — PENDING 지정가 매도 수량 합계 쿼리 추가
@Query("SELECT COALESCE(SUM(t.stockAmount), 0) FROM TradeEntity t " +
"WHERE t.accountId = :accountId AND t.stockCode = :stockCode " +
"AND t.tradeStatus = 'PENDING' AND t.orderType = 'LIMIT' AND t.tradeType = 'SELL'")
int sumPendingLimitSellQuantity(@Param("accountId") UUID accountId,
@Param("stockCode") String stockCode);
TradeRepository (도메인 인터페이스) — 위 쿼리 메서드 선언 추가
TradeRepositoryImpl — 인터페이스 위임 구현 추가
TradeServiceImpl — 검증 로직 수정
// 수정 전
if (held < request.stockAmount()) { ... }
// 수정 후
int lockedInPending = tradeRepository.sumPendingLimitSellQuantity(
request.accountId(), request.stockCode());
int available = held - lockedInPending;
if (available < request.stockAmount()) { ... }
Result
보유 1주 / PENDING 매도 1건 상태에서 추가 매도 요청 시:
- held = 1, lockedInPending = 1, available = 0
- 0 < 1 → 즉시 거부
좀비 PENDING 주문 적재가 원천 차단됐다. 로그에 미체결잠금, 가용 필드가 추가되어 운영 시 원인 파악이 용이해졌다.
수정 후 로그:
[주문] 보유 부족 거부 — accountId=... stockCode=005930 보유=1 미체결잠금=1 가용=0 요청=1
2. 대회 참여 이력 조회 API 부재 (ranking-service)
Problem
대회가 종료되고 최종 순위가 확정(isFinalized=true)된 이후, 유저가 자신의 대회 참여 이력(어떤 대회에서 몇 티어를 받았는지)을 조회할 수 있는 API가 없었다.
기존 랭킹 조회 API는 두 가지였다.
- GET /api/rankings/competitions/{competitionId} — Redis 기반 실시간 전체 랭킹
- GET /api/rankings/competitions/{competitionId}/me — Redis 기반 특정 대회 내 내 순위
두 API 모두 Redis에서 읽기 때문에 대회 단위로만 조회 가능하고, 유저가 참여한 여러 대회에 걸친 성적 이력(RankTier)을 한 번에 볼 수 없었다.
Analysis
p_rankings 테이블에는 대회 종료 시 finalizeRankingsWithValuations()에 의해 competitionId, userId, rankTier, isFinalized=true 가 정상적으로 저장되고 있었다.
데이터 자체는 존재했지만 userId 기준으로 조회하는 쿼리와 API가 없었다.
기존 Repository 조회 메서드:
findByCompetitionIdAndUserId() → 특정 대회 단건
findAllByCompetitionId() → 특정 대회 전체
누락:
findAllByUserId() → 특정 유저의 전체 이력 ← 없었음
Action
Repository → Service → Controller 계층 순서로 신규 추가했다.
추가 파일 2개:
CompetitionHistoryResult — 서비스→컨트롤러 전달용 애플리케이션 DTO
public record CompetitionHistoryResult(
UUID competitionId,
RankTier rankTier,
LocalDateTime lastUpdatedAt
)
MyCompetitionHistoryResponse — API 응답 DTO
수정 파일 5개:
RankingJpaRepository — userId 기준 이력 조회 쿼리 추가
// isFinalized=true 인 것만 반환 (진행 중인 대회 제외)
List<RankingEntity> findAllByUserIdAndIsFinalizedTrue(UUID userId);
RankingRepository (도메인 인터페이스) — findAllByUserId() 선언 추가
RankingRepositoryImpl — 인터페이스 구현 추가
RankingService / RankingServiceImpl — findMyHistory() 서비스 메서드 추가
@Transactional(readOnly = true)
public List<CompetitionHistoryResult> findMyHistory(UUID userId) {
return rankingRepository.findAllByUserId(userId)
.stream()
.map(CompetitionHistoryResult::from)
.toList();
}
RankingController / RankingControllerDocs — 신규 엔드포인트 추가
GET /api/rankings/me
Header: X-User-Id: {userId}
Result
대회 종료 후 유저가 자신의 참여 이력을 조회할 수 있게 됐다. 확정되지 않은 대회(isFinalized=false)는 반환되지 않는다.
GET /api/rankings/me
{
"status": 200,
"data": [
{
"competitionId": "f182b082-...",
"rankTier": "RANK_1",
"lastUpdatedAt": "2026-05-15T15:37:09"
},
{
"competitionId": "a3c1d044-...",
"rankTier": "TOP_10",
"lastUpdatedAt": "2026-04-20T15:30:00"
}
]
}
scenario_all.http 에 22번 섹션(유저1~4 참여 이력 조회)과 http-client.env.json 에 유저4 변수를 추가해 테스트 시나리오도 보완했다.
'내배캠' 카테고리의 다른 글
| 내일배움캠프 [단기심화 Java 6기] 후기 (0) | 2026.05.26 |
|---|---|
| [내일배움캠프] 인생에서 목표설정의 중요성 (0) | 2026.05.17 |
| TIL - REQUIRES_NEW 자기 호출 시, 순환 참조 발생 (0) | 2026.05.15 |
| TIL - 지정가 매매 시 트러블 슈팅 (0) | 2026.05.14 |
| TIL - DB/Redis/Kafka 분산트랜잭션 대응 (0) | 2026.05.13 |