블록 확정 대기를 제거했더니 결제가 2배 빨라졌다
블록 확정 대기를 제거했더니 결제가 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 메서드가 두 단계로 동작했다:
- 블록 포함 대기 (receipt 폴링)
- 추가 블록 대기 (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
}
}
"보냈는데 아직 처리 안 된 상태"와 "아예 보내지지 않은 상태"를 구분하는 게 핵심이었다.
성능 비교
| 항목 | Before | After |
|---|---|---|
| 평균 결제 시간 | 3~5초 | 1~2초 |
| 타임아웃 발생률 | 간헐적 | 거의 없음 |
배운 점
- 블록체인 일반론을 맹목적으로 따르지 말고, 사용하는 체인의 특성을 파악해야 한다
- Avalanche는 Instant Finality이므로 추가 블록 확정 대기가 불필요하다
- 타임아웃 처리에서 "트랜잭션 존재 여부 확인"이라는 중간 단계가 데이터 정합성을 지켜준다