이 글은 누구를 위한 것인가
- 카드 부정 사용과 계정 도용을 탐지하려는 팀
- 규칙 엔진과 ML 모델을 결합한 사기 탐지 파이프라인을 설계하는 개발자
- false positive를 낮추면서 탐지율을 높이려는 팀
들어가며
사기 탐지는 "놓치면 금전 손실, 과탐지하면 정상 고객 불편"이라는 트레이드오프다. 규칙 엔진으로 명확한 이상 패턴을 1차 차단하고, ML 모델로 미묘한 패턴을 탐지하며, 애매한 케이스는 수동 검토 큐로 보내는 3단계 구조가 효과적이다.
이 글은 bluefoxdev.kr의 실시간 사기 탐지 가이드 를 참고하여 작성했습니다.
1. 사기 탐지 아키텍처
[3단계 파이프라인]
1단계: 규칙 엔진 (하드 차단)
├── 블랙리스트 IP/카드/이메일
├── Velocity Check (단시간 반복 주문)
├── 비정상 금액 패턴
└── 해외 IP + 국내 카드
2단계: ML 스코어링 (소프트 차단)
├── 피처: 주문 금액, 시간, 배송지, 디바이스
├── 모델: XGBoost / Isolation Forest
├── 스코어 0.0 ~ 1.0
└── 임계값: 0.7↑ 차단, 0.4~0.7 검토
3단계: 수동 검토 큐
├── ML 스코어 중간 영역
├── 고액 주문
└── 신규 고객 + 배송지 변경
[Velocity Check 기준]
동일 IP: 1시간 내 5건↑
동일 카드: 24시간 내 3건↑
동일 배송지: 다른 카드 3개↑
동일 계정: 10분 내 결제 시도 3회↑
2. 사기 탐지 구현
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);
interface OrderContext {
orderId: string;
userId: string;
amount: number;
ip: string;
cardLast4: string;
cardBin: string;
shippingAddress: string;
deviceFingerprint: string;
userAgent: string;
isNewUser: boolean;
}
// Velocity Check (Redis 슬라이딩 윈도우)
async function velocityCheck(ctx: OrderContext): Promise<{ blocked: boolean; reason?: string }> {
const now = Date.now();
const checks = [
{ key: `vel:ip:${ctx.ip}`, limit: 5, window: 3600 },
{ key: `vel:card:${ctx.cardLast4}`, limit: 3, window: 86400 },
{ key: `vel:user:${ctx.userId}`, limit: 3, window: 600 },
];
for (const check of checks) {
await redis.zremrangebyscore(check.key, 0, now - check.window * 1000);
const count = await redis.zcard(check.key);
if (count >= check.limit) {
return { blocked: true, reason: `Velocity: ${check.key} (${count}/${check.limit})` };
}
await redis.zadd(check.key, now, `${ctx.orderId}:${now}`);
await redis.expire(check.key, check.window);
}
return { blocked: false };
}
// 규칙 엔진
async function ruleEngine(ctx: OrderContext): Promise<{ risk: 'block' | 'review' | 'pass'; reasons: string[] }> {
const reasons: string[] = [];
let maxRisk: 'block' | 'review' | 'pass' = 'pass';
// 블랙리스트 확인
const isBlacklisted = await redis.sismember('blacklist:ips', ctx.ip);
if (isBlacklisted) { reasons.push('Blacklisted IP'); maxRisk = 'block'; }
// 고액 주문 + 신규 사용자
if (ctx.amount > 1_000_000 && ctx.isNewUser) {
reasons.push('High value + new user');
if (maxRisk !== 'block') maxRisk = 'review';
}
// 배송지와 카드 발급국 불일치 (BIN 기반)
const cardCountry = await getCountryFromBin(ctx.cardBin);
if (cardCountry && cardCountry !== 'KR' && ctx.shippingAddress.includes('대한민국')) {
reasons.push('Foreign card + domestic shipping');
if (maxRisk !== 'block') maxRisk = 'review';
}
return { risk: maxRisk, reasons };
}
// 통합 사기 탐지 파이프라인
async function detectFraud(ctx: OrderContext) {
// 1단계: Velocity Check
const velocity = await velocityCheck(ctx);
if (velocity.blocked) {
await logFraudEvent(ctx.orderId, 'VELOCITY_BLOCKED', velocity.reason!);
return { action: 'block', reason: velocity.reason };
}
// 2단계: 규칙 엔진
const rules = await ruleEngine(ctx);
if (rules.risk === 'block') {
await logFraudEvent(ctx.orderId, 'RULE_BLOCKED', rules.reasons.join(', '));
return { action: 'block', reason: rules.reasons[0] };
}
// 3단계: ML 스코어 (외부 서비스 호출)
const mlScore = await callFraudMLModel(ctx);
if (mlScore > 0.7) {
await logFraudEvent(ctx.orderId, 'ML_BLOCKED', `score: ${mlScore}`);
return { action: 'block', reason: 'Suspicious pattern detected' };
}
if (mlScore > 0.4 || rules.risk === 'review') {
await addToReviewQueue(ctx.orderId, { mlScore, ruleReasons: rules.reasons });
return { action: 'review', mlScore };
}
return { action: 'pass', mlScore };
}
async function getCountryFromBin(bin: string): Promise<string | null> { return 'KR'; }
async function callFraudMLModel(ctx: OrderContext): Promise<number> { return Math.random(); }
async function logFraudEvent(orderId: string, type: string, detail: string) {}
async function addToReviewQueue(orderId: string, data: any) {}
마무리
사기 탐지의 핵심은 속도와 정확도의 균형이다. Velocity Check는 Redis 슬라이딩 윈도우로 마이크로초 단위로 처리하고, ML 스코어는 외부 서비스로 분리해 독립적으로 개선한다. false positive를 낮추려면 "차단 → 검토 → 통과" 3단계 구조로 애매한 케이스는 사람이 최종 판단하도록 한다.