파일 기반 입력 큐 병목을 Redis 기반 메모리 큐로 개선한 경험
본 글은 실제 운영 환경에서 발생한 문제를 바탕으로 작성되었으며, 보안 및 기밀 유지를 위해 일부 구조와 수치는 일반화했습니다.
여느 날처럼 이슈를 처리하던 중, 꽤 흥미로운 병목을 만났습니다.
“특정 모듈(이하 B 모듈)이 바라보는 입력 경로에 파일이 과도하게 쌓이고, 처리량이 급격히 떨어진다.”
처음에는 당연히 이런 의심이 들었습니다.
- 중간에 모듈이 죽었나? (근데 처리량은 나오고 있음)
- 특정 파일 때문에 처리가 지연되나? (근데 순차적으로는 처리됨)
- 그러면 왜 시간이 갈수록 점점 더 느려지지?
결론부터 말하면, 문제는 “로직” 이전에 입력 큐를 파일 시스템으로 구현한 구조 자체였습니다.
1) 시스템 구조: 파이프라인 + 파일 기반 전달
프로젝트는 여러 모듈이 파이프라인 형태로 연결된 구조입니다.
A 모듈 → B 모듈 → C 모듈 → ...
각 모듈은 이전 모듈의 결과를 입력으로 받아 처리한 뒤 다음 모듈로 전달합니다.
전달되는 데이터는 “원본 파일”이 아니라, 파일 메타데이터 + 모듈들이 누적 작성한 JSON 입니다.
{
"file": {
"fileId": "abc123",
"path": "/data/file.exe",
"sha256": "...",
"timestamp": "..."
},
"aModule": { "...": "..." },
"bModule": { "...": "..." }
}기존 전달 방식은 단순했습니다.
- A 모듈이 B 모듈 입력 디렉토리에 JSON 파일을 생성
- B 모듈은 입력 디렉토리를 스캔해 파일을 읽고 처리
- 처리 결과를 다음 모듈(C)의 입력 디렉토리에 다시 파일로 생성
2) 증상: 입력이 늘면 처리량이 “점점” 떨어짐

입력 파일이 많지 않을 때는 시간당 약 2만 건 수준으로 문제가 없었습니다.
하지만 특정 시간대(피크)에 B 모듈 입력 파일이 증가하면:
- B 모듈 처리 속도가 점점 느려지고
- 입력 경로의 적재량이 더 늘고
- 결국 전체 파이프라인 처리량이 B 모듈에 의해 끌려 내려갔습니다.
즉, 병목 현상이 발생하고있었습니다.
3) 원인: 파일 시스템을 큐로 사용했을 때의 구조적 한계
병목의 핵심은 이거였습니다.
파일 시스템을 “저장소”가 아니라 “대기열(큐)”로 사용하면서, 큐가 길어질수록 큐에서 작업을 꺼내는 비용이 증가했다.
입력 디렉토리에 파일이 많이 쌓일수록:
- 디렉토리 탐색 비용 증가
- 파일 open/read/close 반복 비용 증가
- 작은 파일이 많을수록 OS 파일 시스템 오버헤드 증가
즉, 쌓이면 쌓일수록 처리 효율이 감소하는 구조였습니다.
또 하나의 문제는, 중복된 input이 들어온 상황에서 캐싱 없이 항상 동일 로직을 다시 수행하는 구조였습니다.
(결국 B가 빨랐다면 파일은 덜 쌓였겠지만, 구조적으로 파일이 쌓이면 더 느려지는 악순환이 발생)
정리하면:
- 구조 문제(파일 큐)
- 캐싱 부재(중복 작업 반복)
이 둘이 함께 처리량을 갉아먹었습니다.
4) 그래도 파일 큐를 썼던 이유
이 구조가 왜 생겼는지도 나름 이해는 됐습니다.
- 러닝커브가 낮음 (OS 파일 시스템은 누구나 다룸)
- 외부 의존성이 없음 (Redis 같은 별도 인프라가 필요 없음)
- 장애 복구가 직관적임 (처리 중 죽으면 파일이 남아 재기동하면 이어서 처리 가능)
다만 “큐 길이가 길어질수록 비효율이 커지는 구조”라, 피크 트래픽이 들어오는 순간 병목이 터지는 건 피할 수 없었습니다.
5) 해결 방향: 서버 스펙 증설 대신 구조 개선
물론 “서버 자원을 더 준다”는 선택지도 있습니다.
하지만 온프레미스 환경이고, 모듈별 CPU 바인딩이 되어 있는 구조라서
CPU 재할당까지 건드리면 영향 범위가 너무 컸습니다.
그래서 이번 해결은 현실적으로 이렇게 정리했습니다.
- 입력 큐를 파일 → Redis로 변경
- 결과는 메모리 캐싱으로 중복 비용 절감
- Redis 장애/포화 시에는 파일 시스템을 fallback으로 사용
6) 개선 구조: 파일 큐 → Redis 큐 (+ fallback)
변경 전: A 모듈 → 파일 생성 → B 모듈이 파일 읽기
변경 후: A 모듈 → Redis에 push → B 모듈이 Redis에서 pop
여기서 중요한 건 “Redis를 쓰면 끝”이 아니라,
포화/장애 상황에서도 유실 없이 돌아가게 만드는 것이었습니다.
6-1) push 정책 (A → Redis)
- 기본은 Redis에 enqueue
- 큐가 임계치를 넘으면(예: 전체 사용량/길이 기준)
- Redis 대신 파일 시스템에 스풀(spool)로 적재 (fallback)
- 평시에는 Redis로 가니 성능 저하는 피크 트래픽에만 제한됨
6-2) pop 정책 (B ← Redis)
- B는 Redis에서 배치로 일정 개수씩 가져옴 (예: 100개)
- 가져온 배치를 while로 처리
- 처리 결과는 DB 저장 + 캐시 저장(중복 처리 방지)
- 캐시 TTL은 운영 상황을 고려해 제한 (예: 4시간)
7) 용량/한도는 어떻게 잡았나
디스크에 비해 메모리는 상대적으로 비싸고 한정적인 자원이기 때문에, Redis 큐의 최대 용량을 사전에 계산하여 제한을 두었습니다.
목표는 피크 상황에서도 최소 4시간 동안 데이터를 유실 없이 버틸 수 있도록 하는 것이었습니다.
운영 데이터를 기준으로 계산했습니다.
- 평균 메시지 크기: 약 1KB
- 시간당 처리량: 약 20,000건
- 목표 버퍼 시간: 4시간
따라서 최대 적재 가능 메시지 수는 다음과 같습니다.
20,000 × 4 = 80,000 건
총 메모리 사용량은: 80,000 × 1 KB ≈ 78 MB
Redis 내부 자료구조 오버헤드와 메모리 단편화를 고려하여 약 1.5배의 여유를 반영했습니다.
78 MB × 1.5 ≈ 117 MB
추가적인 트래픽 spike 상황까지 고려하여 최종적으로 약 150MB 수준으로 큐 최대 용량을 산정했습니다.
이를 통해 피크 트래픽 상황에서도 최소 4시간 동안 안정적으로 데이터를 유지할 수 있도록 했습니다.
캐시도 마찬가지로:
- 평균 캐시 엔트리 크기: 약 0.5KB
- 시간당 처리량: 약 20,000건
- TTL 4시간
위와같은 공식을 사용해 약 80MB 수준의 최대 용량을 산정했고,
총 150 + 80 = 약 230MB의 추가적인 메모리 사용량을 사용하는 것으로 산정했습니다.
결론: 메모리는 늘지만, 서버 여유 메모리 범위 내에서 충분히 감당 가능했고,
그 대신 병목을 제거해 전체 처리량이 안정화됨.
8) 결과: backlog 증가와 무관하게 처리 속도 안정화
구조 개선 후 가장 큰 변화는 쌓인 파일 증가 상황에서도 처리 속도가 안정적으로 유지된다는 점이었습니다.
기존 파일 기반 구조에서는 입력 디렉토리에 파일이 많이 쌓일수록 다음 작업을 가져오기 위해 더 많은 디렉토리 탐색과 파일 I/O가 발생했고, 그 결과 쌓인 파일이 증가할수록 처리 속도가 점점 감소하는 현상이 발생했습니다.
실제로 피크 시간대에는 쌓인 파일이 누적되면서 처리 속도가 평상시 대비 30-50% 수준으로 크게 감소하는 구간이 발생했습니다. (수-수십만개의 파일이 쌓여있었음.)
반면 Redis 기반 큐로 변경한 이후에는, 큐 길이와 관계없이 일정한 비용으로 작업을 가져올 수 있게 되었고, 트래픽이 증가하는 상황에서도 처리 속도가 안정적으로 유지되었습니다.
또한 결과 캐싱을 도입함으로써 중복 작업에 대한 재처리 비용을 줄일 수 있었고, 기존에 디렉토리 스캔에 사용되던 불필요한 CPU 사용이 감소하여 전체적인 자원 사용 효율도 개선되었습니다.
그 결과, 입력량이 증가하는 피크 상황에서도 전체 파이프라인의 처리량이 특정 모듈에 의해 제한되지 않고 안정적으로 유지될 수 있었습니다.
9) 장애 대비: Redis가 죽거나 불안정할 때
Redis를 쓰면 반드시 고려해야 할 건 두 가지였습니다.
9-1) Redis 영속화(AOF)
- Redis 장애/재기동 시 큐 데이터가 사라지지 않도록 AOF 활성화
9-2) Redis 장애 시 재처리 가능한 구조
- enqueue 실패 시 파일 스풀로 fallback
- B 모듈도 Redis pop이 실패하면 스풀 디렉토리에서 처리하도록 우회
- 결과적으로 “Redis가 있어도 좋고, 없어도 시스템은 유실 없이 돌아가는” 형태
10) 결론

마치 과거에 엔딩을 보고 한동안 빠져들었던 팩토리오(Factorio)를 다시 하는 기분이었습니다.
처음에는 단순히 생산량이 부족한 줄 알았지만, 병목은 언제나 “가장 느린 구간”에서 발생했고, 그 구간을 찾아 구조를 바꾸는 순간 전체 생산량이 눈에 띄게 회복되었습니다.
이번 개선 역시 비슷했습니다. 단순히 처리 로직을 최적화하거나 서버 자원을 늘리는 방식이 아니라, 데이터가 흐르는 방식 자체를 다시 설계하는 것이 핵심이었습니다.
기존에는 파일 시스템을 큐로 사용하면서 입력이 증가할수록 디렉토리 탐색 비용과 파일 I/O 오버헤드가 함께 증가했고, 이는 자연스럽게 처리 효율 저하로 이어졌습니다.
반면 Redis 기반 메모리 큐로 전환한 이후에는, 큐의 길이와 관계없이 일정한 비용으로 작업을 가져올 수 있게 되었고, 그 결과 입력량이 증가하는 상황에서도 안정적인 처리 속도를 유지할 수 있게 되었습니다.
또한, 단순히 Redis를 도입하는 것에서 그치지 않고, 큐 용량을 처리량 기반으로 산정하고 캐시 TTL과 메모리 사용량을 사전에 계산했으며, Redis 장애 시 fallback 구조까지 고려하여 실제 운영 환경에서도 안정적으로 동작할 수 있도록 설계했습니다.
이를 통해 병목을 제거하는 동시에, 시스템이 예측 가능한 범위 내에서 안정적으로 동작하도록 만들 수 있었습니다.
이번 경험을 통해 다시 한 번 느낀 점은, 성능 문제의 원인은 단순히 “코드가 느려서”가 아니라, 구조가 특정 상황에서 비효율적으로 동작하도록 설계되어 있기 때문인 경우가 많다는 것이었습니다. 그리고 그 병목을 해결하는 가장 효과적인 방법은, 개별 로직을 최적화하는 것이 아니라 데이터 흐름과 구조를 올바른 형태로 재설계하는 것이라는 점이었습니다.
생산 라인을 하나씩 정리해 병목을 제거했을 때 전체 생산량이 회복되던 것처럼, 이번 개선 역시 파이프라인의 흐름을 정리함으로써 시스템 전체의 처리 안정성을 확보할 수 있었던 경험이었습니다.