인게임 결제 영수증 위변조와 환불 어뷰징을 막는 AI 자동 검증 파이프라인

모바일 게임의 인게임 결제 영수증 위변조와 반복 환불 어뷰징을 탐지하는 서버 검증 구조를 설명합니다. Apple·Google 스토어 검증, 멱등성, 이벤트 스트림, 이상탐지 모델과 운영 대응까지 단계별로 정리합니다.

✓ 복사 완료!
챕터자동화 테스트 & 회귀 검증
난이도중급
상태● 검토 완료
대상 엔진
Engine Agnostic
최종 업데이트
인게임 결제 영수증 위변조와 환불 어뷰징을 막는 AI 자동 검증 파이프라인

핵심 요약

인게임 결제 영수증은 클라이언트가 제출한 값을 신뢰하지 말고 게임 서버가 Apple App Store Server API 또는 Google Play Developer API로 직접 검증해야 합니다. 검증 결과를 멱등적으로 저장한 뒤 결제·환불·계정·기기·네트워크 신호를 결합해 위험 점수를 계산하면 영수증 위변조와 반복 환불 어뷰징을 자동으로 분류할 수 있습니다.

핵심은 AI를 스토어 영수증의 진위를 판단하는 단독 권한자로 사용하는 것이 아니라 공식 스토어 검증 결과 위에 행동 패턴과 관계형 신호를 분석하는 보조 판정 계층으로 배치하는 것입니다.

인게임 결제 영수증 위변조는 왜 발생하는가?

모바일 클라이언트는 공격자가 직접 수정할 수 있는 환경입니다. APK·IPA 패치, 후킹, 프록시, 에뮬레이터, 루팅 또는 탈옥 환경을 이용하면 클라이언트가 서버에 보내는 구매 완료 요청을 바꿀 수 있습니다.

대표적인 공격 패턴은 다음과 같습니다.

공격 패턴공격자가 노리는 것서버에서 확인할 신호
가짜 영수증 전송결제하지 않고 재화 획득스토어 API 검증 실패, 상품 ID 불일치
정상 영수증 재사용하나의 결제로 여러 번 지급transaction ID 중복, idempotency key 충돌
다른 계정의 영수증 사용타 계정 결제 결과 탈취사용자 ID와 스토어 계정 매핑 불일치
상품 가격·수량 변조낮은 금액으로 고가 상품 획득서버 상품 카탈로그와 가격 불일치
환불 직후 재사용환불된 결제의 재화 유지refund·revocation 이벤트와 지급 상태 불일치
지연된 결제 완료 악용검증 전 재화 선지급pending 상태, acknowledgement 지연

따라서 클라이언트가 보내는 productId, purchaseToken, transactionId는 검증에 필요한 입력일 뿐 권한 증명이 아닙니다. 재화 지급 여부는 서버가 확인한 상태와 서버 내부의 지급 원장으로 결정해야 합니다.

서버 영수증 검증은 어떻게 구현할까?

1단계: 결제 요청을 원본 그대로 수집한다

클라이언트는 결제 플랫폼별 토큰과 앱 환경 정보를 서버에 전달합니다. 서버는 요청 수신 시각, 계정 ID, 디바이스 ID 해시, 앱 버전, IP 대역, 플랫폼, 상품 ID를 함께 기록합니다.

민감한 원문 영수증은 접근 권한을 제한하고 필요한 경우 암호화합니다. 로그에는 토큰 전체를 남기지 말고 앞뒤 일부만 마스킹한 식별자를 사용해야 합니다.

interface PurchaseRequest {
  userId: string;
  platform: 'ios' | 'android';
  productId: string;
  purchaseToken: string;
  clientRequestId: string;
  appVersion: string;
  deviceHash: string;
}

clientRequestId는 네트워크 재시도와 사용자의 반복 탭을 구분하기 위한 요청 ID입니다. 단, 이 값만으로 중복 지급을 막으면 안 됩니다. 최종 멱등성 기준은 스토어가 발급한 transaction ID 또는 purchase token이어야 합니다.

2단계: 스토어 공식 API에서 결제 상태를 확인한다

검증 방식은 플랫폼마다 다릅니다.

플랫폼서버 검증 대상주요 확인 항목
AppleApp Store Server API, App Store Server Notifications V2signed transaction, product ID, bundle ID, environment, purchase date, revocation date
Google PlayGoogle Play Developer APIpackage name, product ID, purchase token, purchase state, acknowledgement state, consumption state

Apple은 JWS 형태의 signed transaction을 검증하고 서명 체인과 bundleId, productId, environment를 확인해야 합니다. Google Play는 서버 자격 증명으로 구매 토큰을 조회하고 packageName, 상품 ID, 구매 상태, acknowledgement 상태를 검증해야 합니다.

서버가 확인해야 하는 최소 조건은 다음과 같습니다.

  1. 앱 패키지 또는 번들 ID가 현재 서비스와 일치하는가?
  2. 상품 ID가 서버 상품 카탈로그에 존재하는가?
  3. 결제 상태가 실제 구매 완료 또는 소비 가능한 상태인가?
  4. 이미 지급되거나 취소·환불·철회된 거래가 아닌가?
  5. 해당 거래가 요청한 사용자와 올바르게 연결되는가?
  6. 테스트 환경 영수증이 운영 환경으로 유입되지 않았는가?
type VerificationResult = {
  valid: boolean;
  status: 'PURCHASED' | 'PENDING' | 'REFUNDED' | 'REVOKED' | 'INVALID';
  transactionId: string;
  productId: string;
  purchasedAt: Date;
  statusUpdatedAt: Date;
  refundedAt?: Date;
};

async function verifyPurchase(req: PurchaseRequest): Promise<VerificationResult> {
  if (req.platform === 'ios') {
    return verifyWithAppleAppStoreServerApi(req);
  }

  return verifyWithGooglePlayDeveloperApi(req);
}

실제 지급 로직에서는 valid === true만 보는 것보다 상태 전이를 명시적으로 관리하는 편이 안전합니다. 예를 들어 PENDING은 재화를 지급하지 않고 검증 완료 이벤트를 기다립니다. REFUNDED와 REVOKED는 신규 지급을 차단하고 이미 지급된 재화를 회수할지 별도 정책으로 결정합니다.

중복 지급과 재사용 공격을 어떻게 차단할까?

결제 검증 테이블과 재화 지급 원장을 분리합니다. 검증 테이블은 스토어 거래의 최신 상태를 저장하고 지급 원장은 실제 게임 경제에 반영된 변동을 기록합니다.

CREATE TABLE purchase_transactions (
    platform              VARCHAR(16) NOT NULL,
    transaction_id        VARCHAR(255) NOT NULL,
    user_id               BIGINT NOT NULL,
    product_id            VARCHAR(128) NOT NULL,
    status                VARCHAR(32) NOT NULL,
    status_updated_at     TIMESTAMPTZ NOT NULL,
    purchased_at          TIMESTAMP NOT NULL,
    refunded_at           TIMESTAMP NULL,
    raw_payload_hash      CHAR(64) NOT NULL,
    created_at            TIMESTAMP NOT NULL,
    updated_at            TIMESTAMP NOT NULL,
    PRIMARY KEY (platform, transaction_id)
);

CREATE TABLE economy_ledger (
    ledger_id             BIGSERIAL PRIMARY KEY,
    platform              VARCHAR(16) NOT NULL,
    user_id               BIGINT NOT NULL,
    transaction_id        VARCHAR(255) NOT NULL,
    currency_type         VARCHAR(32) NOT NULL,
    amount                BIGINT NOT NULL,
    reason                VARCHAR(64) NOT NULL,
    created_at            TIMESTAMP NOT NULL,
    UNIQUE (platform, transaction_id, reason)
);

트랜잭션 ID에 유니크 제약 조건을 걸고 검증 저장과 지급 원장 기록을 하나의 데이터베이스 트랜잭션으로 처리합니다. 메시지 큐를 사용한다면 outbox pattern 또는 소비자 측 멱등성 키를 함께 사용해야 합니다.

await db.transaction(async tx => {
  // 상태는 기존 거래에도 갱신한다. 구현은 provider 이벤트 시각/버전을 비교해
  // 오래된 검증 응답이 환불·철회 상태를 되돌리지 않도록 원자화한다.
  await tx.purchaseTransactions.upsertLatestStatus({
    platform: req.platform,
    transactionId: result.transactionId,
    userId: req.userId,
    productId: result.productId,
    status: result.status,
    statusUpdatedAt: result.statusUpdatedAt,
    rawPayloadHash: hash(req.purchaseToken)
  });

  if (result.status !== 'PURCHASED') {
    return;
  }

  const grant = await tx.economyLedger.insertIfAbsent({
    platform: req.platform,
    userId: req.userId,
    transactionId: result.transactionId,
    currencyType: 'premium_gem',
    amount: productCatalog[result.productId].grantAmount,
    reason: 'purchase_grant'
  });

  if (grant.inserted) {
    await tx.playerBalances.incrementPremiumGems(
      req.userId,
      productCatalog[result.productId].grantAmount
    );
  }
});

재화 지급량은 클라이언트가 보내는 수량이 아니라 서버의 상품 카탈로그에서 읽어야 합니다. 상품 카탈로그에는 상품 ID, 지급량, 판매 국가, 가격 구간, 활성 기간을 버전과 함께 보관합니다. upsertLatestStatus는 거래의 최신 검증 상태를 갱신하고, 상태 이벤트가 순서 뒤바뀌어도 환불·철회 상태를 과거 PURCHASED 응답이 덮지 않도록 공급자 이벤트 시각이나 버전을 비교해야 합니다. 환불 시 이미 지급한 재화를 회수할지는 별도 원장 조정 정책으로 처리합니다.

환불 어뷰징 패턴은 어떻게 탐지하는가?

환불 자체가 부정행위라는 뜻은 아닙니다. 결제 오류, 미성년자 결제, 가족 결제, 플랫폼 정책에 따른 정상 환불이 존재하므로 자동 차단보다 위험도 분류와 단계적 대응이 적절합니다.

탐지에 유용한 특징량은 다음과 같습니다.

특징량예시해석
환불률최근 30일 환불 금액 ÷ 결제 금액반복 환불 여부
환불 지연 시간구매 후 환불까지의 시간구매 직후 반복 환불 패턴
지급 후 소비율지급 재화 중 소비한 비율환불 전 재화 소진 여부
계정 연결도동일 기기·결제 계정·IP의 계정 수다계정 악용 가능성
거래 속도짧은 시간 내 구매·환불 횟수자동화 또는 공격 도구 가능성
거래 금액 변화평소보다 급격한 고액 구매탈취 계정 또는 비정상 결제
이벤트 불일치스토어 환불 이벤트와 게임 상태 차이회수 지연 또는 처리 누락

계정 단위 특징만 보면 다계정 공격을 놓칠 수 있습니다. 사용자, 기기 해시, 결제 계정, IP 대역, 결제 토큰의 관계를 그래프로 구성하면 여러 계정이 하나의 기기나 토큰을 공유하는 패턴을 찾을 수 있습니다.

AI 자동 검증 파이프라인은 어떤 구조로 설계할까?

AI 파이프라인은 다음 세 계층으로 나누는 것이 실용적입니다.

  1. 결정론적 검증 계층: 스토어 API, 서명, 상품 ID, 거래 상태, 중복 여부를 검증합니다.
  2. 특징량 계층: 계정·기기·결제·환불·재화 소비 이벤트를 시간 창별로 집계합니다.
  3. 위험 판정 계층: 규칙 엔진과 머신러닝 모델을 결합해 승인, 보류, 수동 검토, 제한으로 분류합니다.
flowchart LR
    A[클라이언트 구매 요청] --> B[검증 API]
    B --> C{스토어 공식 API 검증}
    C -->|실패| D[지급 거부 및 감사 로그]
    C -->|성공| E[거래 멱등성 확인]
    E -->|중복| F[기존 결과 반환]
    E -->|신규| G[결제·환불·경제 이벤트 저장]
    G --> H[특징량 생성]
    H --> I[규칙 엔진]
    H --> J[이상탐지 모델]
    I --> K[위험 점수 결합]
    J --> K
    K --> L{정책 임계값}
    L -->|낮음| M[자동 지급]
    L -->|중간| N[지연 지급 또는 추가 검증]
    L -->|높음| O[수동 검토·거래 제한]

규칙 엔진과 모델을 결합하는 방법

결제 보안에서는 설명 가능한 규칙이 중요합니다. 모델이 높은 점수를 냈다는 이유만으로 계정을 영구 정지하면 오탐 이의제기와 운영 리스크가 커집니다.

간단한 위험 점수는 다음처럼 구성할 수 있습니다.

R=w1F+w2C+w3D+w4N+w5ER = w_1 F + w_2 C + w_3 D + w_4 N + w_5 E

여기서 F는 환불 패턴 점수, C는 계정·기기 연결도 점수, D는 거래 속도 점수, N은 신규성 또는 평소 행동과의 차이, E는 스토어 이벤트와 게임 상태의 불일치 점수입니다. 가중치 w_i는 오탐 비용과 미탐 비용을 고려해 검증 데이터로 조정합니다.

초기 운영에서는 다음과 같은 규칙을 함께 둡니다.

  • 공식 API 검증 실패: 즉시 지급 거부
  • 동일 transaction ID의 재요청: 최초 결과 반환
  • 환불 또는 철회 이벤트 수신: 신규 지급 차단
  • 짧은 시간에 여러 계정이 동일 기기 해시 사용: 위험 점수 가산
  • 구매 후 즉시 재화 대부분 소진 후 반복 환불: 수동 검토 큐로 이동
  • 모델 점수가 높지만 정상 결제 이력이 충분한 계정: 자동 영구 정지 대신 지급 지연

모델 후보로는 초기에는 Isolation Forest나 Local Outlier Factor 같은 비지도 이상탐지를 사용할 수 있습니다. 라벨이 충분히 쌓이면 XGBoost 또는 LightGBM 기반 이진 분류 모델로 전환하고 SHAP 값으로 판정 근거를 운영자에게 제공합니다.

모델 학습 데이터에는 반드시 시간 기준을 적용해야 합니다. 미래의 환불 결과를 과거의 승인 시점 특징량에 섞으면 데이터 누수로 인해 실제 운영 성능이 과대평가됩니다.

환불 이벤트와 결제 상태를 어떻게 동기화할까?

구매 요청 시점의 동기식 검증만으로는 이후 환불과 철회를 처리할 수 없습니다. Apple App Store Server Notifications V2와 Google Play Real-time Developer Notifications를 이벤트 수집 경로로 사용하고 상태 변경을 비동기 처리해야 합니다.

이벤트 처리 순서는 다음과 같습니다.

  1. 스토어 웹훅의 서명과 발행자를 검증합니다.
  2. 이벤트 ID를 기준으로 중복 이벤트를 제거합니다.
  3. transaction ID 또는 purchase token으로 거래를 조회합니다.
  4. 거래 상태를 PURCHASED, REFUNDED, REVOKED 등으로 갱신합니다.
  5. 정책에 따라 회수 대상 재화와 계정 제한 상태를 계산합니다.
  6. 감사 로그와 운영 알림을 남깁니다.

웹훅은 네트워크 오류로 재전송될 수 있으므로 이벤트 ID에 유니크 제약을 적용합니다. 또한 이벤트 수신 순서가 뒤섞일 수 있으므로 단순히 마지막으로 도착한 이벤트를 최신 상태로 취급하면 안 됩니다. 플랫폼의 발생 시각과 거래 상태 전이 규칙을 함께 검증해야 합니다.

운영 환경에서 꼭 기록해야 할 감사 데이터

결제 보안 시스템은 판정 결과보다 판정 과정을 재현할 수 있어야 합니다. 최소한 다음 데이터를 보관합니다.

데이터목적
원본 페이로드 해시동일 요청·변조 여부 확인
스토어 API 응답 상태공식 검증 결과 재현
모델 버전과 특징량 버전과거 판정 재현
규칙 ID와 적용 값운영자 설명과 이의제기 대응
거래 상태 전이환불·철회 처리 감사
지급 원장 ID재화 지급과 회수 추적
수동 검토 결과재학습용 라벨 생성

개인정보와 결제 토큰은 보존 기간과 접근 권한을 분리합니다. 분석용 데이터셋에는 원본 식별자 대신 가명화된 키와 집계 특징량을 사용하고 운영 로그에는 카드 번호나 인증 정보가 유입되지 않도록 입력 단계에서 차단합니다.

성능과 장애 대응에서 주의할 점

스토어 API 호출이 느리거나 일시적으로 실패하면 결제 요청 전체를 무한 재시도해서는 안 됩니다. API 타임아웃, 회로 차단기, 지수 백오프, 재처리 큐를 사용하고 검증이 끝나기 전에는 재화를 지급하지 않는 정책을 유지합니다.

권장 상태는 다음과 같습니다.

  • RECEIVED: 서버가 요청을 수신한 상태
  • VERIFYING: 스토어 검증 중인 상태
  • PENDING: 결제 결과가 아직 확정되지 않은 상태
  • GRANTED: 검증 및 지급 원장 기록이 완료된 상태
  • REFUNDED: 환불이 확인된 상태
  • REVOKED: 구매 권리가 철회된 상태
  • REJECTED: 위변조·불일치·정책 위반으로 거부된 상태

멀티플레이 게임에서는 클라이언트가 보유한 재화 수량을 다른 플레이어에게 전파하지 말고 서버 권위 모델에서 계산해야 합니다. 결제 결과는 서버 경제 원장에 반영한 뒤 게임 세션과 캐시를 갱신합니다. 캐시 갱신이 실패해도 원장 기록이 중복 지급으로 이어지지 않도록 이벤트 소비를 멱등적으로 설계합니다.

자주 묻는 질문 (FAQ)

클라이언트에서 영수증을 검증하면 안 되나요?

안 됩니다. 클라이언트는 공격자가 수정할 수 있으므로 진위 판단과 재화 지급은 서버에서 수행해야 합니다. 클라이언트 검증은 UX 개선이나 네트워크 절약을 위한 보조 수단으로만 사용합니다.

AI가 환불 사용자를 자동 정지해도 되나요?

고위험 거래의 즉시 지급 차단에는 사용할 수 있지만 영구 정지는 수동 검토나 추가 신호를 거치는 편이 안전합니다. 정상 환불과 부정 환불을 구분해야 하며 모델의 판정 근거와 이의제기 절차를 남겨야 합니다.

영수증 검증 API가 일시적으로 실패하면 재화를 지급해야 하나요?

검증 결과가 확정되지 않은 상태에서는 지급하지 않는 것이 원칙입니다. 요청을 PENDING으로 저장하고 재처리 큐에서 재시도한 뒤 검증 완료 이벤트가 도착했을 때 멱등적으로 지급합니다.

정리

인게임 결제 보안의 기준점은 AI 모델이 아니라 공식 스토어의 서버 검증과 서버 권위 경제 원장입니다. 그 위에 거래 상태 전이, 환불 이벤트, 계정·기기 관계, 재화 소비 패턴을 결합하면 영수증 위변조와 환불 어뷰징을 자동으로 분류할 수 있습니다.

운영을 시작할 때는 공식 API 검증 → 거래 멱등성 → 비동기 환불 동기화 → 규칙 기반 차단 → 모델 기반 위험 점수 순서로 범위를 넓히는 것이 좋습니다. 이 구조는 탐지 정확도뿐 아니라 재현 가능한 감사와 안전한 수동 대응까지 함께 확보합니다.