KBKRW 외부 송금 부하 시험 보고서 (동시 사용자 50명·100명)
요약
처리 성능: 동시 사용자 100명에서 완료 처리량 초당 3.61건, 성공률 95.02%
처리 규모: 두 회차 합계 2,355건 송금 완료, 원장·체인 불일치 신규 발생 0건
시험 조건: 요청 접수부터 온체인 확정과 원장 반영까지 전 구간을 지나는 외부 송금
동시 사용자 50명과 100명 두 규모로 외부 송금 부하 시험을 수행하였다. 두 회차 모두 목표 성공률 95%를 충족하였다.
다만 이 결과는 시험 도중 발견한 결함 하나를 고친 뒤에 얻은 것이다. 최초 시험은 시작 7초 만에 전면 중단되었고 완료 건수가 0이었다. 원인은 목적지와 금액이 같은 두 출금이 동시에 서명될 때 트랜잭션이 완전히 동일해져 서명값이 겹치는 것이었다. 그 충돌 한 건이 출금 처리기 전체를 영구 정지시켰다. 상위 저장소에 이미 수정이 있었으나 시험 환경에 반영되어 있지 않았다. 해당 수정을 적용한 이미지를 새로 만들어 배포한 뒤 재시험하였다.
재시험에서는 같은 충돌이 50명 회차에서 29회, 100명 회차에서 53회 발생하였으나 처리기는 한 번도 멈추지 않았다. 매번 새 블록해시로 다시 서명해 정상 처리되었다.
1. 시험 개요
본 시험은 KB PoC 앱의 외부 송금 경로가 동시 사용자 부하에서 정상 동작하는지 확인하기 위하여 수행하였다. 외부 송금을 대상으로 삼은 이유는 KB가 요구한 처리 경로를 모두 지나는 흐름이 외부로 나가는 거래뿐이기 때문이다. 충전, 환전, 행내 송금, 행내 결제는 원장에서 끝나며 체인을 타지 않는다.
부하는 앱 서버 내부에서 발생시켰다. 부하 발생기를 앱 서버의 도커 네트워크에 직접 붙여 백엔드 컨테이너를 호출하였으므로 로드밸런서와 외부 구간의 지연이 측정치에 섞이지 않는다.
체인 접속은 전용 유료 엔드포인트를 사용하였다. 직전까지 쓰던 무료 RPC는 크레딧이 소진되어 있었으므로 시험에 앞서 교체하고 정상 동작을 확인하였다.
2. 결과 요약
| 항목 | 50명 회차 | 100명 회차 |
|---|---|---|
| 동시 사용자 | 50명 | 100명 |
| 지속 시간 | 5분 | 5분 |
| 송금 요청 접수 | 1,109건 | 1,246건 |
| 송금 완료 | 1,068건 | 1,184건 |
| 성공률 | 96.30% | 95.02% |
| 완료 처리량 | 초당 3.41건 | 초당 3.61건 |
| 완주 시간 중앙값 | 12.35초 | 25.80초 |
| 완주 시간 p95 | 21.71초 | 32.61초 |
| 출금 처리기 정지 | 0회 | 0회 |
| 원장·체인 불일치 신규 | 0건 | 0건 |
부하를 두 배로 올렸을 때 완료 처리량은 초당 3.41건에서 3.61건으로 거의 변하지 않았고 완주 시간만 두 배가 되었다. 현재 구성이 초당 3.5건 부근에서 포화한다는 뜻이다.
3. 시험 환경
3.1 구성
| 항목 | 값 |
|---|---|
| 대상 | KB PoC 앱 백엔드 |
| 부하 발생기 | k6, 앱 서버 내부 컨테이너로 실행 |
| 접속 경로 | 도커 네트워크 직결, 로드밸런서와 외부 구간 미경유 |
| 체인 | 솔라나 테스트넷 |
| 체인 접속 | 전용 RPC 엔드포인트 |
| 대상 경로 | 외부 송금 전 구간 |
3.2 시험 조건
| 항목 | 값 |
|---|---|
| 시험 계정 | 50명 회차 50개, 100명 회차 73개 |
| 송금 목적지 | 10개 주소 순환 배정 |
| 건당 금액 | 5,000 KBKRW |
| 트래블룰 | 미적용, 임계 1,000,000 KBKRW 미만 |
| 완주 판정 | 거래 상태가 완료로 바뀔 때까지 1초 간격 조회, 최대 3분 |
| 상관키 | 요청 메모에 회차·사용자·반복 번호를 기록 |
금액을 트래블룰 임계 아래로 둔 것은 의도한 조건이다. 임계를 넘기면 상대 기관 검증이 붙는데 현재 상대 기관이 응답하지 않아 전 건이 보류에 묶인다. 그 경우 송금 처리 성능 자체를 측정할 수 없다.
100명 회차의 계정이 73개인 이유는 신규 가입 축하금을 지급하는 재원이 시험 준비 중 소진되었기 때문이다. 잔여 450,099 KBKRW인데 가입 1건당 1,000,000 KBKRW가 필요하여 계정을 100개까지 늘리지 못하였다.
3.3 노드월렛 서비스
| 서비스 | 역할 |
|---|---|
| coordinator | 출금 승인, 서명 요청, 브로드캐스트, 확정 처리 |
| approver | 정책 평가와 승인 판정 |
| executor | 실행키 서명 |
| chain-relay | 체인 조회와 입금 감지 |
| PostgreSQL | 원장, 체인, 정책 데이터베이스 |
4. 최초 시험의 실패와 원인
4.1 실패 양상
동시 사용자 50명으로 수행한 최초 시험은 실패하였다. 요청 100건이 모두 정상 접수되고 승인 판정까지 통과하였으나 완료된 건이 하나도 없었다.
| 구간 | 도달 건수 |
|---|---|
| 요청 접수 | 100건 |
| 승인 판정 | 100건 |
| 서명 | 34건 |
| 브로드캐스트 | 33건 |
| 온체인 확정 | 0건 |
승인과 서명 사이에서 처리가 끊겼다. 앱 거래 100건은 전량 진행 중 상태로 남아 사용자 화면에서는 송금이 끝나지 않은 것으로 보였다.
4.2 원인
출금 처리기가 원장의 트랜잭션 서명 유일성 제약을 위반하며 정지하였다. 시험 개시 7초 만이었다.
충돌한 두 건은 목적지와 금액이 같았고 같은 순간에 처리되어 같은 블록해시를 사용하였다. 솔라나 트랜잭션의 서명은 트랜잭션 내용에서 결정되므로 내용이 완전히 같으면 서명값도 같아진다.
| 항목 | 출금 A | 출금 B |
|---|---|---|
| 요청 시각 | 07:13:38.997 | 07:13:38.999 |
| 목적지 | 동일 | 동일 |
| 금액 | 5,000 KBKRW | 5,000 KBKRW |
| 서명값 | 동일 | 동일 |
| 결과 | 전송 기록 | 원장 기록 실패, 처리기 정지 |
출금 트랜잭션에 건마다 달라지는 값이 들어 있지 않아 목적지와 금액이 같은 두 출금이 같은 블록해시 유효 구간에 서명되면 반드시 충돌한다. 이번 시험은 목적지 10개에 사용자 50명, 금액 고정이었으므로 충돌은 필연이었다.
두 번째 문제는 오류 처리 방식이었다. 한 건의 원장 기록 실패가 해당 건의 실패로 끝나지 않고 출금 처리기 전체를 정지시켰다. 그 결과 뒤이어 접수된 정상 요청까지 승인 상태에서 멈추었다.
4.3 조치
상위 저장소에 해당 결함의 수정이 이미 있었다. 중복 서명이 발생하면 정지하는 대신 새 블록해시로 다시 서명해 재시도하도록 바꾼 수정이다. 시험 환경의 이미지에는 이 수정이 반영되어 있지 않았고 레지스트리의 최신 이미지에도 없었다.
시험 환경이 사용하는 빌드 브랜치에 해당 수정만 반영한 이미지를 만들어 배포하였다. 트래블룰 관련 기능 등 시험 환경 전용 구현이 빠지지 않았음을 배포 전에 확인하였다.
5. 재시험 결과
5.1 구간별 소요 시간
원장에 기록된 시각을 기준으로 산출하였다.
| 구간 | 50명 중앙값 | 50명 p95 | 100명 중앙값 | 100명 p95 |
|---|---|---|---|---|
| 요청 접수 → 원장 반영 | 11.19초 | 20.52초 | 24.77초 | 31.66초 |
| 승인 판정 | 0.26초 | 0.28초 | 0.26초 | 0.29초 |
| 서명 | 6.01초 | 8.39초 | 18.58초 | 20.87초 |
| 브로드캐스트 | 0.03초 | 0.04초 | 0.03초 | 0.04초 |
| 온체인 확정 | 4.09초 | 12.24초 | 4.92초 | 14.05초 |
5.2 결함 수정의 동작 확인
| 항목 | 50명 회차 | 100명 회차 |
|---|---|---|
| 중복 서명 충돌 발생 | 29회 | 53회 |
| 충돌로 인한 처리기 정지 | 0회 | 0회 |
충돌은 수정 전과 같은 빈도로 계속 발생하였으나 처리기는 한 번도 멈추지 않았다. 매번 새 블록해시로 다시 서명해 정상 처리되었다. 수정이 의도대로 동작함을 확인한 근거다.
5.3 실패 건의 성격
| 회차 | 실패 건수 | 체인 전송 여부 |
|---|---|---|
| 50명 | 41건 | 전량 서명 이전 종료, 체인 미전송 |
| 100명 | 62건 | 전량 서명 이전 종료, 체인 미전송 |
두 회차의 실패는 모두 서명 이전 단계에서 끝나 체인에 나가지 않았다. 원장에 서명값이 남지 않은 것으로 확인하였다. 자금 이동 없이 실패로 정확히 기록되었으므로 원장과 체인이 어긋난 건은 새로 생기지 않았다.
6. 병목
부하를 두 배로 올렸을 때 구간별로 반응이 갈렸다.
| 구간 | 50명 대비 100명 |
|---|---|
| 승인 판정 | 변화 없음 |
| 서명 | 3.1배 |
| 브로드캐스트 | 변화 없음 |
| 온체인 확정 | 1.2배 |
승인 판정과 브로드캐스트는 부하가 두 배가 되어도 그대로였고 온체인 확정도 거의 변하지 않았다. 서명만 3배로 늘었다. 100명 회차의 완주 시간 25.80초 가운데 18.58초가 서명 대기다.
체인도 정책 평가도 아니고 서명이 한계라는 뜻이다. 처리량을 더 올리려면 서명 처리 능력을 먼저 늘려야 한다.
7. 남은 문제
최초 시험에서 처리기가 정지한 뒤 재기동하는 과정에서 원장과 체인이 어긋난 건이 76건 생겼다. 체인에서는 정상 처리되었으나 원장에는 실패로 기록된 건들이다.
전송까지 갔으나 확정 기록이 없는 건을 재기동 시 실패로 단정한 결과다. 솔라나에서는 전송한 트랜잭션이 뒤늦게 확정될 수 있으므로 확정 여부를 체인에 다시 묻지 않고 실패로 확정하면 이런 불일치가 발생한다.
이 현상에 대한 수정도 상위 저장소에 있으나 이번 배포에는 넣지 않았다. 처리기가 정지하지 않으면 재기동할 일이 없어 새로 발생하지는 않지만 이미 생긴 76건은 체인 결과를 기준으로 원장을 정정해야 한다.
8. 종합 의견
동시 사용자 100명 규모에서 외부 송금은 성공률 95.02%, 완료 처리량 초당 3.61건으로 목표를 충족하였다. 요청 접수와 승인 판정은 두 회차 모두 1초 안쪽에 처리되었고 체인 전송과 확정도 부하에 크게 흔들리지 않았다.
다만 현재 구성은 초당 3.5건 부근에서 포화한다. 부하를 두 배로 올려도 처리량이 늘지 않고 대기 시간만 길어졌으며 그 대기는 대부분 서명 구간에서 발생하였다. 더 높은 처리량이 필요하면 서명 처리 능력 확충이 선행되어야 한다.
이번 시험에서 드러난 결함은 동시 요청이 겹치는 순간에만 나타나는 종류였다. 소규모 시험에서는 보이지 않았고 동시 사용자 3명 조건에서는 전 건이 정상 완주하였다. 규모를 갖춘 부하 시험이 아니었으면 발견되지 않았을 결함이다.
9. 측정 근거
구간별 소요 시간은 부하 발생기의 측정값이 아니라 원장에 기록된 시각을 사용하였다. 요청 접수와 원장 반영은 앱 데이터베이스에서, 승인·서명·전송·확정은 노드월렛 원장에서 추출하였다. 회차 구분은 요청 메모에 심은 상관키로 하였으므로 다른 회차의 거래가 섞이지 않는다.
온체인 결과는 솔라나 표준 조회 방식으로 확인하였으며 조회 시 과거 기록까지 포함하도록 지정하였다.
| 자료 | 내용 |
|---|---|
| 50명 회차 원자료 | 앱 원장, 출금 단계, 스크리닝 기록 |
| 100명 회차 원자료 | 앱 원장, 출금 단계, 스크리닝 기록 |
| 실시간 대시보드 녹화 | 회차별 영상 각 5분 30초 |
<!-- HUMANIZE-SUMMARY v1.6.1 run_id: 2026-08-24-001 route: light (route_hint=light, risk_band=medium score 5) · 강도 보수 genre: report (격식체 -다 유지) metrics: char_in: 5738 char_out: 5735 change_rate: 0.6% self_check: 6/6 grade: A categories: # before → after C-11 연결어미 뒤 쉼표 (★ ending_comma_rate z=+2.66 S1 트리거): 7 → 0 A-18 관형절 다중 중첩 문장: 3 → 2 A-1/A-2/A-7/A-8/A-16 번역투 어휘: 0 → 0 (원문에 없음) D-1~D-7 AI 관용구·hype: 0 → 0 H-1 문두 접속사 5회+: 0 → 0 ('다만' 2회 — 임계 미만, 보존) I-3 '~다는 뜻이다': 2 → 2 (규칙 허용 한도 2회 이내 — 보존) J-1/J-2/C-5 볼드·따옴표·이모지: 0 → 0 untouched: - 마크다운 표 11개 전량 (수치·항목명·구분선 원형) - 헤딩 텍스트 전량 - 수치·시각·고유명사 (KBKRW·솔라나·노드월렛·coordinator·approver·executor·chain-relay·PostgreSQL·k6·RPC·KB PoC) self_check: - 고유명사·수치·인용·내용 앵커 100% 보존: OK - 변경률 30% 이하: OK (0.6%) - 장르 이탈 없음: OK (기술 보고서 유지) - register 보존: OK ('-하였다' 원형 유지, '했→하였' 상향 없음, 하향 없음) - S1 잔존 0건: OK (C-11 7건 전량 해소) - 인공 표현 추가 없음: OK (신규 어휘 삽입 0, 문장 분리 시 접속 표현 신설 없음) highlights: - id: C-11 + A-18 before: "…서명값이 겹치는 것이었고, 그 충돌 한 건이 출금 처리기 전체를 영구 정지시켰다." after: "…서명값이 겹치는 것이었다. 그 충돌 한 건이 출금 처리기 전체를 영구 정지시켰다." - id: C-11 + A-18 before: "상위 저장소에 이미 수정이 있었으나 시험 환경에 반영되어 있지 않아, 해당 수정을 적용한 이미지를 새로 만들어 배포한 뒤 재시험하였다." after: "상위 저장소에 이미 수정이 있었으나 시험 환경에 반영되어 있지 않았다. 해당 수정을 적용한 이미지를 새로 만들어 배포한 뒤 재시험하였다." - id: C-11 + A-18 before: "…전 건이 보류에 묶이며, 그 경우 송금 처리 성능 자체를 측정할 수 없다." after: "…전 건이 보류에 묶인다. 그 경우 송금 처리 성능 자체를 측정할 수 없다." - id: C-11 before: "출금 트랜잭션에 건마다 달라지는 값이 들어 있지 않아, 목적지와 금액이…" after: "출금 트랜잭션에 건마다 달라지는 값이 들어 있지 않아 목적지와 금액이…" - id: C-11 before: "…이 수정이 반영되어 있지 않았고, 레지스트리의 최신 이미지에도 없었다." after: "…이 수정이 반영되어 있지 않았고 레지스트리의 최신 이미지에도 없었다." residual_findings: - id: A-1 severity: S1(조건부) detail: "'이 현상에 대한 수정도' — 1회 단발이라 A-1 '남발' 임계 미달. 보수 강도 지침에 따라 보존." - id: A-11 severity: S2 detail: "'확인하기 위하여 수행하였다' — 1회. 보고서 격식 register의 정상 표현으로 판단, 보존." - id: lexical_diversity z=-3.48 severity: 정보 detail: "'송금·원장·체인·처리기' 반복은 기술 보고서의 용어 일관성 요구. 동의어 치환은 내용 앵커 훼손이므로 미조치." grade_reason: "A — S1(C-11) 잔존 0건, 자체검증 6항 전부 통과, 표·수치·헤딩 무손상. 변경률 0.6%는 A 기준 밴드(10~25%) 하한 미만이나, 이는 과소 윤문이 아니라 light 경로 입력(어휘형 AI 티 0건)에 보수 강도를 적용한 결과다." over_polish_aborted: false -->
10. 재시험 절차
같은 조건으로 다시 돌리는 방법이다. 회차는 요청 메모에 심는 상관키로 구분되므로, 몇 번을 돌려도 이전 회차와 섞이지 않는다.
10.1 사전 확인
시험 전에 세 가지를 본다. 하나라도 어긋나면 부하가 아니라 환경 탓으로 결과가 망가진다.
| 확인 항목 | 기준 | 어긋났을 때 |
|---|---|---|
| 체인 접속 | RPC 상태 조회가 정상 응답 | 엔드포인트 교체 후 재확인 |
| 시험 계정 잔액 | 계정당 최소 30만 KBKRW | 계정 추가 준비 또는 회차 단축 |
| 출금 처리기 | 로그가 실제로 흐르는 상태 | 처리기 재기동 |
세 번째가 특히 중요하다. 이 스택은 처리기가 멈춰도 컨테이너는 정상으로 보이고 상태 점검 응답도 정상을 돌려준다. 살아 있는지는 상태 점검이 아니라 로그가 흐르는지로 판단해야 한다.
10.2 회차 실행
bash run.sh 50 5m # 동시 사용자 50명, 5분
bash run.sh 100 5m # 동시 사용자 100명, 5분
부하 발생기를 앱 서버로 올리고, 서버 안에서 실행하고, 끝나면 원장에서 결과를 걷어 집계까지 한 번에 이어진다. 건당 금액을 바꾸려면 AMOUNT, 회차 이름을 지정하려면 RUN_ID를 앞에 붙인다.
10.3 실시간 대시보드
회차가 도는 동안 부하 발생기가 웹 대시보드를 연다. 서버 안에서 열리므로 밖에서 보려면 터널이 필요하다.
ssh -N -L 5665:127.0.0.1:5665 solana-app
브라우저에서 http://127.0.0.1:5665로 들어간다. 대시보드는 회차가 끝나면 사라지지만, 같은 내용의 정적 보고서가 결과 폴더에 남는다.
10.4 녹화
대시보드가 응답하기 시작한 뒤에 녹화를 붙인다. 회차보다 먼저 시작하면 빈 화면이, 늦게 시작하면 앞부분이 빠진다.
PROJECT=k6dash DASH_URL=http://127.0.0.1:5665 DSF=1 WATCH_MS=330000 npm run record
npm run compose
WATCH_MS는 회차 길이보다 넉넉히 잡는다. 짧으면 그래프가 차오르는 도중에 끊긴다.
10.5 결과 회수
영상과 원자료는 부하를 건 워크스테이션에 남는다. 앱 서버가 아니다. 녹화가 가상 화면에 브라우저를 띄워 워크스테이션에서 찍히기 때문이다.
bash fetch-videos.sh # 영상만
bash fetch-videos.sh --with-data # 영상과 회차 원자료
회차마다 파일 이름이 달라지므로 이름을 고정해 받지 않는다. 목록을 먼저 확인하고 받는다.
10.6 실패했을 때 볼 순서
완료 건수가 0이거나 대부분이 진행 중에 머물면 다음 순서로 좁힌다. 위에서부터 보는 편이 빠르다.
| 순서 | 확인 | 해당하면 |
|---|---|---|
| 1 | 출금 처리기가 정지했는가 | 정지 사유를 로그에서 확인, 재기동 |
| 2 | 체인 접속에 요청 제한이나 크레딧 오류가 있는가 | 엔드포인트 교체 |
| 3 | 시험 계정 잔액이 남아 있는가 | 계정 보충 |
| 4 | 상대 기관 검증이 걸렸는가 | 건당 금액을 트래블룰 임계 아래로 |
처리기 정지는 재기동으로 풀리지만, 정지 상태에서 전송까지 갔던 건이 실패로 확정되며 원장과 체인이 어긋날 수 있다. 재기동 전에 전송 기록이 있는 건을 먼저 세어 두면 나중에 정정 대상을 특정하기 쉽다.