본문으로 건너뛰기
전체 글

병렬화의 효과는 어디에서 사라졌을까

· 약 9분
Bankware Global Engineering

서로 독립적인 거래를 함께 실행하고, 앞 거래를 기다리던 일을 준비되는 대로 배정하면 계산 시간과 불필요한 대기를 줄일 수 있다. 니고는 순차 실행과 같은 결과를 지키면서 이 기회를 이용하려고 여러 작업 배정 방식을 검토했다.

그런데 정확하게 동작하는 병렬 엔진을 만들었다고 해서 노드의 전체 처리 시간이 같은 비율로 줄지는 않았다. 2026년 8월 7일의 한 실험에서 거래 1,000건의 블록 실행 시간은 약 18% 줄었지만, 측정한 로컬 처리 경로 전체는 약 2.4%만 줄었다.

실행 구간에서 얻은 이득은 어디로 간 것일까? 당시 기록을 다시 따라가면 세 가지를 구분해야 한다. 무엇을 측정했는지, 실행 단계에 어떤 일이 남아 있었는지, 그리고 실행 방식들을 어떻게 비교했는지다. 이 글은 그 실험을 해석한 회고이며, 현재 니고의 처리량을 새로 측정한 결과는 아니다.

실행 앞에도 시간이 든다

기존 기록 하나를 소비하고 새 기록 하나를 만드는 Direct 거래 1,000건을 준비했다. 각 거래는 서로 다른 입력을 사용하므로 다른 거래의 결과를 기다리지 않는다. 서명까지 마친 거래들을 노드가 받아 블록 실행 결과를 만드는 과정을 측정했다. 동일한 준비 데이터를 반복 처리하는 실험이며, 운영 중인 원장에 실제 송금이 계속 쌓이는 부하는 아니다.

여기서 거래를 받는 일은 목록에 넣는 것으로 끝나지 않는다. 서명이 유효한지, 사용하려는 상태와 요청이 규칙에 맞는지 검사한 뒤 대기 거래 저장소인 TxPool에 넣는다. 이 수용·검증 단계를 admission이라고 부른다. 그 뒤 실행할 거래 묶음을 꺼내고, 블록 실행 요청을 구성하고, 실행을 마친 다음 대기 목록을 정리한다.

비교한 것은 이 중 블록 실행을 순차로 할 때와 작업자 네 개로 병렬 실행할 때였다. 앞단의 서명 검증은 두 경우 모두 작업자 네 개를 사용했다. 따라서 아래 표의 ‘순차’는 노드의 모든 작업을 한 줄로 실행했다는 뜻이 아니다.

단위는 1,000건을 한 번 처리하는 데 걸린 평균 시간인 ms다.

측정 구간순차 실행병렬 4개
거래 수용·검증37.86237.885
블록 실행6.3135.176
그 밖의 처리0.4810.511
전체44.65643.572

‘그 밖의 처리’는 전체에서 앞의 두 구간을 뺀 값이다. 거래 묶음 조회, 실행 요청 준비, 대기 목록 정리와 계측상 나머지를 포함한다. 모든 상태는 메모리에 두었다.

이 표의 ‘전체’는 거래 수용부터 실행과 대기 목록 정리까지다. 거래의 생성·서명, HTTP 요청 해석, 네트워크 전달, 합의, 상태 커밋먼트 계산과 DB 커밋은 포함하지 않는다. 사용자가 거래를 보내 확정받기까지의 지연이나 운영 TPS로 바꾸어 읽을 수 없는 이유다.

18%의 개선이 2.4%가 된 이유

순차 경로에서 블록 실행은 전체 44.656ms 중 6.313ms, 약 14%였다. 그 부분을 약 18% 줄이면 전체에서 줄어드는 몫은 대략 14%의 18%, 약 2.5%다. 실제 전체 감소율 2.4%와 비슷하다. 나머지 단계의 시간도 조금씩 달라지므로 정확히 일치하지는 않는다.

실행이 절약한 시간은 1.137ms였다. 전체에서도 1.084ms가 줄었다. 줄인 시간이 거의 그대로 전체에 나타났지만, 전체 시간에서 차지하는 비율이 작았다. 병렬화가 만든 이득이 다른 곳에서 모두 사라진 것은 아니었다.

범위를 더 분명히 보기 위해 극단적인 가정을 해보자. 다른 비용이 그대로인데 블록 실행 6.313ms를 아예 0으로 만들 수 있다고 해도, 전체에는 약 38.3ms가 남는다. 이 조건에서 실행 단계만 고쳐 줄일 수 있는 몫은 약 14%까지다. 실제로 실행 비용을 없앨 수 있다는 예측이 아니라, 최적화할 구간의 크기를 보는 계산이다.

당시 가장 큰 구간은 전체의 약 85%를 차지한 거래 수용·검증이었다. 그 안에는 서명에서 공개키를 복구하고 검증하는 계산을 비롯해 여러 검사가 들어 있다. 37.862ms 전부를 서명 연산의 시간이라고 부를 수는 없지만, 블록 실행 작업자만 늘려서는 앞단의 비용을 직접 줄이지 못한다는 점은 분명하다.

이미 검증한 일을 다시 하느냐가 실험을 바꾼다

여기서 또 하나의 경계가 중요해졌다. 블록을 실행할 때 거래의 서명을 다시 검증하는가?

자기 노드의 TxPool에서 꺼낸 거래라면 앞단에서 이미 서명을 검사했다. 니고는 그 검증 결과를 같은 프로세스 안에서만 유효한 증표로 전달해, 조건이 맞는 거래의 서명 계산을 반복하지 않는다. 이를 admission proof라고 부른다. 거래 내용과 검증 기준이 맞는지 확인하며, 상태에 따라 달라지는 검사까지 모두 생략하는 것은 아니다.

다른 노드에서 받은 블록에는 이 로컬 증표가 없다. 검증 노드는 자신의 기준으로 서명을 확인해야 한다. 증표는 다른 노드가 그대로 믿는 합의 자료가 아니며, 최초 검증 비용 자체를 없애는 장치도 아니다.

앞의 표는 거래를 처음 수용하며 서명을 검사하고 증표를 만드는 비용과, 그 증표를 재사용하는 블록 실행을 함께 잰 것이다. 반면 블록 실행만 따로 재는 실험에서는 증표를 미리 준비하고, 그 생성 비용을 측정 밖에 둘 수 있다. 두 측정은 서로 다른 질문에 답한다. 하나는 처음 받아 처리하는 비용이고, 다른 하나는 검증을 마친 뒤 블록을 실행하는 데 남은 비용이다.

서명을 다시 검증할 때는 거래 하나의 계산이 무겁다. 이를 여러 작업자에게 나눌 여지가 크다. 이미 검증한 결과를 재사용하면 같은 거래의 실행은 훨씬 가벼워진다. 그러면 의존성을 분석하고 작업을 배분하며 결과를 모으는 비용의 비중이 커진다. 중복 계산을 잘 없앴기 때문에, 그 뒤 단계에서는 병렬화의 이점이 작아질 수도 있는 것이다.

기다려야 하는 거래가 섞이면 판단이 다시 달라진다

앞의 1,000건은 모두 독립적이었다. 앞 거래가 만든 출력을 다음 거래가 소비하는 경우에는 그 결과가 준비될 때까지 기다려야 한다. 작업자를 더 둔다고 이 의존 관계 자체를 없앨 수는 없다.

별도 실험에서는 Direct 거래 1,000건 중 250건을 앞 거래의 출력을 이어서 소비하는 하나의 사슬로 만들고, 나머지 750건은 독립적으로 두었다. 여기서 ‘의존성 25%’는 이 구성의 뜻이다. 거래들이 무작위로 25% 확률로 충돌한다는 뜻은 아니다.

다음은 그 실험의 블록 실행만 비교한 평균 시간이며, 단위는 ms다. 앞의 로컬 전체 경로 실험과는 거래 구성과 측정 조건이 다르므로 표 사이의 절댓값을 직접 비교하지 않는다. 이 표의 병렬 경로는 함께 실행할 거래를 묶음 단위로 처리하는 WAVE이며, 작업자 네 개를 사용했다.

서명 처리순차 실행병렬 4개
다시 검증121.01664.351
검증 결과 재사용4.7635.759

두 행은 같은 의존 구조를 서로 다른 서명 검증 조건에서 측정한 결과다. 둘째 행은 사전 검증과 증표 생성 시간을 포함하지 않는다.

서명을 다시 검사하는 조건에서는 병렬 경로가 빨랐지만, 그 계산을 재사용하는 조건에서는 순차 경로가 빨랐다. 독립 거래의 실행이 빨라졌던 첫 실험과도 다른 결과다. 이 기록은 “Direct 거래는 병렬화하면 빠르다”는 한 문장으로 묶을 수 없다.

배정 방식을 바꾸면 기다림을 줄인 만큼 빨라질까

같은 의존성 실험에서는 준비된 거래를 바로 배정하는 READY_QUEUE와, 작업을 끝낸 작업자가 준비된 후속 거래를 이어 맡는 WORK_FIRST_CONTINUATION도 비교했다. 기다림이나 작업을 다시 넘기는 비용을 줄이려는 선택이었지만, 실제 이득은 거래당 남은 계산량에 따라 달랐다.

작업자 네 개·의존성 25%·서명 재검증 조건에서 평균 시간은 READY_QUEUE가 45.455ms, WORK_FIRST_CONTINUATION이 45.079ms였다. WAVE의 64.351ms보다 짧았다. 반면 서명 검증 결과를 재사용하면 두 대안 모두 같은 조건의 WAVE와 순차 실행보다 느렸다. 재사용 조건의 의존성 25%·50%, 작업자 두 개·네 개를 통틀어 세 병렬 방식 모두 순차 실행보다 느렸다.

서명 재검증·의존성 50%에서는 작업자 두 개와 네 개 각각의 비교에서 WORK_FIRST_CONTINUATION의 평균이 가장 짧았다. 하지만 같은 작업자 수에서 느린 실행을 가늠하는 p99는 READY_QUEUE가 더 짧았다. p99는 측정 표본의 99%가 그 시간 이내에 끝났다는 경계값이다. 평균을 줄인 방식이 오래 걸리는 실행까지 가장 잘 줄인 것은 아니었다.

이것이 모든 작업 배정 방식에 대해 영원히 순차 실행이 낫다는 증거는 아니다. 당시에는 거래 실행이 가벼워진 만큼, 작업 배정과 결과 수집 비용을 상쇄하기 어려웠다고 해석할 수 있다. 비교 대상은 Direct 거래로 구성한 부하였다. Native Program이나 혼합 블록에서 세 방식을 비교한 결과로 확대할 수 없다. 당시에는 모든 조건에서 이기는 방식이 없다고 판단해 하나의 대안을 일반 기본값으로 올리지 않았다.

계산을 나누는 데에도 계산이 든다

병렬 실행에는 거래 본문 외의 일이 있다. 의존 관계를 분석하고, 작업별 임시 상태를 준비하고, 실행 결과를 검증해 정해진 순서로 모아야 한다. 작업을 더 빨리 배정할 수 있어도 이런 비용까지 없어지는 것은 아니다.

당시 개선은 이 비용을 줄이는 데 집중했다. 거래마다 작업 하나를 제출하던 부분은 여러 거래를 제한된 수의 작업으로 묶었다. 결과는 나중에 정렬하는 대신 처음부터 정해진 거래 위치에 보관했다.

이 변경은 검증을 없애거나 거래 순서를 바꾼 것이 아니다. 같은 조건을 확인하면서 작업 제출, 중간 자료 구성과 결과 수집의 비용을 줄인 것이다. 첫 표는 이 조정 뒤의 측정이다. 다만 표 자체는 개선 전후 비교가 아니라, 조정한 구현에서 순차와 병렬을 비교한 것이므로 개별 변경의 효과까지 분해해 보여주지는 않는다.

따라서 작업자 수나 대기를 줄이는 아이디어만으로 실행 방식을 고르기 어렵다. 거래당 남은 계산량과 의존 관계를 맞춘 조건에서, 배정 비용을 포함한 결과를 비교해야 한다.

측정 순서를 바꾸자 우열이 뒤집혔다

이 결론에 도달하기 전에 측정 방법도 고쳐야 했다. 1,000건의 로컬 전체 경로를 순차 방식부터 고정 순서로 재면 병렬 방식이 약 3.6% 느렸다. 순서를 뒤집어 병렬 방식부터 재면 병렬 방식이 약 3.3% 빨랐다. 코드의 우열을 말하려던 숫자가 측정 순서에 따라 반대 방향을 가리킨 것이다.

당시 보고서는 긴 서명 검증 구간이 장비의 온도·주파수와 실행 순서의 영향을 받았다고 해석했다. 다만 보존된 기록만으로 온도나 주파수의 기여를 따로 확인할 수는 없다. 직접 확인한 사실은 고정된 측정 순서에 따라 전체 시간의 우열이 뒤집혔다는 점이다.

그래서 한 번의 측정 호출 안에 순차와 병렬 경로를 모두 넣고, 다음 호출에서는 실행 순서를 뒤집었다. 각 경로는 별도의 메모리 상태와 대기 목록, 실행 엔진을 사용했다. 이렇게 짝지어 순서를 교대한 측정이 첫 표의 근거다. 차이가 컸던 거래 수용·검증 시간은 두 경로에서 거의 같아졌고, 전체 시간은 병렬 경로가 약 2.4% 짧게 나왔다.

순서 교대가 모든 측정 오차를 없애지는 않는다. 그래도 같은 앞단을 쓰는 두 경로의 비교가 실험 순서에 크게 흔들린다면, 엔진을 고치기 전에 비교 방법부터 점검해야 한다는 근거는 충분했다.

이 기록으로 정할 수 있는 것

이 글의 두 표는 2026년 8월 7일에 보존한 로컬 실험 결과다.

측정 조건과 평균 계산

Java HotSpot 21.0.7과 JMH 1.37을 사용했고, 벤치마크를 호출하는 스레드는 하나였다. 이는 내부 서명 검증이나 블록 실행의 작업자 수와 별개다.

첫 실험은 별도 JVM 세 개에서 각각 1초씩 다섯 차례 준비 실행을 한 뒤, 1초씩 여덟 차례 측정했다. 표의 값은 총 24개 측정 구간에서 각 경로가 쓴 시간의 합을 실행 횟수의 합으로 나눈 평균이다. 순차와 병렬을 합친 측정 호출 전체의 점수를 어느 한 경로의 시간으로 쓰지 않았다. 두 번째 의존성 실험은 별도 JVM 세 개, 1초씩 두 차례 준비 실행과 네 차례 표본 측정을 사용했다. 둘째 표의 병렬 수치는 WAVE이며, 본문에 소개한 READY_QUEUE와 WORK_FIRST_CONTINUATION 결과도 이 의존성 실험에서 얻었다. 순차·병렬을 교대한 첫 실험과 달리 별도 실행으로 비교했다.

해당 두 결과 문서에는 CPU 모델·메모리·OS 정보가 직접 남아 있지 않다. 인접 날짜의 다른 실험 장비를 이 수치의 확정 조건으로 대신 적지는 않았다. 따라서 이 기록은 당시 비교에서 무엇을 배웠는지 설명하는 데 사용하며, 다른 환경의 성능을 예측하거나 현재 성능을 보장하는 기준으로 삼지 않는다.

실행 구간의 이득이 작게 보였던 이유는 측정 범위를 넓히면 알 수 있었다. 배정 방식을 바꾼 효과를 해석하려면 이미 제거한 서명 계산과 남은 배정 비용을 구분해야 했다. 그 판단조차 비교 순서가 바뀌면 흔들릴 수 있었다.

다음 최적화를 고르는 출발점도 여기에 있다. 가장 큰 구간을 찾고, 그 안에서 반복하는 일을 줄이고, 서로 기다릴 필요가 없는 계산을 나눈다. 그 뒤 같은 조건의 전체 경로에서 다시 확인한다. DB 커밋과 합의를 포함한 노드 성능은 그 범위까지 넓힌 별도 측정으로 답해야 한다. 병렬화의 효과를 판단하려면, 줄인 시간과 그 시간을 포함하는 전체를 함께 보아야 한다.