경매장 시세 조작을 줄이는 지연 시세 지표와 이상 거래 격리 큐 설계

게임 경매장의 자전 거래와 시세 조작을 줄이기 위해 PostgreSQL 16, Redis Streams 기반의 지연 시세 지표와 이상 거래 격리 큐를 설계하는 방법을 설명합니다.

✓ 복사 완료!
챕터인게임 경제 & 보상 체계
난이도중급
상태● 검토 완료
대상 엔진
Engine Agnostic
최종 업데이트
경매장 시세 조작을 줄이는 지연 시세 지표와 이상 거래 격리 큐 설계

핵심 요약

이 글은 방금 정산된 거래의 시세 반영을 지연하고 위험 신호가 있는 거래를 재검토하는 설계안입니다. 거래량 급증, 계정 간 반복 거래, 기준 가격과의 큰 편차를 검토 대상으로 삼되, 이런 신호만으로 조작 거래라고 확정하지 않습니다. 지연과 격리가 조작의 영향을 얼마나 줄이는지는 실제 거래 데이터와 검수 결과로 평가해야 합니다.

핵심 구성은 체결 이벤트 로그 → 지연 버퍼 → 이상 거래 판정 → 정상 시세 집계 또는 격리 큐입니다.

경매장 시세 조작은 왜 발생하는가?

경매장 가격은 플레이어가 참고하는 공개 지표입니다. 따라서 특정 아이템의 가격을 인위적으로 올리거나 내리면 다음 거래의 가격과 유저의 기대 가격까지 영향을 받을 수 있습니다.

이 설계에서 검토할 위협 시나리오는 다음과 같습니다. 실제 게임에서의 발생 빈도나 법적 분류를 제시하는 표는 아닙니다.

조작 패턴동작 방식탐지 단서
자전 거래동일인이 보유한 계정끼리 사고팔기계정·IP·디바이스·결제수단 관계
저가 매집 후 고가 거래먼저 물량을 확보한 뒤 높은 가격으로 체결짧은 시간 내 가격 급등과 재등록
허위 호가체결 의도 없이 높은 가격의 주문을 반복 등록취소율, 주문 유지 시간, 체결률
시세 덤핑낮은 가격의 거래를 만들어 기준 가격을 하락동일 그룹의 반복 저가 체결
거래량 부풀리기작은 금액의 거래를 다수 발생거래 간격, 계정 쌍, 금액 분포

시세 집계에는 아이템과 재화의 이동이 확정된 정산 거래를 사용하고, 주문 등록·취소 정보는 별도의 탐지 신호로 사용합니다. 등록 가격이나 체결 기록만으로 정산 완료 여부와 거래의 정당성을 확인할 수는 없습니다.

지연 시세 지표는 어떻게 설계할까?

즉시 시세 반영의 한계

가장 단순한 구현은 최근 체결가를 현재 시세로 사용하는 방식입니다.

current_price = latest_executed_trade.price

하지만 이 방식은 조작자가 소량의 비정상 거래만 만들어도 다음 요청부터 시세가 변하는 문제가 있습니다. 특히 거래량이 적은 아이템은 단 한 번의 극단값이 기준 가격이 되기 쉽습니다.

지연 윈도우 적용

지연 시세는 현재 시각 t에서 지연 시간 D > 0과 집계 기간 W > 0을 정하고, 정산 완료 시각이 (t - D - W, t - D]인 거래를 집계합니다. 같은 아이템·서버·통화에 속하고, 가격은 동일한 최소 화폐 단위의 단위당 가격인 거래만 비교한다고 가정합니다.

예를 들어 D = 15분, W = 60분이면 12시 00분의 시세는 10시 45분 이후부터 11시 45분까지 정산된 거래 중 정책상 허용된 거래로 계산합니다. 오래된 모든 거래를 무제한으로 포함하는 방식은 아닙니다.

Et={i∣t−D−W<settled_ati≤t−D, statusi∈{verified,released}}E_t = \{i \mid t-D-W < settled\_at_i \le t-D,\ status_i \in \{verified,released\}\}

여기서 settled_at은 주문 체결 시각과 구분한 정산 완료 시각입니다. 이후 취소·롤백으로 거래가 무효가 되면 현재 상태와 집계 기여분도 갱신해야 합니다.

중앙값과 절사 평균을 비교한다

단순 평균은 극단값의 크기에 영향을 받고, 중앙값은 정렬 순서에 기반합니다. 절사 평균은 양쪽 꼬리의 일부 표본을 제외합니다. 이 성질을 활용해 지표를 비교하되, 중앙값이나 절사 평균이 모든 시장에서 적합하거나 조작을 막는다고 가정하지 않습니다. NIST 위치 통계량 설명

St=(pricei)i∈Et,Pt=median(St)S_t = (price_i)_{i \in E_t},\qquad P_t = median(S_t)

S_t는 거래별 가격 표본이며 같은 가격이 반복되어도 각 거래를 그대로 포함합니다. 거래량이 충분한 경우에는 상·하위 일정 비율을 제거한 절사 평균을 사용할 수 있습니다.

TrimmedMeant=mean(remove_extremes(St,r))TrimmedMean_t = mean(remove\_extremes(S_t, r))

r은 각 꼬리에서 제거할 비율로 0 ≤ r < 0.5입니다. 이 예에서는 표본 수 n에 대해 양쪽에서 각각 floor(n × r)개를 제거합니다. r = 0.1, n = 20이면 최저·최고 가격을 각각 2건 제외하고 나머지 16건의 평균을 구합니다. 표본이 없으면 가격을 0으로 반환하지 않고 데이터 부족을 표시합니다. 다음 표의 20건 기준과 지표 선택은 검증이 필요한 정책 예시입니다.

조건추천 지표
최근 검증 거래가 20건 미만이전 확정 시세 유지 또는 넓은 기간의 중앙값
거래가 충분하고 가격 변동이 큼중앙값과 절사 평균을 함께 계산
거래량과 가격을 함께 반영해야 함허용 거래만 이용한 VWAP
급격한 변동을 완화해야 함EWMA 보조 지표

VWAP는 거래량으로 가격을 가중 평균한 값입니다. 여기서는 허용 거래의 수량을 가중치로 사용합니다. JPX VWAP 정의

VWAPt=∑i∈Etpricei×quantityi∑i∈EtquantityiVWAP_t = \frac{\sum_{i \in E_t} price_i \times quantity_i}{\sum_{i \in E_t} quantity_i}

수량은 양수여야 하며, 허용 거래가 없어 분모가 0이면 VWAP를 계산하지 않습니다. 높은 수량의 거래는 이 식에서 큰 가중치를 받으므로 VWAP 자체가 거래의 정당성을 검증해 주지는 않습니다.

EWMA를 사용한다면 새 지표 = λ × 현재 관측값 + (1 - λ) × 이전 지표로 계산하고 초기값을 정의합니다. 0 < λ < 1이면 가격 변화의 일부만 이번 갱신에 반영하지만, 조작 여부를 판별하는 공식은 아닙니다. NIST EWMA 설명

이상 거래를 어떻게 판정할까?

이 설계에서는 여러 신호를 점수화하고 시세 반영 허용, 관찰, 격리로 분기합니다. 분기 수를 늘리는 것이 오탐 피해를 줄이는지는 대기 시간, 재검토 비용, 실제 정상 거래의 처리 결과를 함께 측정해야 합니다.

권장 탐지 신호

신호예시의미
가격 편차최근 기준가 대비 40% 이상 차이극단값 가능성
계정 관계동일 IP·디바이스·결제수단의 반복 거래공모 또는 다계정 가능성
거래 집중도짧은 시간에 특정 계정 쌍에 거래 집중자전 거래 가능성
취소 패턴고가 주문 등록 후 반복 취소허위 호가 가능성
거래 간격일정한 간격으로 반복되는 소액 거래자동화 또는 거래량 조작 가능성
재등록 패턴구매 직후 비슷한 가격으로 재등록단기 시세 조작 가능성

같은 IP는 가정·학교·공유망에서도 나타날 수 있고, 급등이나 재등록도 정상 거래일 수 있습니다. 표의 신호는 조사 단서이며 제재의 확정 근거가 아닙니다.

관계 분석에는 원문 식별자 노출을 줄이는 내부 관계 ID 등을 검토하고 접근 권한과 보존 기간을 제한합니다. 해시값이나 ip_fingerprint도 다른 정보와 결합해 이용자를 식별할 수 있으므로 자동으로 익명 데이터가 되는 것은 아닙니다. 단순 해시는 후보 공간이 작은 입력을 추측하는 공격도 고려해야 합니다. ICO 가명처리 기술 지침

점수 계산 예시

type TradeSignal = {
  priceDeviation: number;
  accountRelation: number;
  cancelRate: number;
  pairConcentration: number;
  velocity: number;
};

function calculateRiskScore(signal: TradeSignal): number {
  const values = [signal.priceDeviation, signal.accountRelation,
    signal.cancelRate, signal.pairConcentration, signal.velocity];
  if (values.some(value => !Number.isFinite(value) || value < 0 || value > 1)) {
    throw new RangeError("Risk features must be finite values in [0, 1]");
  }
  return (
    signal.priceDeviation * 0.35 +
    signal.accountRelation * 0.25 +
    signal.cancelRate * 0.15 +
    signal.pairConcentration * 0.15 +
    signal.velocity * 0.1
  );
}

function classifyTrade(score: number): "verified" | "observed" | "quarantined" {
  if (!Number.isFinite(score) || score < 0 || score > 1) {
    throw new RangeError("Risk score must be finite and in [0, 1]");
  }
  if (score >= 0.75) return "quarantined";
  if (score >= 0.45) return "observed";
  return "verified";
}

입력 5개는 모두 위험 방향으로 정규화한 0–1 값입니다. 가격 편차율이나 초당 거래 건수를 원시 값 그대로 넣지 않습니다. 기준가가 0이거나 없는 경우의 정책, 정규화 함수, 관측 기간을 따로 정의하고 버전으로 관리해야 합니다. 가중치 합은 1이며 임계값 0.45와 0.75는 예시입니다. verified는 이 정책의 자동 검사를 통과했다는 뜻이며 조작이 없음을 증명하는 상태는 아닙니다.

이상 거래 격리 큐는 어떻게 구현할까?

큐에 넣을 데이터

격리 큐에는 거래 전체 원문을 무제한으로 넣기보다 재현에 필요한 이벤트와 판정 근거를 보존합니다.

{
  "tradeId": "00000000-0000-4000-8000-000000000123",
  "itemId": "item_sword_legendary_01",
  "buyerIdHash": "usr_hash_a1",
  "sellerIdHash": "usr_hash_b7",
  "price": 125000,
  "quantity": 1,
  "settledAt": "2026-08-29T03:10:00Z",
  "riskScore": 0.86,
  "signals": ["price_deviation", "pair_concentration"],
  "state": "quarantined"
}

이 예는 거래 하나에 아이템 종류 하나를 담으며 price는 단위당 가격입니다. JSON의 ISO 시각은 데이터베이스의 TIMESTAMPTZ에 대응하고, TypeScript에서는 파싱·검증한 밀리초 값 settledAtMs를 사용합니다. tradeId는 아래 PostgreSQL 열과 맞춘 UUID 예시입니다.

이벤트의 state를 저장할 때는 데이터베이스의 status 열에 명시적으로 매핑합니다. 데이터베이스의 큰 정수 가격을 JavaScript에서 처리할 때는 안전 정수 범위를 확인하거나 BigInt·정수 문자열 등 손실 없는 표현을 사용해야 합니다.

한 거래의 현재 격리 항목은 tradeId로 중복을 막습니다. 상태 변경 이벤트는 거래 ID만으로 중복을 막으면 후속 재검토까지 버릴 수 있으므로 (tradeId, eventKind, decisionVersion) 같은 별도 키를 사용합니다.

상태 머신 정의

상태설명시세 반영
pending지연 윈도우 대기 중반영하지 않음
observed관찰·재검토 대상으로 분류됨이 예에서는 제외
verified자동 위험 검사 정책을 통과함기간 안이면 반영
quarantined이상 거래로 격리됨반영하지 않음
rejected조작 또는 무효 거래로 확정반영하지 않음
released재검토 후 시세 반영을 허용함기간 안이면 반영

상태 변경은 기존 행을 덮어쓰기보다 append-only 이벤트 로그로 남기는 편이 감사와 장애 복구에 유리합니다. 현재 상태는 이벤트를 투영한 읽기 모델로 제공할 수 있습니다.

경매장 거래 이벤트가 지연 시세 집계와 이상 거래 격리 큐로 분기되는 구조를 보여주는 서버 아키텍처 다이어그램

설명용 개념도이며 실제 서버의 실행 기록이나 PostgreSQL·Redis 공식 화면은 아닙니다. 그림의 60초 지연은 별도 예시이며, 본문 예제의 15분 설정과 다릅니다. 그림에서 생략된 정상 거래 집계 경로와 트랜잭션 경계도 구현해야 합니다.

PostgreSQL 16 테이블 예시

CREATE TABLE auction_trades (
  trade_id UUID PRIMARY KEY,
  item_id TEXT NOT NULL,
  buyer_id_hash TEXT NOT NULL,
  seller_id_hash TEXT NOT NULL,
  price BIGINT NOT NULL CHECK (price > 0),
  quantity INT NOT NULL CHECK (quantity > 0),
  settled_at TIMESTAMPTZ NOT NULL,
  status TEXT NOT NULL CHECK (status IN
    ('pending', 'observed', 'verified', 'quarantined', 'rejected', 'released')),
  risk_score NUMERIC(5,4) NOT NULL DEFAULT 0 CHECK (risk_score BETWEEN 0 AND 1),
  decision_version BIGINT NOT NULL DEFAULT 0 CHECK (decision_version >= 0),
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX auction_trades_price_index
  ON auction_trades (item_id, settled_at, status);

CREATE TABLE trade_quarantine_queue (
  trade_id UUID PRIMARY KEY REFERENCES auction_trades(trade_id),
  reason_codes TEXT[] NOT NULL,
  priority INT NOT NULL DEFAULT 0,
  retry_count INT NOT NULL DEFAULT 0 CHECK (retry_count >= 0),
  next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  locked_at TIMESTAMPTZ,
  lease_version BIGINT NOT NULL DEFAULT 0 CHECK (lease_version >= 0),
  reviewed_at TIMESTAMPTZ,
  reviewer_id TEXT
);

이 스키마는 현재 상태와 격리 큐의 일부입니다. 불변 이벤트 로그, 아웃박스, 예약 재처리, 관찰 큐는 생략되어 있으며 완성된 서비스 구현이 아닙니다. 입력 범위는 PostgreSQL 제약 조건으로도 제한합니다.

큐 작업자는 짧은 트랜잭션에서 FOR UPDATE SKIP LOCKED로 다른 트랜잭션이 잠근 후보 행을 건너뛰고 임대를 기록할 수 있습니다. 행 잠금은 트랜잭션이 끝나면 해제되므로 이후 작업 소유권은 별도 임대 정책으로 관리해야 합니다. PostgreSQL 16 SELECT 잠금 설명

WITH next_job AS (
  SELECT trade_id
  FROM trade_quarantine_queue
  WHERE next_attempt_at <= now()
    AND reviewed_at IS NULL
    AND (locked_at IS NULL OR locked_at < now() - INTERVAL '5 minutes')
  ORDER BY priority DESC, next_attempt_at ASC
  FOR UPDATE SKIP LOCKED
  LIMIT 1
)
UPDATE trade_quarantine_queue q
SET locked_at = now(), retry_count = retry_count + 1,
    lease_version = lease_version + 1
FROM next_job j
WHERE q.trade_id = j.trade_id
RETURNING q.*;

5분 임대는 예시입니다. 임대가 만료되면 다른 워커가 다시 가져갈 수 있으므로, 느린 이전 워커의 완료 결과는 획득 당시 lease_version과 현재 거래의 decision_version이 모두 일치할 때만 받아야 합니다. 조건 확인·상태 변경·검수 완료 표시·아웃박스 기록을 같은 트랜잭션으로 처리하고, 조건이 불일치하면 결과를 폐기하거나 다시 검토합니다. 임대 갱신도 같은 소유권 조건을 확인해야 합니다.

지연 시세와 격리 큐를 연결하는 처리 순서

Step 1. 체결 이벤트를 불변 로그에 기록한다

정산 API는 주문 상태와 아이템·재화 이동을 확정하면서 TradeSettled 이벤트를 기록합니다. 이벤트에는 아이템 ID, 수량, 단위당 가격, 정산 완료 시각, 구매자와 판매자의 내부 관계 키를 포함합니다.

거래 행과 아웃박스 행을 같은 PostgreSQL 트랜잭션으로 커밋하고 별도 퍼블리셔가 메시지를 전달하는 방식을 사용할 수 있습니다. 전달 중 장애로 중복 메시지가 생길 수 있으므로 소비 측의 멱등 처리도 필요합니다. AWS 트랜잭션 아웃박스 설명

Step 2. 지연 버퍼에서 대기시킨다

소비자는 settled_at + delay가 현재 시각 이하인지 확인합니다. 아직 대기 시간이 끝나지 않았다면 데이터베이스에 재처리 예정 시각을 저장하고, 예약 워커가 해당 거래를 다시 깨우도록 구현해야 합니다. 아래 함수는 지연 통과 여부만 검사하며 집계 기간·아이템·상태 검사를 대신하지 않습니다.

function isEligibleForAggregation(
  settledAtMs: number,
  nowMs: number,
  delayMs: number,
): boolean {
  if (![settledAtMs, nowMs, delayMs].every(value =>
    Number.isSafeInteger(value) && value >= 0) || delayMs === 0) {
    throw new RangeError("Times must be non-negative safe integers; delay must be positive");
  }
  return settledAtMs <= nowMs - delayMs;
}

지연 시간은 공통값 또는 아이템별 정책으로 정할 수 있습니다. 5분·15분·30분 등은 비교 실험의 후보이며 업계 표준이나 권장 범위로 입증된 값은 아닙니다. 시세의 최신성, 대응 비용, 이용자 경험을 함께 평가합니다.

Step 3. 위험 점수를 계산하고 분기한다

지연 시간이 끝난 거래의 신호를 계산한 뒤, 현재 거래 상태와 판정 버전이 바뀌지 않았는지 트랜잭션 안에서 다시 확인합니다. 판정 저장과 후속 작업 기록이 분리되어 일부만 성공하는 문제를 피하도록 다음 경계를 제안합니다.

# 실제 저장소 API가 아닌 트랜잭션 경계 설명용 의사 코드
snapshot = readTrade(tradeId)
signals = evaluateSignals(snapshot) // 장시간 계산은 행 잠금 밖에서 수행
score = calculateRiskScore(signals)
state = classifyTrade(score)

transaction:
  current = readTradeForUpdate(tradeId)
  if current.decisionVersion != snapshot.decisionVersion:
    retryWithFreshSnapshot()
  if current.status != "pending":
    stopWithoutChangingDecision()
  if not isEligibleForAggregation(current.settledAtMs, nowMs, delayMs):
    persistWakeupOnce(tradeId, current.settledAtMs + delayMs)
    commitAndStop()
  version = current.decisionVersion + 1
  updateDecision(tradeId, state, score, version)
  appendDecisionEvent(tradeId, state, version)
  if state == "quarantined":
    upsertUnreviewedQuarantineJob(tradeId, version, signals)
  if state == "observed":
    persistObservationReviewJob(tradeId, version)
  if state == "verified":
    appendOutboxOnce(tradeId, "PriceEligibilityChanged", version)
commit

Step 4. 재검토 결과를 시세 지표에 반영한다

위 코드는 트랜잭션 경계를 보여주는 의사 코드이며 TypeScript로 실행할 수 없습니다. 재시도·중단은 이후 구문을 실행하지 않아야 하며 모든 저장 함수는 같은 트랜잭션에 참여해야 합니다. 신호 계산에 쓰는 거래 정보가 변경되어도 버전을 증가시켜야 합니다. 재격리할 때는 기존 완료 기록을 그대로 재사용하지 않고 새 판정 버전에 대응하는 작업을 준비해야 합니다. 별도 아웃박스 소비자는 최신 판정 버전의 거래 상태를 집계 읽기 모델에 반영합니다.

운영자나 자동 검수기가 격리 거래를 released로 바꾸면 그 거래가 현재 집계 기간 안에 있는 경우에만 다시 계산합니다. 반영된 거래가 나중에 rejected가 되면 해당 기여분을 제거해야 합니다. 이미 공개된 과거 시세를 수정하는 정책도 명시하고 다음 메타데이터를 제공합니다.

  • 현재 시세는 새로 검증된 데이터부터 반영한다.
  • 과거 시세 수정이 필요하면 버전 번호를 증가시킨다.
  • 클라이언트에는 index_version과 calculated_at을 함께 제공한다.
  • 거래소 UI에는 가격이 잠정 지표인지 확정 지표인지 표시한다.

API 응답에는 어떤 메타데이터를 포함해야 할까?

시세 값만 반환하면 클라이언트는 데이터의 신뢰 수준을 알 수 없습니다. 다음 필드를 함께 제공하면 캐시와 UI 정책을 일관되게 만들 수 있습니다.

{
  "itemId": "item_sword_legendary_01",
  "price": 98200,
  "currency": "gold",
  "source": "accepted_settled_trades",
  "windowMinutes": 60,
  "delayMinutes": 15,
  "sampleCount": 48,
  "calculatedAt": "2026-08-29T03:30:00Z",
  "indexVersion": 17,
  "dataQualityLabel": "medium",
  "windowStartExclusive": "2026-08-29T02:15:00Z",
  "windowEndInclusive": "2026-08-29T03:15:00Z",
  "isStaleFallback": false
}

sampleCount는 거래 건수이며 수량 합계와 다릅니다. dataQualityLabel은 표본 수·재검토 비율 등에 대해 정한 정책 라벨이며 통계적 신뢰구간이나 조작이 없을 확률을 뜻하지 않습니다. 이전 시세를 유지한다면 isStaleFallback과 실제 원본 기간을 함께 표시합니다.

운영과 보안에서 놓치기 쉬운 부분

격리 큐가 공격 표면이 되지 않게 한다

공격자가 의도적으로 정상 거래를 대량 격리시키면 검수 큐를 마비시킬 수 있습니다. 따라서 아이템별·계정별·시간대별 큐 용량 제한과 우선순위 정책이 필요합니다.

관측 지표를 분리한다

다음 지표를 정상 거래와 격리 거래로 나눠 수집합니다.

  • 아이템별 체결량과 중앙값
  • 시간당 격리 비율
  • 계정 쌍별 거래 집중도
  • 큐 대기 시간과 재시도 횟수
  • 검수 후 정상 해제 비율
  • 지연 시세와 즉시 시세의 차이

특히 quarantine_rate가 갑자기 증가하면 탐지 규칙이 너무 엄격해졌거나 특정 아이템이 공격받고 있다는 신호일 수 있습니다.

재처리와 멱등성을 보장한다

Redis Streams 소비자 그룹은 미확인 메시지를 추적하며 장애 복구 과정에서 메시지를 다시 처리할 수 있습니다. 데이터베이스 반영 후에 XACK하고, 미확인 메시지 회수·재시도·보존 정책을 따로 구성해야 합니다. 스트림 데이터 삭제나 부적절한 보존 설정까지 자동으로 복구해 주는 것은 아닙니다. Redis XREADGROUP 설명

재처리할 수 있는 다음 작업에는 멱등 처리가 필요합니다.

  • 동일 trade_id의 위험 점수 갱신
  • 격리 큐 삽입
  • 검증 거래의 시세 집계 반영
  • 재검토 후 상태 변경

현재 거래 기여분을 보관하는 읽기 모델은 이 예에서 trade_id당 한 행과 최신 판정 버전을 유지합니다. 집계 대상 상태가 바뀌면 기존 기여분을 교체하거나 제거합니다. 상태 변경 이벤트의 중복 제거 키는 별도로 관리하며, 오래된 버전이 새 상태를 덮어쓰지 않도록 합니다.

자주 묻는 질문 (FAQ)

지연 시간은 몇 분으로 설정해야 하나요?

고정된 정답은 없습니다. 아이템 거래량과 조작 대응 목표를 기준으로 시작하되 5분, 15분, 30분 구간을 비교하고 오탐률과 가격 안정성을 측정해야 합니다.

왜 최근 거래를 바로 삭제하지 않고 격리하나요?

이상 거래는 탐지 오류일 수 있고 사후 조사에 필요한 증거입니다. 정상 시세에서만 제외하고 원본 이벤트와 판정 근거를 보존해야 재검토와 제재 이의 대응이 가능합니다.

중앙값만 사용하면 시세 조작을 막을 수 있나요?

중앙값만으로 조작을 막는다고 보장할 수 없습니다. 표본에 포함된 거래들의 가격 분포 자체가 바뀌면 중앙값도 바뀝니다. 지연, 관계 분석, 재검토를 추가하더라도 효과와 오탐을 실제 데이터로 평가해야 합니다.

정리

지연 시세 지표의 목적은 조작자가 만든 거래가 공개 가격에 즉시 영향을 주지 못하게 하는 것입니다. 이상 거래 격리 큐의 목적은 의심 거래를 정상 집계에서 분리하면서도 재검토 가능한 상태로 보존하는 것입니다.

구현 순서는 정산 완료 이벤트 기록 → 지연·집계 기간 검사 → 위험 정책 적용 → 격리·관찰 거래 재검토 → 버전이 있는 시세 API 제공으로 정리할 수 있습니다. 이 글은 운영 실적이 입증된 시스템 사례가 아닙니다. 실제 데이터로 조작 탐지와 정상 거래 처리 결과를 비교하고, 임대 만료·중복 전달·상태 변경을 포함한 통합 테스트로 설계를 검증해야 합니다.