← 문서 목록

테스트 결과서 (승인·취소·정산 포함)

NestPay 선불 카드형 지갑 · 자동화 테스트 + 거래 E2E 실증 결과 · 실제 실행/DB 기준

문서 성격 본 결과서의 모든 수치는 추정이 아닌 실제 실행 결과입니다. 자동화 테스트는 flutter test 를 각 패키지에서 직접 실행해 통과 수를 얻었고, 거래 E2E 는 로컬 도커 환경에서 실제 API 를 호출해 만들어진 transactions·ledger_entries 실데이터를 docker exec ... mysql 로 조회해 근거로 제시했습니다.

검증 일시: 2026-07-27 · 대상 커밋: 로컬 작업본(미배포) · 백엔드 paynest-v1/apps/api(Spring Boot 3·MyBatis·Flyway) · 앱 paynest-app(Flutter 3.44.7).

목차

1. 검증 환경 및 원천 데이터

실행 환경은 로컬 도커 컴포즈이며, 검증 시점에 아래 컨테이너가 정상 기동(Up) 상태였습니다.

컨테이너역할상태(검증 시점)
nestpay-mariadbMariaDB (스키마 nestpay)Up (healthy)
nestpay-apiSpring Boot APIUp
nestpay-web정적/관리자 웹Up
nestpay-storage오브젝트 스토리지(파일)Up (healthy)

거래 원천 표: transactions 12건(연속 id 중 1건은 롤백 갭, 5절 참조), ledger_entries 28행. 시스템 지갑 6개(정산클리어링·미매칭·수수료수익·선물에스크로·소멸·몰수) + 회원카드 2개 + 매장 1개.

지갑 매핑 (분개 해석 기준)

지갑 id구분식별검증 시점 잔액
1SYSTEMSETTLEMENT_CLEARING (대외청산)0 (원장으로 도출·아래 5절)
3SYSTEMFEE_REVENUE (수수료수익)0 (원장으로 도출)
9USER_CARD회원카드 #361,200
10USER_CARD회원카드 #460,000
12MERCHANT매장 #214,326

2. 자동화 테스트 결과

2.1 앱(Flutter) 자동화 테스트 — 전체 통과

flutter test 를 3개 패키지에서 각각 실제 실행한 결과입니다(Flutter 3.44.7 stable).

패키지테스트 파일케이스통과실패
packages/nestpay_sharedasync_button_test.dart220
user_app (회원앱)test/widget_test.dart110
store_app (매장앱)test/widget_test.dart110
합계440

확인된 케이스 내용: (shared) "누르면 잠기고 로딩이 보였다가, 끝나면 다시 풀린다"(비동기 버튼 중복탭 방지), "실패(ApiFailure)하면 서버 안내문이 스낵바로 보인다"(에러 표시) · (user_app/store_app) "앱이 켜지면 비로그인 상태에서는 환영 화면이 보인다".

2.2 백엔드(API) 자동화 테스트 — 테스트 코드 미비

확인 사실: apps/api/src/ 아래에 main 디렉터리만 존재하고 src/test 디렉터리가 없습니다. 즉 작성된 백엔드 단위/통합 테스트 클래스가 0개입니다. 다만 빌드 설정(build.gradle)에는 테스트 의존성(spring-boot-starter-test, mybatis-spring-boot-starter-test:3.0.3, JUnit5)이 이미 구성되어 있어, 테스트 골격을 추가할 준비는 되어 있습니다.

판정: 백엔드는 자동화 테스트 없음 → 아래 3절의 수동 E2E(실제 API 호출 + DB 검증)로 핵심 거래 로직을 검증합니다. 정직하게 명시하며, 후속 과제(6절)에 단위/통합 테스트 도입을 포함합니다.

3. 주요 거래 E2E 결과 (승인·취소·출금·정산·멱등)

이번 구축에서 실제로 API 를 호출해 발생시킨 거래를 transactions·ledger_entries 실데이터로 검증했습니다. 각 시나리오의 기대는 요구사항/코드 로직이고, 결과는 DB 실측입니다.

#시나리오기대 동작실측 결과 (DB)판정
E1 결제 승인
QR 주문 결제
회원 잔액 선차감, 매장에 순액 적립, 수수료는 별도 수익지갑으로 분리. 거래 시점 수수료 정책 스냅샷 보존. txn#4 PAYMENT/QR_ORDER CONFIRMED 15,000, fee 450 (fee_rate_snap=3.0000, fee_fixed_snap=0).
원장: DR 회원(w9) 15,000 → 잔액 65,000 · CR 매장(w12) 14,550 · CR 수수료(w3) 450.
통과
E2 결제 취소
승인 건 역분개
원거래를 역분개(매장·수수료 회수, 회원 전액 CR)하여 회원 잔액 복원, 원거래 상태를 CANCELED 로 마킹, 로트 속성 승계 복원. txn#6 결제 1,000(fee 150: rate 5.0000·fixed 100) → txn#7 CANCEL/QR_CANCEL(related_txn_id=6).
역분개: DR 매장(w12) 850 · DR 수수료(w3) 150 · CR 회원(w9) 1,000 → 잔액 64,000→65,000 복원. 원거래 status=CANCELED.
통과
E3 출금
회원 포인트 출금
신청 즉시 선차감(HOLD): 회원 총액 DR, 대외청산에 순액 CR, 수수료 CR. 실이체는 워커가 수행(계약 전 스텁). txn#8 WITHDRAW/FIRMBANK HOLD 3,000, fee 530 (rate 1.0000·fixed 500 → 30+500).
원장: DR 회원(w9) 3,000 → 잔액 62,000 즉시 차감 · CR 청산(w1) 2,470 · CR 수수료(w3) 530.
통과
(실이체 스텁)
E4 정산 출금
매장 정산 신청
매장 지갑 선차감 후 대외청산으로 이전. 멱등키로 같은 신청 중복 방지. txn#12 WITHDRAW/SETTLEMENT CONFIRMED 1,000, idempotency_key=SETTLE:settle-e2e-1785090450.
원장: DR 매장(w12) 1,000 → 잔액 14,326 · CR 청산(w1) 1,000.
통과
E5 멱등키 중복 거부
같은 거래키 재요청
동일 (주체·멱등키) 재요청은 신규 거래를 만들지 않고 409 Conflict 로 거부. 유니크 인덱스 ux_txn_idem = (initiator_type, initiator_id, idempotency_key) 존재 → 중복 시 DuplicateKeyExceptionGlobalExceptionHandler409 CONFLICT 반환. 통과
E6 인증 재사용(replay) 거부
같은 챌린지·서명 재사용
거래 인증에 쓰인 1회용 챌린지를 재사용하면 거부. used_challenges(PK=challenge) 에 INSERT IGNORE(NonceMapper.consume). 반환 0=이미 사용 → "이미 사용된 패스키 인증입니다" 거부(AppPasskeyService). 등록·로그인·거래 인증 모두 이 검사를 지남. 통과

3.1 근거 코드 위치

시나리오파일핵심
E1 결제service/AppPaymentService.javaL192~240 수수료 Fees.calc·스냅샷 저장·DR/CR/FEE 분개
E2 취소service/AppCancelService.javaL103~121 역분개(매장 DR·수수료 DR·회원 CR)·잔액복원·markCanceled
E3 출금service/AppWithdrawService.javaL112~141 선차감 HOLD 분개 / L170~190 워커 실행(스텁, 항상 성공)
E4 정산service/StoreSettleService.javaL81~122 멱등키 SETTLE:·선차감·청산 CR
E5 멱등 409config/GlobalExceptionHandler.javaL104~106 DuplicateKeyException→409
E6 replaymapper/NonceMapper.xml · service/AppPasskeyService.javaINSERT IGNORE / L199~200 소진결과 0=거부

4. 보안 검증 요약

항목검증 내용 / 근거판정
서명·토큰 위조 차단 거래·로그인 인증은 등록된 공개키로 crypto.verify(publicKey, challenge, signature) 통과 실패 시 거부. 챌린지 토큰의 주체·용도(R/K/T)까지 대조. 확인
재사용(replay) 차단 1회용 챌린지 used_challenges(PK) + INSERT IGNORE. 검증 시점 12건 소진 기록 존재, 만료기록 자동정리(purgeExpired). 확인
멱등(중복 처리) 방지 모든 거래(입금·출금·선물·결제·정산)에 사용자 스코프 멱등키 유니크 인덱스 → 재요청 409. 확인
SQL 인젝션 방어 MyBatis 매퍼 전수 점검: 위험한 문자열 치환 ${...} 0건, 전부 #{...} 파라미터 바인딩(PreparedStatement). 확인
Rate limit(요청 제한) rate_limit_rules 14개 규칙 활성(enabled=1): 로그인 10회/5분, 결제생성 10회/5분, 출금신청 10회/5분, 정산신청 5회/분, 가입 5회/시간, 로그인 IP 스프레이 차단 등. USER/IP 기준 분리. 확인
원장 무결성 차변합=대변합, 지갑 잔액 = 원장 누적치 일치(5절 상세). 확인

5. 원장 무결성 검증 (ΣDR=ΣCR·지갑 재현)

5.1 복식부기 균형

ledger_entries 전체 방향별 합계를 실측했습니다.

방향행 수금액 합계
DR (차변)12201,800
CR (대변)16201,800
차이0 (완전 균형)

건별로도 각 거래의 DR 총액 = CR 총액입니다. 예) 결제 15,000 = 매장 14,550 + 수수료 450 · 출금 3,000 = 청산 2,470 + 수수료 530 · 취소 1,000 = 매장 850 + 수수료 150.

5.2 지갑 잔액 재현(원장 누적 = 지갑 잔액)

지갑원장 누적 계산DB 잔액판정
회원카드 #3 (w9)100,000 −20,000(선물) −15,000(결제) −1,000(결제) +1,000(취소) −3,000(출금) −500 −300 = 61,20061,200일치
회원카드 #4 (w10)50,000 +20,000(선물수신) −10,000(출금) = 60,00060,000일치
매장 #2 (w12)+14,550 +850 −850(취소) +485 +291 −1,000(정산) = 14,32614,326일치

정직한 명시 — 시스템 지갑 잔액 컬럼: 시스템 지갑(대외청산·수수료수익)의 wallets.balance 는 0 이며, 해당 원장행의 balance_after 도 NULL 입니다. 즉 시스템 계정의 집계 잔액은 지갑 컬럼이 아니라 원장(ledger)에서 도출하는 구조입니다. 회원·매장 지갑은 balance_after 누적이 wallets.balance 와 일치함을 위 표에서 확인했습니다. 전역 ΣDR=ΣCR 이 성립하므로 원장 자체의 무결성은 보장됩니다.

5.3 auto_increment 갭(id 9) — 롤백 흔적

transactions id 가 …8, (9 없음), 10… 으로 1건 비어 있습니다. auto_increment 값이 소진된 뒤 트랜잭션이 롤백된 시도가 1건 있었음을 의미합니다(E2E 중 중복/검증 실패로 추정). 다만 reject_logs 는 0건이라 원인이 별도 기록되지 않았습니다. 원인 단정 불가(확인필요) — 관측성 보강을 후속 과제로 둡니다.

6. 한계 및 후속 과제

구분현재 한계후속 과제
백엔드 테스트단위/통합 테스트 클래스 0개(src/test 부재). 커버리지 자동 측정 불가.서비스별 JUnit5 단위테스트 + MyBatis 통합테스트 도입(멱등·역분개·수수료 계산 우선).
앱 테스트 범위Flutter 4건은 스모크 수준(버튼·환영화면). 거래 흐름 위젯/골든 테스트 부재.결제·취소·출금 화면 위젯 테스트 및 상태 전이 테스트 확대.
외부연동 실증펌뱅킹 실이체·본인인증·은행 실명조회는 계약 전 스텁(항상 성공). 실제 지급 미실증.연동 계약 후 샌드박스→운영 순으로 실이체 E2E 재검증.
성능/동시성부하·동시성 성능 수치 미측정(로컬 단건 E2E 위주).착수 시 요구사항정의서에 확정할 성능 목표치 기준 부하/동시성 테스트 수행.
관측성롤백 시도(id 9 갭) 원인이 reject_logs 에 미기록.거부/롤백 사유의 구조적 로깅 보강.

총평: 앱 자동화 테스트 4건 전부 통과, 핵심 거래 6개 시나리오(승인·취소·출금·정산·멱등·재사용거부)를 실데이터로 검증했으며 원장은 완전 균형(ΣDR=ΣCR=201,800)·지갑 잔액 재현 일치를 확인했습니다. 백엔드 단위테스트 부재와 외부연동 스텁은 위 후속 과제로 관리합니다.

NestPay 산출물 · (주)페이네스트 · 작성일 2026-07-27 · 실제 코드/DB 기준 · 외부연동(펌뱅킹·본인인증·은행 실명조회)은 계약 전 스텁 상태임을 명시