본문으로 건너뛰기
전체 글

송금 요청이 타임아웃되면 다시 보내도 될까

· 약 10분
Bankware Global Engineering

송금 버튼을 눌렀는데 한참 뒤에 “요청 시간이 초과되었습니다”가 나타났다. 받는 사람에게 토큰이 도착했는지는 보이지 않는다. 다시 누르면 될까?

응답을 기다리던 시간이 끝났다는 사실은 분명하다. 하지만 그동안 거래가 어디까지 진행됐는지는 아직 모른다. 요청이 서버에 도착하지 않았을 수도 있고, 거래가 블록체인에 제출됐거나 이미 실행됐을 수도 있다. 이 차이를 확인하지 않고 새 송금을 만들면 한 번의 의도가 두 번의 자산 이동으로 이어질 수 있다.

BXB처럼 업무 서버와 블록체인 사이를 연결하는 미들웨어에서는 이 공백을 다뤄야 한다. 토큰 10개를 보내는 사례를 따라 기존 거래를 다시 조회하는 일, 같은 서명 거래를 다시 전달하는 일, 새로운 거래를 만드는 일을 구분해 보자.

토큰 10개를 보냈는데 답을 받지 못했다​

이더리움과 호환되는 실행 환경인 EVM의 한 네트워크에서 A가 토큰 T 100개를, B가 0개를 갖고 있다고 하자. 업무 서버는 A에서 B로 10개를 보내려 한다. 한 번 성공하면 A 90개, B 10개가 된다.

이 글의 T는 소수 자릿수가 0이며 발행·소각·토큰 전송 수수료가 없다. 다른 동시 거래도 없고, 네트워크 수수료에 필요한 기본 코인은 A가 따로 갖고 있다. 실제 운영 거래를 재현한 기록이 아닌 독립적인 설명용 예제다.

업무 서버는 이 송금 의도에 요청 번호 R17을 붙인다. 이를 실행하는 첫 EVM 거래의 nonce는 42, 서명된 거래의 해시는 H1이라고 하자. R17과 H1은 설명용 이름이며, 실제 BXB API의 필드나 실제 거래 해시를 그대로 옮긴 값은 아니다.

이번에 일어난 일을 나중에 모두 확인했다면 다음과 같은 시간선일 수 있다.

업무 요청 R17: A → B, T 10개
↓
nonce 42의 거래에 서명 → H1
↓
노드로 제출
↓
업무 서버의 응답 대기 종료
↓
체인에서는 H1 실행 성공
↓
후속 조회로 결과 확인

중간의 타임아웃은 이 흐름을 되감지 않는다. 노드가 이미 받은 거래를 취소하지도 않는다. 위 시간선은 여러 가능한 경우 중 하나이며, 타임아웃 화면만으로 어느 단계에 있는지는 알 수 없다.

서명을 한 사람이 한 명인지, MPC로 여러 참여자가 공동 서명했는지도 이 문제를 없애 주지는 않는다. MPC 중계와 세션을 다룬 글에서 나눈 것처럼, 서명을 완성하는 일과 체인에서 실행 결과를 확인하는 일은 각각의 완료 조건이 있다. 여기서는 이미 서명을 만든 뒤의 거래 처리를 중심으로 살펴본다.

같은 송금에도 식별자가 여러 개 필요하다​

타임아웃 뒤 기존 작업을 찾으려면 무엇을 같은 것으로 볼지 먼저 정해야 한다. 고객의 송금 의도, 서명된 거래, 체인에서의 거래 순서는 서로 다른 대상을 가리킨다.

식별자예제에서 가리키는 것
업무 요청 번호 R17A가 B에게 T 10개를 한 번 보내려는 의도
거래 해시 H1특정 내용과 서명을 가진 하나의 EVM 거래
A의 nonce 42해당 네트워크에서 A의 거래 순서를 정하는 값

EVM의 거래 nonce는 주소에 연결된 순서 값이다. MPC 계산에서 사용하는 비밀 일회성 값과는 다른 공개 필드다. 노드와 체인은 이 값으로 거래의 순서를 검사하지만, 업무 서버가 붙인 R17의 의미를 저절로 알지는 못한다.

거래 해시도 필요하다. 예제와 같은 EIP-1559 거래에서는 서명된 거래 데이터를 통해 해시를 계산할 수 있으므로, 노드의 성공 응답을 받아야만 거래를 식별할 수 있는 것은 아니다. 같은 서명 바이트를 유지하면 같은 해시를 얻는다. 반대로 nonce나 수수료 조건 등을 바꿔 새로 서명하면 추적할 거래도 달라질 수 있다. EIP-1559의 거래 형식은 nonce·수수료·호출 데이터·서명이 어떻게 하나의 거래에 들어가는지 보여 준다.

따라서 업무 요청 번호와 거래 해시를 연결해 보관해야 한다. 이때 네트워크도 함께 식별해야 다른 체인이나 같은 주소의 다른 거래를 섞지 않는다. 나중에 수수료를 조정한 대체 거래를 만들었다면 R17에 연결된 후보 해시는 하나보다 많아질 수 있다.

이 관계를 복구에 활용하려면 외부 제출 전에 필요한 식별 정보와 진행 상태를 내구성 있게 남기는 설계가 필요하다. 서버가 재시작해도 “어떤 송금을 시도했는가”에서 조회를 다시 시작할 수 있어야 하기 때문이다. 이는 업무 연계에 필요한 설계 기준이며, 모든 BXB 경로에 같은 저장 순서가 구현돼 있다는 설명은 아니다.

다시 보낸다는 말에 들어 있는 세 가지 동작​

첫 번째는 재조회다. H1의 거래 정보와 실행 결과를 다시 읽는다. 서명을 새로 만들거나 송금을 추가하지 않는다. 타임아웃 뒤에는 기존 요청 기록과 이 조회 결과부터 연결한다.

두 번째는 같은 서명 거래의 재전파다. 보관한 거래의 바이트를 바꾸지 않고 노드에 다시 전달한다. nonce는 여전히 42이고 해시도 H1이다. 유효한 하나의 EVM 체인 이력에서 같은 발신자의 같은 nonce를 두 번 소비할 수 없으므로, 동일 거래를 전달했다는 이유로 T 10개가 두 번 이동하지는 않는다.

그렇다고 매번 성공 응답이 보장되는 것은 아니다. 노드는 이미 알고 있는 거래로 처리하거나, 이미 nonce가 사용됐다는 응답을 줄 수 있다. 그런 응답을 받았을 때에도 H1의 실제 결과를 확인해야 한다. 이미 서명한 거래를 재전파하는 데 새로운 지갑 서명이나 MPC 계산을 다시 수행할 필요는 없다.

세 번째는 새 거래 생성이다. “A에서 B로 T 10개”라는 API를 처음부터 다시 호출해서 새 nonce를 배정받고 새로 서명하는 경우다. 업무 내용이 같더라도 체인은 별개의 유효한 거래로 볼 수 있다.

가령 H1이 성공한 뒤 서버가 다음 nonce 43으로 같은 전송을 만들었다고 하자. 새 거래 H2까지 성공하면 잔액은 다음처럼 바뀐다.

실행된 거래A의 TB의 T
실행 전1000
H1 성공: 10개 이전9010
H2도 성공: 10개 추가 이전8020

토큰 합계는 여전히 100개다. 문제는 보존 법칙이 아니라, 한 번 보내려던 업무 의도가 두 번 실행됐다는 데 있다. 두 거래는 서로 다른 nonce를 올바르게 사용했으므로 nonce 검사만으로는 이 중복을 막을 수 없다.

같은 nonce 42를 유지하면서 수수료를 올려 새로 서명하는 대체 거래는 또 구분해야 한다. 원래 거래와 같은 순서 자리를 두고 경쟁하며 해시는 달라진다. Geth의 거래 풀 설명은 같은 주소·nonce에 여러 후보 거래가 있을 수 있음을 설명한다. 대체 요청을 보냈다는 사실만으로 원래 거래가 취소됐다고 판단할 수 없고, 어느 후보가 실행됐는지 추적해야 한다. 수수료 변경의 승인 범위와 대체 거래의 접수 조건도 별도로 적용해야 한다.

조회 결과가 없다는 것도 하나의 관찰이다​

EVM에서 receipt는 블록에 포함된 거래의 실행 결과를 담는 영수증이다. eth_getTransactionReceipt는 거래 해시로 이를 조회하며, 영수증을 찾지 못하면 null을 반환한다. 아직 블록에 포함되지 않은 거래에는 receipt가 없다.

따라서 null은 “이 노드의 조회에서 아직 영수증을 찾지 못했다”는 뜻으로 읽어야 한다. 거래가 제출되지 않았거나, 다른 노드만 알고 있거나, 처리 대기 중일 수 있다. 응답이 없다는 사실만으로 거래가 영원히 실행되지 않을 것이라고 판정할 수는 없다. 잔액이 아직 A 100개, B 0개로 보인다는 사실도 같은 이유로 충분한 취소 근거가 되지 못한다.

receipt를 얻었다면 실행 성공과 실패를 나눈다. 이 예제의 일반 EVM 거래에서 status가 0이면 실행이 실패해 토큰 이전은 되돌아간다. A 100개, B 0개는 유지되지만, 블록에 포함돼 실행을 시도했으므로 A의 기본 코인에서는 가스비가 들고 nonce도 소비된다. 거래가 아예 제출되지 않은 경우와 다른 결과다.

status가 1이면 거래 실행이 성공했다는 뜻이다. 그다음에는 업무에서 기대한 T 10개의 이동을 확인한다. 토큰 T의 이벤트에서 발신자 A·수신자 B·수량 10을 맞추고, 필요한 상태 조회와 연결한다. ERC-20 표준은 호출자가 false 반환도 처리하도록 요구한다. 함수가 실패를 반환해도 예외를 일으키지 않는 구현이 있으므로, 일반 receipt의 실행 성공을 모든 컨트랙트에서의 업무 성공과 동일시하면 안 된다. 함수의 반환값이 receipt에 그대로 담기는 것도 아니다.

마지막으로, 실행 결과를 발견한 시점과 업무에서 완료로 인정할 시점을 나눈다. 합의 과정에서 블록이 교체되는 reorg를 고려해 해당 네트워크의 확정 기준을 적용해야 한다. 이더리움의 PoS 확정성처럼 별도 합의 조건이 있는 경우도 있어, receipt 한 번 조회를 모든 EVM 네트워크의 최종 확정으로 일반화할 수는 없다.

BXB는 제출 기록과 결과 확인을 나누어 다룬다​

BXB의 일반 EVM 실행 경로에서는 거래를 서명해 노드에 제출하는 단계와, receipt를 기다려 실행 결과를 기록하는 단계가 나뉜다. 전송 기록에는 거래 해시·nonce 등 추적에 필요한 값을 연결하고, 후속 확인에서는 사용한 가스와 실행 결과를 보강한다.

이 구분은 컨트랙트를 REST API로 연결하는 글에서 설명한 거래 해시 응답의 의미와 이어진다. API가 돌려준 해시는 이후 조회의 출발점이며, 고객의 “10개 보내기”가 끝났다는 판정은 별도의 확인을 거친다.

확인한 EVM 구현에는 저장된 미확인 거래의 receipt를 주기적으로 다시 조회하는 수집기도 있다. 대상은 거래 해시를 가진 저장 기록 중 조회 조건에 맞는 항목이다. 설정된 기간과 처리량의 범위에서 결과를 보강하는 구조이므로, 타임아웃이 난 모든 요청을 기록 유무와 관계없이 자동 복구한다고 해석할 수는 없다. 이 수집 작업은 기존 결과를 읽으며 새 송금을 생성하지 않는다.

거래 로그가 채워지는 일과 업무 상태를 복구하는 일도 구별해야 한다. 예를 들어 체인에서 전송 성공을 찾았더라도 업무 서버의 지급 요청이 계속 “처리 중”이라면, 그 요청을 같은 거래 결과에 연결해 상태를 갱신해야 한다. 이때 해야 할 일은 결과 반영이며, 고객에게 토큰을 다시 보내는 작업을 시작하는 것이 아니다.

체인이 달라지면 결과를 기다리는 조건도 달라진다. BXB의 Solana 처리 경로에는 결과 대기 중 타임아웃을 곧바로 실행 실패로 기록하지 않고, 거래 식별자로 후속 상태를 조회하는 흐름이 있다. XRPL 제출 경로에는 결과가 불명확한 상태와 거래 해시를 남기는 처리가 있다. 공통으로 필요한 것은 “아직 모른다”는 상태를 보존하고 이후 확인을 이어 가는 일이다.

구체적인 재시도 조건은 체인의 규칙을 따른다. Solana의 일반적인 최근 blockhash 거래는 유효 기간과 마지막 유효 블록 높이를 함께 살펴야 한다. XRPL은 validated 원장의 결과와 LastLedgerSequence를 사용한다. 유효 기간이 지났더라도 그 전에 실행됐는지 확인해야 하므로, 만료만 보고 새 송금을 만들 수는 없다. 이 조건들을 EVM의 nonce와 같은 규칙으로 취급하지 않는 것이 멀티체인 결과 추적의 출발점이다. 멀티체인 API의 공통화와 체인별 차이에서는 EVM·Cardano·Solana·XRPL의 독립 전송 예제로 자산 식별, 비용과 수취 조건까지 비교한다.

업무 요청 번호가 중복 실행을 막으려면​

R17을 로그에 적었다는 사실만으로 중복 실행이 차단되지는 않는다. 추적용 correlation ID는 여러 로그를 한 요청에 연결하는 데 유용하지만, 같은 요청이 다시 들어왔을 때 새 거래 생성을 막는 검사를 대신하지 않는다.

같은 요청을 반복해도 추가 효과를 만들지 않도록 하는 성질을 멱등성(idempotency)이라고 한다. 이 송금 업무에 적용하려면 다음과 같은 연계 규칙이 필요하다. 아래는 예제의 설계 기준이며 BXB 전체 API에 공통으로 제공되는 계약을 나열한 것이 아니다.

  • 요청을 처음 받기 전에 업무 주체와 요청 번호의 범위를 정하고, 같은 번호로 수취인·토큰·수량을 바꿀 수 없게 한다.
  • 같은 요청이 동시에 들어와도 하나만 새 실행을 시작하도록 수락 기록의 고유성을 보장한다.
  • 기존 요청이 있으면 그 요청의 거래 후보와 확인 상태를 찾아 응답하거나 조회를 안내한다.
  • 서버가 중단돼도 기존 제출 시도를 추적할 수 있도록, 업무 기록과 체인 제출 사이의 복구 절차를 둔다.

BXB에서 확인한 구체적인 예는 XRPL 제출 경로의 중복 키 처리다. 네트워크·지갑·idempotencyKey 조합으로 기존 요청을 확인하고, 전송 전에 저장하는 기록의 고유성 제약도 사용한다. 같은 키가 다시 들어오면 새 전송을 시작하는 대신 충돌 응답과 저장된 거래 해시를 제공하는 방식이다.

이는 같은 요청을 재식별하는 장치다. 매번 새 키를 발급해 호출하면 같은 송금 의도였다는 연결이 사라진다. 또한 이 XRPL의 계약을 EVM을 포함한 모든 API의 보장으로 확대할 수는 없다. 중복 키 처리, 체인에서의 nonce·순서 검사, 실행 결과 조회는 각자 다른 단계의 중복과 불확실성을 다룬다.

타임아웃 뒤에는 어떤 상태로 끝나야 할까​

처음의 R17로 돌아가 보자. 이미 H1의 T 10개 전송이 성공했고 필요한 확정 조건도 충족했다면, 업무 상태를 완료로 연결한다. 최종 잔액은 A 90개, B 10개다. 응답을 놓쳤다는 이유로 추가 송금을 만들지 않는다.

아직 결과를 모른다면 확인 중이라는 상태를 유지한다. 거래가 계속 유효하고 재전파가 필요하다고 판단한 경우에는 기존 서명 데이터를 사용한다. 수수료를 바꿀 필요가 있다면 대체 거래로 별도 추적한다. 어느 경우든 새 nonce로 같은 업무를 다시 실행하는 것과 구분한다.

실행 실패가 필요한 확정 수준에서 확인됐다면 원인을 고친 뒤 새 시도를 검토할 수 있다. 확실히 미제출인 경우에도 기존 작업이 뒤늦게 제출되지 않도록 실행 주체를 정리한 뒤 진행해야 한다. 새 시도를 하더라도 원래 업무 요청과 이전 시도 기록의 관계를 유지해야 운영자가 전체 결과를 설명할 수 있다.

이 예제에서는 미제출로 끝나거나 실행이 실패하면 토큰 잔액이 A 100개, B 0개이고, 한 번 성공하면 A 90개, B 10개다. 블록에서 실행을 시도한 경우의 기본 코인 수수료는 따로 정산한다. 결과 미확인 상태에서는 이 잔액 중 하나를 최종 결과로 단정하지 않는다.

송금 시스템이 타임아웃 뒤에 이어 가야 할 것은 기존 송금 의도와 그 실행 결과의 연결이다. 그 연결을 유지해야 다시 조회할지, 같은 거래를 재전파할지, 새 시도를 허용할지 판단할 수 있다. 이렇게 확인한 결과를 업무에서 익숙한 DB 조회로 제공하려면 원장 이벤트와 저장 기록을 맞추는 과정도 필요하다. 블록체인 거래를 DB에서 조회하기까지에서는 이벤트 해석과 적재를 따라 체인 실행 완료와 조회 반영 시점의 차이를 살펴본다.