목록으로

블록 확정 대기를 제거했더니 결제가 2배 빨라졌다

1

블록 확정 대기를 제거했더니 결제가 2배 빨라졌다

2025년 12월 15일 — txHash 전파 확정 대기 로직 추가 (최초 구현) 2025년 12월 19일 — tx 대기 개선 2026년 2월 10일 — 블록 확정 대기 완전 제거, 성능 최적화

처음 설계: "6블록 기다려야 안전하다"

블록체인 개발할 때 보통 이런 조언을 듣는다.

"트랜잭션이 블록에 포함된 후에도 추가 블록이 쌓일 때까지 기다려야 합니다. 블록이 재구성(reorg)되면 거래가 사라질 수 있으니까요."

→ 블록 재구성(Reorg)이란 블록체인에서 더 긴 체인이 발견되면 기존 블록이 버려지고 새 체인으로 교체되는 현상이다. 이미 확인됐다고 생각한 거래가 취소될 수 있어서 위험하다.

비트코인이면 6블록(약 1시간), 이더리움이면 12블록(약 3분). 맞는 말이다. 대부분의 블록체인에서는.

처음에는 우리도 이 원칙을 따랐다.

// 초기 설정
avalanche:
  confirmations: 1  # 1블록 확인 후 완료 처리
  confirmation:
    polling-interval: 1000  # 1초 간격
    timeout-seconds: 40

waitForConfirmation 메서드가 두 단계로 동작했다:

  1. 블록 포함 대기 (receipt 폴링)
  2. 추가 블록 대기 (confirmations 수만큼)

결제 한 건에 평균 3~5초, 느릴 때는 10초 넘게 걸렸다.

Avalanche의 특성을 뒤늦게 깨닫다

Avalanche C-Chain은 일반 블록체인과 근본적으로 다르다. Instant Finality(즉시 확정) 특성이 있다. → Finality(확정성)란 한번 처리된 거래가 절대로 되돌릴 수 없게 되는 시점을 말한다. Instant Finality는 블록에 포함되는 순간 바로 확정된다는 뜻이다.

  • 비트코인/이더리움: 가장 긴 체인을 따라가므로, 짧은 체인의 블록은 버려질 수 있다 (reorg)
  • Avalanche: 합의 프로토콜(Snowball) 자체가 확정성을 보장하므로, 블록에 들어가면 되돌릴 수 없다 → 합의 프로토콜이란 블록체인 네트워크의 참여자들이 "이 거래가 유효하다"고 동의하는 규칙이다. Snowball은 Avalanche만의 합의 방식으로, 여러 번 랜덤 투표를 거쳐 빠르게 합의에 도달한다.

즉, 블록에 포함되는 순간이 곧 최종 확정이다. 추가 블록을 기다리는 건 의미 없는 대기였다.

변경: waitForBlockInclusion만 사용

fun waitForBlockInclusion(txHash: String): TransactionReceipt {
    val attempts = (timeoutMs / pollingIntervalMs).toInt().coerceAtLeast(1)

    // → PollingTransactionReceiptProcessor는 일정 간격으로 블록체인에 "이 거래 처리됐나?" 반복해서 물어보는 도구다.
    val receiptProcessor = PollingTransactionReceiptProcessor(
        web3j, pollingIntervalMs, attempts
    )

    val receipt = try {
        receiptProcessor.waitForTransactionReceipt(txHash)
    } catch (e: Exception) {
        // 타임아웃 시 마지막으로 한 번 더 확인
        val finalReceipt = web3j.ethGetTransactionReceipt(txHash).send()
        if (finalReceipt.transactionReceipt.isPresent) {
            finalReceipt.transactionReceipt.get()
        } else {
            // 네트워크에는 있지만 블록에 아직 안 들어간 상태
            // → pending 성공 처리 (곧 확정될 것)
            val pendingReceipt = TransactionReceipt()
            pendingReceipt.transactionHash = txHash
            pendingReceipt.status = "0x1"
            pendingReceipt
        }
    }

    // Receipt 캐시 (나중에 블록 타임스탬프 조회 시 중복 RPC 방지)
    // → Receipt(영수증)는 블록체인 거래의 처리 결과를 담은 데이터로, 성공 여부·블록 번호·가스 사용량 등이 들어있다.
    if (receipt.blockNumber != null) {
        receiptCache[txHash] = receipt
    }

    return receipt
}

추가 블록 확정 대기 로직을 완전히 제거하고, 블록 포함 확인만으로 끝냈다.

타임아웃 처리가 까다로웠다

블록에 포함되기 전에 40초 타임아웃이 걸리면 어떡하냐는 문제가 있었다. 트랜잭션을 보냈는데 결과를 모르는 상태.

// PaymentManagerService의 executeFunction 내부
val receipt = try {
    transactionConfirmationService.waitForBlockInclusion(txHash)
} catch (e: Exception) {
    // 트랜잭션이 네트워크에 존재하는지 확인
    val txExists = web3j.ethGetTransactionByHash(txHash).send()
        .transaction.isPresent

    if (txExists) {
        // 존재하면 → 곧 처리될 것이므로 성공으로 간주
        return txHash
    } else {
        // 존재하지 않으면 → nonce 동기화 후 재시도
        continue
    }
}

"보냈는데 아직 처리 안 된 상태"와 "아예 보내지지 않은 상태"를 구분하는 게 핵심이었다.

성능 비교

항목BeforeAfter
평균 결제 시간3~5초1~2초
타임아웃 발생률간헐적거의 없음

배운 점

  • 블록체인 일반론을 맹목적으로 따르지 말고, 사용하는 체인의 특성을 파악해야 한다
  • Avalanche는 Instant Finality이므로 추가 블록 확정 대기가 불필요하다
  • 타임아웃 처리에서 "트랜잭션 존재 여부 확인"이라는 중간 단계가 데이터 정합성을 지켜준다
블록 확정 대기를 제거했더니 결제가 2배 빨라졌다 | KYUDORI