“Redis-Lua + RabbitMQ로 초대권 트랜잭션 병목을 끊다 — 178% 성능 개선 사례”
들어가며
다가오는 UMF 2025(EDM 페스티벌) 를 앞두고, 우리는 ‘초대권(Invitation)’ 기능을 새로 설계·개발했습니다. 행사 특성상 수천 명 사용자가 특정 시점에 동시 입장하는 초고부하 상황이 예측되었고, 실제로 3,000장 이상의 초대권이 발급되었습니다. 문제는 입장 시점에 요청이 한꺼번에 몰린다는 점이었습니다.
기존 아키텍처는 이 트래픽을 감당하지 못했습니다. 동시 60건을 넘기면서부터 서비스가 불안정해졌고, DB Connection Pool이 고갈되어 이후 요청이 처리되지 않는 상황이 반복되었습니다. MSA 구조였음에도 내부적으로 블록체인 기록 로직을 트랜잭션 안에서 동기적으로 실행하는 설계 때문에, 트랜잭션 경합과 DB 잠금이 누적된 것이 근본 원인이었습니다.
이를 해결하기 위해, 우리는 Redis 이벤트 기반 트랜잭션 제어(원자 연산 + 대기열/디스패치)와 (선택적으로) RabbitMQ를 활용한 후속 처리 비동기화를 도입했습니다. 핵심 아이디어는 단순합니다.
“선점/가용성 판단은 Redis의 원자 연산으로 초저지연 처리하고, 무거운 후속 로직은 트랜잭션 밖으로 분리한다.”
그 결과, 초당 약 70TPS 수준이던 처리량이 3,000TPS 이상 (약 42.9배) 증가했고, 안정적인 처리율을 달성했습니다.
이후 글에서는 Redis를 중심으로 트랜잭션 병목을 제거하고, 안정적인 초고부하 처리를 가능하게 만든 구조적 개선 과정을 상세히 다뤄보겠습니다
1. 블록체인 트랜잭션의 특성과 병목 원인
블록체인 시스템의 트랜잭션은 우리가 흔히 사용하는 데이터베이스(DB) 트랜잭션과는 다릅니다.
DB 트랜잭션은 하나의 커넥션 안에서 여러 작업을 묶어 원자적으로 처리하는 개념이지만, 블록체인의 트랜잭션은 “계정(Account)”을 통해 네트워크에 전송되는 서명된 실행 단위를 의미합니다.
여기서 중요한 차이점은, 하나의 계정(EOA, Externally Owned Account) 은 동시에 여러 트랜잭션을 보낼 수 없다는 점입니다.
그 이유는 블록체인 네트워크가 트랜잭션의 순서(Nonce) 를 기반으로 동작하기 때문입니다.
각 계정은 트랜잭션을 보낼 때마다 nonce 값을 1씩 증가시키며, 이 값이 겹치거나 순서가 어긋나면 네트워크는 해당 요청을 거부(revert) 하거나 대기(pending) 상태로 둡니다.

즉,
- 같은 계정에서 여러 트랜잭션을 동시에 보내면 → Nonce 충돌 발생
- Nonce 충돌이 나면 → 이전 트랜잭션이 처리될 때까지 네트워크가 대기
- 결국 서버 입장에서는 여러 요청이 동시에 들어와도, 실제 블록체인 네트워크에서는 순차(Sequential) 처리가 됩니다.

이더리움 메인넷은 한 트랜잭션이 블록에 포함되기까지 평균 약 8초, 빠른 체인에서도 1~2초 정도가 소요됩니다.
이 시간 동안은 동일한 계정에서 새로운 트랜잭션을 전송할 수 없는 상태가 됩니다.
문제는 이러한 블록체인의 직렬 처리 특성이 그대로 백엔드 서비스의 병목으로 전이된다는 점입니다.
특히 우리 시스템은 모든 발급 및 입장 트랜잭션을 단일 Pool 계정을 통해 처리하고 있었기 때문에,
블록체인 측에서는 트랜잭션 대기로 인한 지연이 발생하고, 서버 측에서는 그동안 DB 트랜잭션이 점유되어 Connection Pool이 고갈되는 연쇄적인 병목 현상이 발생했습니다.
2. 기존 아키텍처 구조 와 문제점

기존 아키텍처에서 관리자가 초대권 발행 요청을 70개 보냈다고 가정해보겠습니다.
서비스는 데이터베이스에 저장된 단 2개의 EOA(Ethereum 계정) 를 사용해 트랜잭션을 처리했습니다.
요청이 들어오면, 서버는 다음과 같은 순서로 동작했습니다.
- DB에서 사용 가능한 계정을 조회
- 비관적 락(SELECT ... FOR UPDATE) 을 걸어 계정을 선점
- 선점에 실패한 요청은 재시도하며 동일한 쿼리를 반복 실행
- 선점이 완료된 요청은 해당 계정으로 트랜잭션 오브젝트를 생성하고 이더리움 네트워크(EVM)에 전송하여 초대권을 발행
이 구조는 동시 요청이 적은 환경에서는 큰 문제가 없었습니다.
하지만 요청이 동시에 몰릴 경우, 심각한 성능 저하를 일으켰습니다.
비관적 락의 문제
SELECT ... FOR UPDATE 쿼리는 DB 자원을 가장 많이 사용하는 쿼리 중 하나입니다.
락을 걸어 다른 트랜잭션의 접근을 막기 때문에, 락이 해제되기 전까지는 동일한 행(row)에 접근하는 모든 요청이 대기 상태로 머무르게 됩니다. 위 예시에서 70개의 요청 중 2개만 계정을 선점하고, 나머지 68개 요청은 선점을 위해 반복적으로 재시도합니다.
이 과정에서 DB는 락 대기 상태를 유지하며 Connection Pool의 커넥션을 점점 소모하게 됩니다. 결국 일정 수 이상의 요청이 쌓이면 Connection Pool이 가득 차서 그 이후의 요청은 더 이상 처리되지 않는 병목 현상이 발생합니다.
체결 지연과 누적 부하
블록체인 트랜잭션은 평균적으로 약 8초(이더리움 기준) 가 소요되며, 이 시간 동안 해당 계정은 새로운 트랜잭션을 처리할 수 없다고 앞에서 설명한 바있습니다. 즉, 트랜잭션이 체결되기 전까지 락이 유지되며, 남은 68개의 요청은 계속해서 “계정 선점” 쿼리를 재시도하게 됩니다. 만약 요청이 초당 5회씩 재시도된다면, 68 * 5 = 340 회의 DB 쿼리가 매초 발생하게 됩니다.이는 단일 서비스 인스턴스 수준에서도 DB 부하를 폭발적으로 증가시키는 수준입니다.
단순한 확장으로는 해결되지 않는다
이 문제를 피하기 위한 단순한 방법 으로는 계정을 10개 .. 200개 그 이상으로 늘리는 것입니다. 하지만 이 방식은 요청이 적을 때 불필요한 리소스 낭비를 초래합니다. 또한 계정이 많아질수록 관리 비용이 증가하고, 보안·트랜잭션 관리의 복잡도도 함께 커집니다.
구조적 한계
결국 이 구조는 동시에 많은 사용자가 초대권을 발급받는 상황(예: 대규모 행사 입장) 에서는확실히 한계를 드러냈습니다.
- DB 락 경합
- 커넥션 풀 고갈
- 트랜잭션 지연 누적
이 세 가지 문제는 시스템 전반의 처리 속도를 떨어뜨리고, 서비스 중단으로 이어질 가능성이 높았습니다. 이러한 이유로, 이번 UMF 행사를 대비하여 우리는 기존 구조를 이벤트 기반의 트랜잭션 처리 구조로 개선하기로 결정했습니다.
3. 개선 방향 및 적용한 과정
1. DLQ + Back-Off 적용 - 사다리형 구조

기존 시스템에서는 요청이 실패하면 단순히 “재시도”만 반복했습니다. 하지만 트랜잭션이 블록체인 체결을 기다리는 동안(평균 8초 이상), 서버는 여전히 Connection Pool을 점유하고 있었죠. 결국 “일시적 실패”조차 시스템 전체의 병목으로 번지게 되었습니다.
이 문제를 해결하려면,
- 실패를 즉시 재시도하지 않고 지연시켜야 하고,
- 계속 실패하는 요청은 다른 큐로 분리해야 했습니다.
이 두 가지 조건을 동시에 만족시키는 구조가 바로 DLQ(Dead Letter Queue) 와 Back-Off 의 조합입니다.
DLQ(Dead Letter Queue)란?
DLQ는 메시지 큐 시스템에서 정상적으로 처리되지 못한 메시지를 별도로 보관하는 큐를 의미합니다. 즉,
- 컨슈머(Consumer)가 메시지를 처리하다가 예외가 발생하거나,
- 재시도 횟수를 초과해도 성공하지 못한 메시지들을
즉시 폐기하지 않고 DLQ에 보관함으로써,이후 운영자가 해당 메시지를 수동으로 확인하거나 재처리할 수 있도록 합니다.
이 방식은 단순히 실패를 무시하지 않고, “실패한 이벤트도 추적 가능한 데이터로 관리”할 수 있게 해주므로 대규모 비동기 처리 시스템의 안정성을 높이는 핵심 요소입니다.
Back-Off 재시도란?
Back-Off은 요청이 실패했을 때 일정한 지연 시간을 두고 재시도하는 전략입니다.
단순히 즉시 재시도하는 대신, 실패가 반복될수록 재시도 간격을 점차 늘려주는(Exponential Back-Off) 방식을 적용합니다.
| 시도횟수 | 대기 시간 |
| 1 회차 | 1초 |
| 2 회차 | 2초 |
| 3 회차 | 5초 |
이벤트 기반 재시도 구조 적용 및 결과
이러한 DLQ + Back-Off 방식을 실제 시스템에 적용하자, 요청 흐름이 명확히 구분되었습니다.
요청이 처음 들어왔을 때 계정을 성공적으로 선점한 요청은 정상적으로 초대권 발행이 진행됩니다. 반면, 계정을 선점하지 못한 나머지 68개의 요청은 이전처럼 무한히 반복 재시도를 하지 않습니다. 대신, 이 요청들은 다음 큐(Queue) 로 메시지를 전달하며,
각 메시지에는 랜덤한 Back-Off 지연 시간이 함께 설정됩니다. 이 지연 시간 동안 요청은 잠시 대기하고, 대기 후 다시 “계정 선점 실행 큐”로 메시지를 재발행하여 쿼리 부하를 분산시켰습니다. 즉, 모든 요청이 동시에 DB를 두드리는 대신, 시간 차를 두어 안정적인 재시도가 가능해졌습니다. 또한, 지정된 재시도 횟수를 초과한 메시지는 무한 루프에 빠지지 않도록 DLQ(Dead Letter Queue) 로 이동시켰습니다. 이 DLQ에 적재된 메시지는 추후 운영자가 실패한 이벤트를 트래킹하고 재처리할 수 있도록 설계했습니다.
여전히 남은 문제
물론 완벽한 해결책은 아니었습니다. 이 구조는 안정성을 크게 높였지만, 여전히 몇 가지 한계가 존재했습니다.
- Back-Off가 적용되면서 요청들이 일정 시간 대기 상태에 머물게 되었고, 서버 내부적으로는 대기 중인 인터벌(Interval) 처리 부담이 남았습니다.
- 트래픽이 매우 많을 경우, 랜덤한 Back-Off를 적용하더라도 대기 시간이 겹치는 구간이 발생해, 순간적인 동시성 스파이크가 완전히 사라지지는 않았습니다.
결과적으로, 이 개선을 통해 동시 70건의 요청을 약 120건까지 확장할 수 있었지만, 여전히 우리가 목표로 한 수천 TPS 수준의 처리량에는 미치지 못했습니다.
2. Redis + Lua 스크립트를 활용한 이벤트 기반 확장
DLQ와 Back-Off 구조를 통해 재시도 안정성은 확보했지만, 여전히 DB 락(SELECT ... FOR UPDATE) 이 전체 성능의 병목으로 남아 있었습니다. 트랜잭션이 블록체인에 체결되는 동안(평균 8초 이상) DB 커넥션이 점유되고 있었고, 이는 Connection Pool을 빠르게 소진시키며 TPS 확장을 가로막는 주요 원인이었습니다.
이 문제를 해결하기 위해, DLQ의 메시지 흐름 구조를 그대로 유지하면서 **Redis를 활용한 “사다리 타기형 구조”**를 구상했습니다.

핵심 아이디어는 단순합니다. DB의 계정 선점을 Redis와 동기화하고, 계정 가용 상태를 캐시 기반으로 제어하는 것입니다.
Redis를 활용한 캐시 기반 계정 선점
Redis를 사용하면 매번 DB에 접근하지 않고도 계정의 가용 여부를 즉시 확인할 수 있습니다. 예를 들어, 계정을 선점할 때는 Redis에 +1을 수행하고, 사용을 완료하면 -1을 수행하여 DB의 상태와 Redis의 캐시를 동기화할 수 있습니다.
이 방식의 장점은 명확합니다.
- 불필요한 DB 락 경쟁이 사라지고
- 계정 선점 및 해제가 메모리 기반 연산으로 전환

Lua 스크립트를 통한 원자적 제어
Redis의 Lua 스크립트를 활용하면, “가용 슬롯 증가 → 대기열 확인 → 대기 중 메시지를 깨워 실행 큐로 이동” 같은 일련의 과정을 단일 트랜잭션처럼 원자적으로 처리할 수 있습니다. 즉, 여러 요청이 동시에 동일한 키를 조작하더라도
레이스 컨디션(race condition)이 발생하지 않습니다. 이 구조는 단순한 캐시 제어를 넘어, Redis가 분산 환경에서의 락 관리자 역할을 수행하도록 합니다.
세마포어(Semaphore) 방식 적용
여기에 세마포어(Semaphore) 개념을 결합했습니다. 세마포어란 한정된 자원의 접근 허용 개수를 제어하는 동기화 기법으로,
자원 접근 시 카운트를 감소시키고, 해제 시 다시 증가시켜 동시에 접근 가능한 프로세스 수를 제어합니다.
Redis를 기반으로 세마포어를 구현하면, 여러 서버 인스턴스가 동시에 Redis를 공유하더라도
전역 단위의 동시성 제어(Distributed Semaphore) 가 가능합니다. 이를 통해 특정 계정 풀(pool)에 허용된 동시 요청 개수를 실시간으로 관리할 수 있으며, 모든 요청이 동일 시점에 몰리는 스파이크 현상도 자연스럽게 완화됩니다.
테스트 및 결과

테스트 시나리오
이번 성능 테스트는 Redis + Lua + 세마포어 기반 구조가 실제로 병목을 얼마나 줄이는지를 검증하기 위해 진행했습니다.
테스트는 아래와 같은 조건으로 구성되었습니다.
- 요청 수(Requests): 10, 20, 30, 50, 70, 100개의 동시 요청
- 계정 수(Accounts): 5개, 7개, 10개로 나누어 단계별 실험
- 측정 항목: 처리 시간(Time)과 TPS(Transaction Per Second)
즉, 계정 풀 크기를 점진적으로 늘려가며 동일한 요청 부하를 반복 적용하여
시스템의 응답 속도와 처리 효율(TPS)을 동시에 측정했습니다.
테스트 방법
- 기존 방식 (DB Lock 기반)
- 각 요청은 SELECT ... FOR UPDATE로 DB에서 직접 계정을 잠그며,
트랜잭션이 끝날 때까지 커넥션을 점유하는 구조였습니다.
- 각 요청은 SELECT ... FOR UPDATE로 DB에서 직접 계정을 잠그며,
- 개선 방식 (Redis + Lua + 세마포어 기반)
- 계정 선점과 해제를 Redis로 처리하고,
Lua 스크립트를 통해 원자적 증감 연산 및 큐 관리가 수행됩니다. - DB는 단순 상태 동기화 역할만 수행하며,
Redis가 실질적인 병렬 제어(세마포어 역할)를 담당합니다.
- 계정 선점과 해제를 Redis로 처리하고,
테스트 결과
| 시간(Time) | 최대 69.9% 단축, 평균 약 46.9% 단축 |
| TPS(Transaction Per Second) | 최대 170% 향상, 평균 약 107.9% 향상 |
요청 수가 많을수록(특히 70건 이상) 성능 향상이 두드러졌으며,Redis 기반 구조로 전환한 후 처리 안정성과 속도 모두 향상되었습니다. 특히 100개 동시 요청 시,
- 개선 전: 119.5초 / 0.82 TPS
- 개선 후: 45.5초 / 2.2 TPS
로, 약 170% 이상의 TPS 향상을 달성했습니다.
결론
Redis + Lua + 세마포어 구조를 적용한 결과, 단순 DLQ + Back-Off 단계에서 약 170% 추가적인 성능 향상을 확인했습니다.이제 시스템은 재시도 안정성뿐만 아니라, DB 부하 분산, 트랜잭션 병목 제거, 처리량 확장성까지 확보하게 되었습니다.
마무리
이번 UMF 2025 초대권 시스템은 단순한 성능 개선을 넘어, 구조적 병목을 제거하고 안정성을 확보한 실전 사례였습니다.
Redis-Lua와 RabbitMQ 기반의 비동기 트랜잭션 제어를 적용한 결과, 수천 건의 요청이 동시에 몰리는 상황에서도
시스템은 한 번도 중단되지 않았으며, 실제 행사 현장에서도 단 한 건의 오류 없이 안정적으로 초대권 발급이 완료되었습니다.
무엇보다, 이번 개선을 통해 단순히 수치를 올리는 것을 넘어 “대규모 실사용 환경에서도 견고하게 버틸 수 있는 구조”를 직접 설계하고 검증할 수 있었다는 점에서 큰 의미가 있었습니다. 행사 기간 동안 사용자가 직접 초대권을 발급받고 입장 과정을 원활히 진행하는 모습을 보며, 그동안의 수많은 테스트와 튜닝 과정이 결실을 맺은 것 같아 정말 뿌듯했습니다. 앞으로도 이번 경험을 바탕으로,
더 많은 트래픽을 안정적으로 처리하고, 블록체인 기반 서비스의 신뢰성과 확장성을 강화할 수 있는 구조적 개선을 지속해 나갈 예정입니다. 감사합니다.