sayu.day

블록체인 인덱서 설계 딥다이브: ETL, DB 설계, Rust SDK, 멱등성

블록체인은 쓰기 무결성에는 강하지만 읽기 질의에는 불리한 구조입니다. "내 NFT 목록"처럼 단순한 질의도 체인 원본 데이터만으로 처리하면 전체 이력 스캔이 필요합니다. 이 문제를 해결하는 오프체인 읽기 최적화 계층이 인덱서이며, 이 글에서는 인덱서가 필요한 이유부터 ETL 아키텍처, DB 설계, Rust SDK, Diesel 배치 처리, 멱등성 설계까지 전 과정을 하나의 흐름으로 정리합니다.

인덱서가 필요한 이유

인덱서의 역할은 네 단계로 요약됩니다.

  1. 체인 트랜잭션/이벤트 스트림 수집
  2. 도메인 모델로 파싱
  3. 쿼리 친화적인 DB 스키마에 적재
  4. API에 밀리초 단위 읽기 제공

RPC 노드와의 차이

  • RPC 노드: 현재 상태 조회와 트랜잭션 전송을 담당합니다.
  • 인덱서: 이력 조회, 집계, 검색에 최적화된 읽기 계층을 제공합니다.

둘은 대체 관계가 아니라 상호 보완 관계이며, 대부분의 서비스는 둘 다 필요합니다. 최소 아키텍처는 Node Stream -> Parser -> DB -> API이고, 여기서 Parser 품질이 곧 서비스 데이터 품질입니다. 인덱서는 선택 기능이 아니라 프로덕션 데이터 제품의 기본 구성요소입니다.

ETL 파이프라인으로 본 아키텍처

인덱서의 내부 구조는 전형적인 ETL 파이프라인입니다.

  • Extract: 체인 노드 스트림에서 데이터 수집
  • Transform: 이벤트 디코딩과 도메인 매핑
  • Load: 배치 단위 DB 적재

Extract: 스트림 수집

대부분 gRPC 스트리밍을 사용합니다. 이 단계의 핵심 과제는 세 가지입니다.

  • 시작 버전 결정: 어디서부터 읽을지 명확히 정의
  • 재연결/백오프: 네트워크 장애 시 자동 복구
  • 백프레셔 관리: 다운스트림 처리 속도에 맞춘 수신 제어

운영에서는 마지막 처리 버전을 항상 추적해 재시작 시 정확히 이어받을 수 있어야 합니다.

Transform: 디코딩과 필터링

핵심은 "무엇을 버리고 무엇을 저장할지" 결정하는 것입니다.

  • 관심 이벤트만 필터링
  • 스키마 버전별 파서 분기
  • 파싱 실패 격리로 전체 파이프라인 중단 방지

파싱에 실패한 payload는 샘플을 보관해 두어야 원인 분석과 재처리가 가능합니다.

Load: 배치 적재

적재 성능은 배치 전략이 결정합니다.

  • 청크 분할
  • 트랜잭션 경계 관리
  • 이력/스냅샷 테이블 동시 갱신

단계별 지연과 실패율을 각각 계측하고, 문제 구간을 다시 돌릴 수 있는 복구 모드(range replay)를 처음부터 설계에 포함해야 합니다. 좋은 인덱서 아키텍처는 "빠른 처리"보다 "재처리 가능한 안정성"을 우선합니다.

DB 설계: 이력 테이블과 스냅샷 테이블

결론부터 말하면 둘 다 필요합니다.

  • 이력 테이블: 모든 변경을 누적 저장합니다. 분석, 감사, 시점 재구성에 필수입니다.
  • 스냅샷 테이블: 현재 상태만 유지합니다. 사용자 조회 성능에 필수입니다.

이중 테이블 패턴과 version guard

한 이벤트를 처리할 때 두 테이블을 함께 갱신합니다.

  1. 이력 테이블에 INSERT
  2. 스냅샷 테이블에 UPSERT

스냅샷 UPSERT에는 반드시 버전 조건(WHERE last_version < incoming_version)을 넣어 오래된 데이터가 최신 상태를 덮어쓰는 역전 업데이트를 막아야 합니다.

인덱스와 키 전략

  • 이력 테이블: 시간, 주체 주소, 버전 인덱스
  • 스냅샷 테이블: 현재 조회 키(소유자, 상태) 중심 인덱스

실무 원칙은 세 가지입니다.

  1. PK는 이벤트 식별자/버전 기반으로 고정합니다.
  2. 스냅샷 갱신 시 version guard 조건을 적용합니다.
  3. 대규모 집계는 이력 테이블에서, 사용자 조회는 스냅샷에서 분리 처리합니다.

인덱서 DB 설계의 목표는 정규화가 아니라 "질문 유형별 최적화"입니다.

Rust SDK: 오케스트레이터와 핸들러

인덱서를 여러 개 만들다 보면 gRPC 재연결, 버전 추적, 배치 저장 같은 인프라 코드가 반복됩니다. 이를 SDK로 공통화하면 팀 전체의 안정성이 올라갑니다. 핵심 구성은 네 가지입니다.

  1. 파이프라인 오케스트레이터
  2. 도메인 핸들러 Trait
  3. 채널 기반 step 연결
  4. 설정/헬스/복구 인프라

핸들러 인터페이스 설계

핸들러 trait의 시그니처는 process(context) -> Result<Option<context>> 형태로 단순하게 유지합니다.

  • Some: 다음 단계로 전달
  • None: 필터링 드롭

이 패턴이면 확장성과 흐름 제어가 단순해지고, 도메인 핸들러는 순수 로직에만 집중할 수 있습니다.

채널 기반 단계 연결

단계 사이를 채널로 연결하면 세 가지 이점이 있습니다.

  • 단계별 비동기 병렬화
  • 백프레셔 자동 전파
  • 장애 단계 격리

운영 관점에서는 시작 버전 결정 로직을 표준화하고, 재시작 시 정확히 이어받으며, 처리량/지연/실패율을 단계별로 계측해야 합니다. 좋은 SDK는 기능 추가보다 "실패했을 때 예측 가능한 동작"을 제공합니다.

Diesel로 구현하는 대량 쓰기

인덱서에서 ORM 선택 기준은 생산성보다 "대량 쓰기와 복구 시 안전성"입니다. Diesel은 컴파일 타임 타입 검증과 강한 Upsert 표현력이 장점입니다. 최적화 포인트는 네 가지입니다.

청크 분할

PostgreSQL에는 쿼리당 바인딩 파라미터 상한이 있어 대량 row를 한 번에 넣으면 실패할 수 있습니다. 컬럼 수를 기준으로 안전한 청크 크기를 계산해 분할 저장해야 합니다.

배치 INSERT

행 단위 저장 대신 벡터 배치 INSERT로 DB round-trip을 줄입니다.

조건부 UPSERT

ON CONFLICT DO UPDATE에 버전 비교 조건을 넣어 오래된 데이터가 최신 상태를 덮지 않게 합니다.

단일 트랜잭션 경계

이력 INSERT와 스냅샷 UPSERT를 같은 DB 트랜잭션으로 묶어 원자성을 보장합니다.

운영 팁도 함께 정리합니다.

  1. 커넥션 풀 크기는 DB 한계와 워크로드에 맞춰 조정합니다.
  2. 청크 실패 재시도는 멱등 보장이 전제될 때만 수행합니다.
  3. 느린 쿼리는 SQL 로그와 실행계획으로 지속 점검합니다.

멱등성: 다시 돌려도 같은 결과

인덱서는 재시작, 네트워크 장애, 수동 복구 과정에서 동일 트랜잭션을 반복 처리하게 됩니다. 멱등성이 없으면 중복 데이터, 잔액 왜곡, 상태 오염이 발생합니다.

실전 패턴

  1. 이력 테이블: 복합 PK + ON CONFLICT DO NOTHING
  2. 스냅샷 테이블: UPSERT + version guard
  3. 값 갱신은 상대 연산(+1)보다 절대 상태 반영을 우선

범위 재처리 API

start_version, end_version을 받는 범위 재처리 API를 별도로 두되, 정상 파이프라인과 같은 멱등 핸들러를 재사용해야 합니다. 복구 경로에만 존재하는 별도 로직은 또 다른 불일치의 원인이 됩니다.

버전 트래킹

  • 프로세서별 마지막 처리 버전 저장
  • 배치 단위 커밋 시점 명확화
  • 재시작 시 last_version + 1부터 재개

멱등성 검증 질문

  1. 같은 이벤트를 두 번 처리해도 결과가 같은가
  2. out-of-order 배치가 와도 최신 상태가 유지되는가
  3. 부분 실패 후 재시도해도 중복 부작용이 없는가

정리

  • 인덱서는 블록체인의 읽기 한계를 보완하는 오프체인 읽기 최적화 계층이며, RPC 노드와 상호 보완 관계입니다.
  • 내부 구조는 ETL 파이프라인이고, 설계 우선순위는 처리 속도보다 재처리 가능한 안정성입니다.
  • DB는 이력 테이블과 스냅샷 테이블의 이중 패턴으로 구성하고, 스냅샷 갱신에는 version guard를 적용합니다.
  • Rust SDK는 오케스트레이터, process -> Result<Option<...>> 핸들러, 채널 기반 연결로 구성해 실패 시 예측 가능한 동작을 보장합니다.
  • Diesel에서는 청크 분할, 배치 INSERT, 조건부 UPSERT, 단일 트랜잭션 경계가 핵심입니다.
  • 인덱서의 신뢰성은 처리 속도가 아니라 멱등성에서 시작합니다. 빠른 시스템보다 다시 돌려도 같은 결과가 나오는 시스템이 운영에서 이깁니다.