본문으로 건너뛰기
전체 글

RPC 노드를 바꾸면 실패한 거래도 해결될까

· 약 8분
Bankware Global Engineering

거래를 보냈는데 RPC 응답이 오지 않는다. 대신 사용할 노드가 하나 더 있다면, 그쪽으로 연결을 바꾸면 될까?

다른 노드는 다음 요청을 처리할 수 있다. 그러나 먼저 보낸 거래까지 없었던 일이 되지는 않는다. 연결 대상을 바꾸는 일과 거래의 실행 결과를 확인하는 일은 서로 다른 상태를 다룬다.

송금 요청의 타임아웃과 재시도에서는 같은 업무를 다시 실행할 때 생기는 문제를 살펴봤다. 이번에는 그 아래의 연결 계층으로 들어간다. BXB가 노드를 고르는 방식, 장애 후보를 다시 확인하는 방식, 미러가 다른 노드에 조회를 재시도하는 방식을 비교해 보자.

1. N1의 응답을 놓쳤을 때 N2에 물어볼 것​

같은 EVM 네트워크에 연결하는 RPC 노드 N1과 N2가 있다고 하자. RPC는 프로그램이 다른 시스템에 작업을 요청하고 응답을 받는 호출 방식이다. 블록체인에서는 거래 제출과 블록·잔액·거래 결과 조회에 사용한다.

토큰 T의 잔액은 A 100개, B 0개이고 목표는 A에서 B로 10개를 보내는 것이다. 소수 자릿수는 0, 토큰 전송 수수료는 없으며 A는 가스비용 기본 코인을 별도로 갖고 있다. 서명된 거래의 해시를 H라고 하고, 이 예제에서는 서명 데이터와 H를 후속 확인에 사용할 수 있게 보관했다고 가정한다. N1·N2·H는 설명용 이름이며 실제 장애 기록은 아니다.

N1로 거래 H를 보냈지만 통신 오류로 제출 응답을 받지 못했다. 이 시점에는 N1이 거래를 전달하기 전에 끊겼는지, 이미 네트워크에 전달한 뒤 응답만 사라졌는지 알 수 없다.

여기서 N2에 요청할 수 있는 중요한 작업은 같은 H의 결과 조회다. N2가 살아 있다는 이유로 A에서 B로 보내는 새 거래부터 만들지는 않는다. 뒤에서 H의 실행 성공과 토큰 이동을 확인한다면, 원래 의도한 송금 한 번으로 이 사례를 끝낼 수 있다.

이후 설명할 흐름은 다음과 같다. 도식의 “확인 중”은 설명용 상태이며 특정 API의 상태 이름이 아니다.

이것은 결과를 확인하는 절차의 예다. BXB의 모든 실패 요청이 자동으로 이 전체 절차를 수행하거나, 모든 미응답 거래의 해시를 저장한다는 뜻은 아니다.

2. 노드 풀은 다음 요청의 대상을 고른다​

BXB 어댑터는 네트워크별로 사용할 노드 클라이언트 목록을 관리한다. 이 목록을 노드 풀이라고 하자. 설정에서 사용하도록 지정된 노드들로 풀을 만들고, 요청을 처리할 코드가 클라이언트를 요구하면 하나를 선택한다.

모든 클라이언트의 상태가 정상이면 목록을 차례로 돌아가며 선택한다. 이 방식이 라운드 로빈이다. 일부가 비정상으로 표시돼 있으면 정상인 부분 목록에서만 차례로 선택한다. 만들어진 풀에서 정상 후보가 하나도 없으면 해당 선택 요청은 503 Service Unavailable로 끝난다.

풀의 현재 상태다음 선택
N1·N2 모두 정상두 후보를 순서대로 선택
N1 비정상, N2 정상N2 선택
N1·N2 모두 비정상사용 가능한 노드 없음으로 오류

이 표의 정상·비정상은 클라이언트가 기록한 상태다. 한 번 선택한 순간에 실제 노드가 반드시 응답하거나, 가장 최신 블록을 갖고 있다는 보장은 아니다.

예제의 EVM 전송 경로에서 통신 IOException이 발생하면 해당 클라이언트를 비정상으로 표시한다. 이후 다른 코드가 풀에 새로 요청할 때 N1을 제외하고 N2를 선택할 수 있다. 하지만 풀은 방금 실패한 호출의 내용을 받아 N2에서 대신 실행하는 장치가 아니다. 새로 대상을 고르는 시점과 이미 고른 대상에 수행 중인 호출을 구별해야 한다.

실제 BXB의 거래 결과 수집기도 네트워크별 처리 묶음마다 클라이언트 하나를 고르고, 그 묶음 안의 여러 거래를 같은 클라이언트로 조회하는 경로를 갖는다. 따라서 “다음 선택에서 다른 노드를 사용한다”를 “모든 RPC 호출이 즉시 다른 노드로 옮겨 간다”로 확대해서는 안 된다.

3. 정상이라는 표시는 무엇을 검사한 결과일까​

상태 검사는 어떤 질문을 했는지에 따라 의미가 달라진다. 주소로 연결할 수 있는지, 특정 RPC가 응답하는지, 기대한 네트워크인지, 원하는 블록까지 따라왔는지는 다른 질문이다.

확인한 BXB의 EVM 상태 검사에서는 eth_chainId를 호출하고 RPC 오류나 예외가 있는지 확인한다. 이 RPC의 반환값은 체인 식별자다. 다만 이 상태 검사 메서드는 반환된 값을 등록된 체인 식별자와 비교하거나, 최신 블록 높이·동기화 지연을 함께 검사하지 않는다. 이 검사에 통과했다는 사실은 그 범위 안에서 해석해야 한다.

다른 체인의 검사도 동일하지 않다. XRPL 경로는 RPC 객체를 처음 꺼내거나 이전 확인의 유효 시간이 지난 뒤 다시 꺼낼 때, server_info로 기대한 네트워크인지 확인한다. Cardano에는 최신 블록 조회, Solana에는 health RPC를 이용하는 검사 메서드가 있다. 메서드가 존재하는 것과 모든 일반 요청의 실패가 자동으로 노드 제외에 연결되는 것도 별개다.

공통 풀은 클라이언트를 생성할 때 모든 노드에 먼저 상태 검사를 수행하지 않는다. 또한 복귀 스케줄러가 주기적으로 검사하는 대상은 이미 비정상으로 표시된 클라이언트다. 정상 후보 전체를 계속 검사하는 감시 루프와는 범위가 다르다.

이 스케줄러의 기본 설정은 매분 실행이며 설정으로 변경하거나 끌 수 있다. 비정상 후보들의 검사를 병렬로 요청하고, 검사 결과가 정상으로 바뀌면 이후 선택 목록에 다시 포함된다. 이 기본 주기가 “어떤 장애든 1분 안에 탐지하고 복구한다”는 약속은 아니다. 장애를 상태에 반영하는 경로와 검사·응답에 걸리는 시간도 영향을 준다.

4. 노드 목록을 바꿔도 진행 중인 호출이 이동하지는 않는다​

운영 중 노드 설정을 바꿔 풀을 새로 만드는 경우도 있다. 새 목록을 등록했다고 이전 클라이언트를 쓰던 작업이 자동으로 새 클라이언트를 받는 것은 아니다. 이미 가져간 객체와 연결을 계속 사용하는 작업이 남아 있을 수 있다.

BXB는 풀을 교체할 때 이전 풀의 클라이언트를 바로 닫지 않고 30초 뒤에 닫도록 예약한다. 진행 중인 작업이 사용하는 연결을 즉시 끊지 않기 위한 유예다.

이것은 모든 작업의 종료를 확인한 뒤 닫는 절차와는 다르다. 30초보다 오래 걸리는 작업까지 완료를 보장하거나, 다른 노드로 이어받아 실행하는 구현으로 읽으면 안 된다. 노드 설정 변경에서도 다음 세 가지를 따로 봐야 한다.

새 요청이 어느 목록을 사용하는지, 기존 호출이 어느 연결을 계속 사용하는지, 그리고 이미 제출한 거래를 어떤 식별자로 추적하는지다. 연결 자원을 닫는 일은 체인에 전달된 H를 취소하는 일이 아니다.

5. 미러는 한 조회 호출 안에서 다른 노드를 시도한다​

BXB의 미러는 블록체인 데이터를 읽어 별도 DB에 반영하는 구성이다. 체인 이벤트를 DB로 옮기는 과정에서 살펴본 블록·거래 영수증 조회가 여기에 해당한다. 이 모듈의 노드 전환은 앞의 어댑터 풀과 구현 방식이 다르다. 두 모듈이 정상·비정상 상태를 하나의 목록으로 공유한다고 가정하지 않는다.

미러의 조회 클라이언트는 한 번 호출되면 설정된 시작 노드부터 시도한다. 지정된 종류의 HTTP 오류나 클라이언트 예외가 나면 다음 후보에 같은 조회 목적의 요청을 만든다. 노드마다 설정한 RPC 메서드를 사용할 수 있어, 전송 바이트가 언제나 완전히 같다는 뜻은 아니다. 한 호출에서 후보 목록을 최대 한 바퀴 살펴보고, 끝내 응답을 얻지 못하면 예외를 반환한다.

확인한 코드의 분기는 다음과 같다. 아래는 이 미러 구현의 정책이며 모든 RPC 서비스에 적용되는 규칙은 아니다.

관측한 실패이 미러 호출의 처리
HTTP 400·404다음 후보를 시도하지 않고 오류 반환
그 외 HTTP 4xx·5xx다음 후보 시도
RestClientException다음 후보 시도

429도 두 번째 행에 포함된다. 마지막 행은 연결 오류 등을 포괄하는 HTTP 클라이언트 예외 계열이다. 노드 수가 정해져 있어도 각 요청이 기다리는 시간은 별개이므로, 시도 횟수 제한을 전체 처리 시간 보장으로 볼 수는 없다.

예를 들어 같은 H의 영수증을 조회할 때 N1이 HTTP 503을 반환하고 N2가 정상 응답을 주면, 이 조회 호출은 N2에서 받은 응답을 반환할 수 있다. 여기서 바꾼 것은 이미 존재하는 결과를 읽을 경로다. 토큰을 보내는 새로운 거래를 만든 것이 아니다.

미러는 메모리에 다음 호출의 시작 인덱스도 관리한다. 다른 후보에서 성공하면 그 성공 후보의 다음 위치 쪽으로 시작점을 갱신하므로, 한 번 성공한 노드에 이후 모든 요청을 고정한다고 설명할 수 없다. 어댑터 풀처럼 지속적인 상태 검사와 제외·복귀를 수행하는 같은 장치도 아니다.

6. HTTP 응답과 원하는 조회 결과는 다를 수 있다​

다른 노드에서 HTTP 응답을 받았다고 곧바로 필요한 데이터를 얻은 것은 아니다. JSON-RPC는 응답 본문에 성공 결과인 result 또는 오류인 error를 담는다. JSON-RPC 명세는 두 필드의 역할을 구분한다. HTTP 전송이 끝났는지와 요청한 RPC가 성공했는지를 따로 확인해야 한다.

성공 응답에도 빈 결과가 있을 수 있다. H의 영수증을 아직 찾지 못했다면 응답의 핵심 부분은 다음과 같을 수 있다.

{
"jsonrpc": "2.0",
"id": 1,
"result": null
}

eth_getTransactionReceipt는 영수증을 찾지 못하면 null을 반환하며, 아직 블록에 포함되지 않은 거래에는 영수증이 없다. N2에서 null을 받았다는 사실은 H가 영원히 실행되지 않을 것이라는 증거가 아니다.

확인한 미러의 노드 순회 루프에는 JSON-RPC error나 result: null의 의미에 따라 다음 노드로 전환하는 판단이 없다. 응답을 받은 뒤 producer나 consumer 쪽에서 결과 부재를 검사하는 경로는 있지만, 그 오류를 앞의 HTTP 후보 순회와 같은 자동 재시도라고 설명할 수는 없다.

원장 시점도 남는다. 같은 네트워크의 두 노드가 응답하더라도 관측한 최신 블록이나 거래 전파 상태는 다를 수 있다. 같은 높이의 블록을 받았다는 사실만으로 내용까지 같다고 단정할 수도 없다. 다른 노드의 응답을 업무에 이어 붙이려면 네트워크, 거래 해시와 출처 블록, 필요한 확정 수준을 맞춰 봐야 한다.

현재의 노드 선택과 HTTP 순회가 이 모든 조건을 대신 검사하거나, 미러의 DB 반영 누락·중복·체인 재구성을 함께 해결하는 것은 아니다. 이 부분은 연결 가능성을 회복한 다음에도 남는 데이터 처리의 책임이다.

7. 연결은 바꾸고, 거래의 정체는 유지한다​

처음의 H로 돌아가 보자. 이 설명용 사례에서는 N1이 H를 네트워크에 전달했고, 응답만 유실됐다고 하자. 처음 N2의 조회에서는 영수증이 없었지만, 이후 조회에서 H의 실행 성공과 T 10개의 이동을 확인한다. 이 후속 조회는 별도의 확인 절차를 가정하며, 미러의 결과 부재 처리가 자동 재조회로 이어진다는 뜻은 아니다. 필요한 확정 기준까지 충족했다면 업무를 완료로 연결한다.

다른 거래가 없다면 최종 잔액은 A 90 / B 10이다. 가스비는 A의 기본 코인에서 별도로 지불한다. N2를 통해 한 일은 H의 결과를 확인한 것이므로, 노드를 바꿨다는 이유로 토큰 10개가 한 번 더 이동하지 않는다.

반대로 아직 H의 결과를 모른다면 확인 중인 상태가 남는다. 정상 노드가 없어 조회가 실패해도 그것은 확인할 연결이 없다는 오류이지, 체인에서 H의 실행 실패가 확정됐다는 뜻은 아니다.

노드 풀은 요청할 대상을 고르고, 미러의 후보 순회는 조회할 다른 경로를 찾는다. 업무 시스템은 그 경로가 바뀌어도 같은 거래와 같은 처리 목적을 추적해야 한다. 이 역할을 구별할 때 노드 전환을 거래 중복 없이 활용할 수 있다.