본문으로 건너뛰기
전체 글

블록체인 거래를 익숙한 DB에서 조회하기까지

· 약 9분
Bankware Global Engineering

토큰 전송이 성공했다. 블록체인 노드에서 거래 영수증도 확인했다. 그런데 업무 화면에서 조회하면 아직 전송 내역이 없다. 거래가 실패한 것일까, 화면이 늦은 것일까?

업무 화면이 블록체인을 직접 읽지 않고 별도의 DB를 조회한다면, 그 사이에는 데이터를 옮기고 해석하는 과정이 있다. 체인에서 실행을 마친 시점과 DB에 조회 가능한 행이 생기는 시점은 다르다. 이 간격을 이해해야 “내역이 안 보인다”는 이유로 이미 끝난 송금을 다시 실행하는 일을 피할 수 있다.

BXB의 블록 미러는 이 연결을 다루는 구성 요소다. 토큰 10개의 이동을 예로 들어, 체인의 로그가 익숙한 테이블로 바뀌는 과정과 그 테이블이 보장해야 할 범위를 살펴보자.

성공한 거래에서 조회할 사건을 고른다​

이더리움과 호환되는 EVM 네트워크 N에서 A는 토큰 T 100개, B는 0개를 갖고 있다. A가 B에게 T 10개를 보내는 거래 H가 블록 501에 포함돼 성공했다고 하자. 전송 뒤 잔액은 A 90개, B 10개다.

T의 소수 자릿수는 6이며, 발행·소각·토큰 전송 수수료와 다른 동시 거래는 없다. 네트워크 수수료용 기본 코인은 A가 별도로 갖고 있다. N·H·A·B는 설명용 이름이다. 이 예제는 실제 운영 거래를 재현한 기록이 아니다.

거래의 실행 결과를 담은 영수증을 receipt라고 한다. EVM의 eth_getTransactionReceipt는 거래 해시로 영수증을 찾고, 실행 상태와 블록 정보, 실행 중 남긴 로그 등을 돌려준다. receipt의 성공 상태만으로 어떤 자산이 얼마만큼 이동했는지까지 알 수는 없다. 그 내용은 해당 컨트랙트의 규칙에 맞춰 읽어야 한다.

표준 ERC-20 토큰은 전송이 일어나면 다음 형태의 이벤트를 남긴다.

event Transfer(
address indexed from,
address indexed to,
uint256 value
);

이벤트는 컨트랙트 실행 중 남기는 구조화된 기록이다. 예제에서는 T가 “A에서 B로 원시 수량 10000000이 이동했다”는 Transfer를 남긴다. 거래는 실행 단위이고, 이벤트는 그 실행 안에서 기록한 사건이다. 하나의 거래가 여러 컨트랙트를 호출하거나 여러 번 전송하면 로그도 여러 개가 될 수 있다.

따라서 수집기는 H를 찾은 다음, 그 안에서 토큰 T가 남긴 기대한 사건을 골라야 한다. 다른 컨트랙트의 이름이 같은 이벤트를 섞거나, 거래 하나를 언제나 전송 한 건으로 바꾸면 업무 내역이 달라질 수 있다.

로그를 읽으려면 주소와 숫자의 자리를 알아야 한다​

RPC로 받은 로그에는 from, to, value라는 친숙한 필드가 그대로 들어 있지 않다. 로그를 발생시킨 컨트랙트의 address, 검색에 쓰는 topics, 나머지 값을 담은 data로 나뉜다.

이때 ABI는 컨트랙트의 함수와 이벤트를 바이트로 표현하고 다시 해석하는 규칙이다. Solidity ABI의 이벤트 규칙에 따르면, 위와 같은 일반 이벤트의 첫 topic에는 이벤트 이름과 매개변수 타입으로 만든 서명 해시가 들어간다. 이 예제에서는 Transfer(address,address,uint256)의 해시다. indexed를 붙인 두 주소는 그다음 topics에, 붙이지 않은 수량은 data에 담긴다.

로그의 위치이 예제에서 읽을 내용
address이벤트를 발생시킨 토큰 T의 컨트랙트 주소
topics[0]이벤트 서명 해시
topics[1]보내는 주소 A
topics[2]받는 주소 B
data원시 수량 10000000

여기서 로그의 address는 받는 사람 B가 아니다. T 컨트랙트가 사건을 남겼으므로 T의 주소다. 자산 종류는 이 컨트랙트 주소와 네트워크를 함께 식별해야 한다. 화면에 표시하는 토큰 이름이 같다는 이유로 같은 자산으로 묶을 수는 없다.

주소 해석에도 한 단계가 있다. indexed address는 20바이트 주소가 32바이트 칸에 맞춰 채워진 형태로 들어간다. 조회용 주소로 저장하려면 ABI 타입에 따라 이를 해석하고, 비교에 사용할 주소 표현을 일관되게 정해야 한다. 앞에 0x를 붙이는 일만으로 이 변환이 끝나는 것은 아니다.

이벤트의 첫 topic만 같다고 모든 로그를 같은 자산 전송으로 읽어서도 안 된다. 지원할 컨트랙트와 이벤트 ABI를 정하고, topics의 개수와 data의 타입까지 맞춰야 한다. 이 글의 표는 그런 해석을 거친 값의 의미를 나타낸다. BXB 저장값을 그대로 복사한 실행 결과표는 아니다.

10000000을 저장하고 10개로 보여 주는 이유​

체인은 이 토큰의 수량을 정수 단위로 다룬다. 사람이 보는 소수점은 토큰의 표시 규칙을 적용한 결과다. ERC-20의 decimals가 6이면 표시 수량은 원시 수량을 10의 6제곱, 즉 1000000으로 나눈 값이다.

전송 전 원시 수량
A: 100000000 / B: 0

전송한 원시 수량: 10000000
↓
전송 후 원시 수량
A: 90000000 / B: 10000000

화면에 표시할 전송 수량
10000000 ÷ 1000000 = 10 T

DB에는 계산의 기준이 되는 원시 정수를 보존하고, 표시할 때 토큰별 소수 자릿수를 적용할 수 있다. 처음부터 부동소수점 숫자로 바꾸면 큰 정수나 작은 단위에서 정밀도를 잃을 수 있다. 원시 정수를 저장하는 DB 열 역시 지원할 값의 범위를 담을 만큼 충분한 정밀도를 가져야 한다.

BXB의 전송 로그 엔티티는 수량을 소수 자릿수가 0인 BigDecimal로 저장하도록 정의한다. 이벤트 처리기도 로그의 16진수 값을 정수로 읽으며 decimals로 나누지 않는다. 관리 API는 이 수량을 문자열로 직렬화하고, 관리 화면은 받은 수량을 보여 주는 경로로 구성돼 있다. 이 경로 자체에 토큰 메타데이터를 결합해 10 T로 바꾸는 처리가 포함됐다고 읽어서는 안 된다.

표시 기능을 붙인다면 네트워크와 컨트랙트 주소에 연결된 메타데이터가 필요하다. ERC-20에서 decimals는 선택 사항이므로 모든 토큰에 같은 값이 있다고 가정할 수도 없다. 메타데이터를 아직 확인하지 못했다면 원시 수량임을 드러내는 편이, 임의의 소수점을 찍는 것보다 의미가 정확하다.

BXB는 블록 조회와 이벤트 적재를 나눈다​

BXB 미러 소스에는 다음과 같은 역할 분리가 있다.

마지막 수집 위치와 현재 블록 높이 비교
↓
다음 블록의 거래 해시 목록 조회
↓
프로세스 안의 이벤트 버퍼에 전달
↓
거래별 receipt 조회
↓
지원 이벤트 해석과 저장 호출
↓
관리 API에서 DB 목록 조회

생산자 역할의 MirrorProducer는 설정된 노드에서 블록 높이를 확인하고 다음 블록을 읽는다. 그 블록의 거래 해시를 Disruptor라는 프로세스 내부 이벤트 버퍼로 전달한다. 소비자 역할의 MirrorConsumer는 해시로 receipt를 조회하고, 포함된 로그를 해당 이벤트 처리기에 넘긴다. TransferEventHandler에는 Transfer 로그에서 주소와 수량 등을 꺼내 저장소에 전달하는 경로가 있다.

원시 데이터를 한꺼번에 업무 화면으로 넘기지 않고, 수집·해석·조회 사이에 저장소를 두는 구성이다. 관리 백엔드는 block_mirror_tx_log를 네트워크로 필터링하고 블록 번호와 로그 순번의 내림차순으로 조회한다. 화면에서 목록과 상세를 읽을 때마다 지갑으로 서명하거나 새 거래를 보낼 필요가 없다.

이번 사건을 조회용 행으로 표현한다면 다음 정보가 중요하다. 로그 순번 7은 예시이며, receipt 안의 배열 번호가 아니라 해당 블록 안에서의 로그 순번이다.

기록할 정보설명용 값
네트워크N
거래 해시H
로그 순번7
블록 번호501
컨트랙트T의 주소
보내는 주소 / 받는 주소A / B
원시 수량10000000

실제 엔티티는 네트워크 ID·거래 해시·로그 순번을 복합 키로 삼는다. 한 거래에서 여러 사건이 발생해도 각각을 구별하고, 같은 사건을 다시 읽었는지 식별하는 기준이 된다. 다만 키가 있다는 사실과 재처리가 끝까지 안전하다는 보장은 다르다. DB 반영 방식과 수집 진행 위치까지 함께 봐야 한다.

여기까지는 소스에서 확인한 구성과 데이터 해석 기준이다. 주소 변환부터 DB 적재·조회까지를 실행 검증한 실습 절차를 제시하는 것은 아니다.

어디까지 읽었는가와 어디까지 저장했는가는 다르다​

블록 501에 거래 H1·H2·H3이 있다고 하자. 생산자는 세 해시를 버퍼로 넘겼고, 소비자는 H1과 H3을 처리했지만 H2를 아직 처리하지 못했다. 이 순간 “501까지 읽었다”와 “501까지 필요한 사건을 모두 저장했다”는 서로 다른 문장이다.

BXB의 생산자는 거래 해시들을 버퍼에 전달한 뒤 수집 위치의 블록 번호를 갱신한다. 그 갱신이 모든 소비자의 DB 저장 완료를 기다리는 구조는 아니다. 따라서 이 번호를 곧바로 업무 조회 데이터의 완성 지점으로 사용하면, 아직 처리 중인 사건까지 완료로 보게 된다. 프로세스 내부 버퍼에 전달했다는 사실도 내구성 있는 DB 기록과 같지 않다.

조회 결과의 완성도를 설명하려면 최소한 다음 세 시점을 구별해야 한다.

시점알 수 있는 것
체인 실행 결과를 관찰함해당 노드에서 거래의 실행 결과를 찾았다
수집 대상들을 전달함그 블록의 거래들을 처리 대상으로 넘겼다
필요한 DB 반영을 완료함그 범위의 조회 데이터를 저장했다

복구 가능한 적재를 설계한다면, 다시 읽을 시작 위치와 DB 반영 완료 지점을 따로 정의할 수 있다. H2 처리가 끝나기 전에는 501 전체의 완료를 표시하지 않고, 재시작하면 미완료 범위를 다시 읽는 식이다. 지원 대상 이벤트가 없는 거래도 “확인했으나 저장할 사건이 없었다”는 처리 완료 판단에는 포함된다. 저장한 행의 개수를 블록의 거래 수와 단순히 비교해서는 완료를 알 수 없다.

재처리 과정에서는 같은 사건을 여러 번 읽을 수 있다. 네트워크·해시·로그 순번은 중복을 구별하는 데 쓰되, 중복 입력에 대한 DB 동작과 완료 위치의 갱신을 함께 설계해야 한다. 예를 들어 행을 저장한 뒤 진행 위치를 남기기 전에 중단돼도 다음 수집이 일관된 결과로 수렴해야 한다. 이것은 업무 조회를 위한 설계 조건이며, 현재 미러에 누락 방지와 정확히 한 번 처리가 모두 구현됐다는 뜻은 아니다.

DB에 들어온 행도 체인 이력과 다시 맞춰야 한다​

블록 번호는 높이일 뿐 그 블록의 고유한 내용은 아니다. 체인이 재구성되는 reorg가 일어나면, 높이 501에서 읽었던 블록이 다른 블록으로 교체될 수 있다. 처음 관찰한 H의 로그가 선택된 체인 이력에서 빠지거나 다른 위치에 나타날 수도 있다.

이때 기존 행을 그대로 두면 업무 화면은 현재 체인과 다른 과거를 보여 준다. 같은 블록 높이만 다시 확인해서는 차이를 알 수 없고, 출처 블록의 해시와 이력의 연속성을 확인할 정보가 필요하다. 영향받은 행을 어떻게 무효화하거나 다시 계산할지, 어느 확정 수준부터 업무 완료로 볼지도 정해야 한다.

이더리움 JSON-RPC의 로그 필터 변경 응답은 재구성으로 제거된 로그를 removed로 알리는 형태를 제공한다. 이것은 체인 이력의 변화까지 따라야 한다는 한 사례다. BXB에서 확인한 경로는 거래별 receipt를 읽으며, 이 필터 인터페이스를 이용해 제거된 로그를 처리하는 경로와는 다르다.

미러의 전송 로그 엔티티에는 블록 번호가 있지만 출처 블록 해시나 제거 여부를 추적하는 필드는 없다. 현재의 복합 키만으로 체인 재구성까지 처리한다고 설명할 수 없는 이유다. 조회 DB를 운영 자료로 쓰려면 수집 재개뿐 아니라 선택한 체인 이력과 다시 맞추는 기준이 필요하다.

노드 연결도 같은 관점에서 봐야 한다. BXB에는 요청이 실패할 때 다른 설정 노드로 연결을 시도하는 경로가 있다. 접속 가능성을 높이는 데 도움이 되지만, 여러 노드로 접속할 수 있다는 사실만으로 모든 노드가 같은 높이와 같은 확정 상태를 보여 준다거나 적재 누락이 없다는 보장이 생기지는 않는다.

조회가 늦을 때 다시 해야 하는 일을 고른다​

처음 사례로 돌아가자. 체인에서 T 10개가 이동했고, 미러가 이벤트를 올바르게 해석해 행으로 반영하면 업무 화면은 그 행과 토큰의 표시 규칙을 이용해 “A에서 B로 10 T”를 보여 줄 수 있다. 체인에서는 실행과 사건 기록을, DB에서는 업무 조회에 맞춘 표현을 맡는 것이다.

화면에 행이 없다면 거래 실행 결과, 수집 진행, 이벤트 해석과 조회 조건을 나눠 확인해야 한다. 아직 수집하지 못한 사건을 다시 읽는 일과 토큰 전송을 다시 실행하는 일은 결과가 전혀 다르다. 송금 타임아웃과 재시도를 다룬 글에서 살펴본 것처럼, 이미 성공한 거래를 새 송금으로 반복하면 잔액은 A 80개, B 20개가 될 수 있다. DB 내역의 공백을 채우려다 자산을 한 번 더 옮겨서는 안 된다.

미러 DB는 조회를 쉽게 만드는 별도의 표현이다. 행 하나가 체인의 합의나 암호학적 증명을 대신하지는 않는다. 어느 네트워크의 어떤 사건에서 왔는지, 어디까지 반영됐는지, 이력이 바뀌면 어떻게 맞출지를 함께 다룰 때 업무 시스템은 그 표현을 근거 있는 조회 자료로 사용할 수 있다.