Redis vs RabbitMQ vs Kafka, 메시지 플랫폼 선택

Y_Sevin·2024년 4월 26일

시작은 "GPU 100%"

개발 중인 서비스인 HairBe는 Stable Diffusion이라는 모델을 사용하고있다.. 이 모델은… 솔직히 말해서 가볍지 않다.
요청 한 번 들어오면 GPU 사용률이 즉시 100%에 붙고, 그 상태에서 추가 요청이 들어오면 문제가 생긴다.

  • 이미 GPU가 풀로 돌고 있는데
  • 그 와중에 추가 요청이 들어오면
  • 기존 작업 처리 속도까지 같이 느려지는 현상이 발생했다

그래서 필자는 AI 서버를 단일 스레드(단일 작업) 기반으로 만들었다.
요청은 한 번에 하나만 처리하도록 Lock을 걸고, “대기열”을 AI 서버 내부에서 돌렸다.

여기까진 괜찮았다.


진짜 문제.. Spot 인스턴스..

문제는 비용이었다.
Stable Diffusion을 계속 돌리려면 GPU 인스턴스 비용이 꽤 부담된다. 그래서 AI 서버는 Spot 인스턴스로 운영하고 있었다.

Spot의 장점은 명확하다.

  • 저렴하다
  • 같은 성능을 훨씬 낮은 비용으로 쓸 수 있다

하지만 단점도 너무 명확하다.

  • 언제든지 중단될 수 있다
  • 중단되는 순간, AI 서버 내부 큐에 있던 요청 데이터가 같이 날아간다

실제로 이 상황이 발생했다.

  • AI 서버가 Spot 종료로 내려감
  • AI 서버 메모리(내부 큐)에 있던 대기 요청이 전부 증발
  • 사용자는 “요청 보냈는데 반응 없음” 상태가 됨

이때 깨달았다.

“대기열을 AI 서버 안에 두면, 서버가 죽는 순간 대기열도 같이 죽는다.”

그래서 구조를 바꿔야 했다.


고민

방법 1) 요청을 보내는 Spring 서버에 큐를 구현한다 ❌

처음엔 "그럼 큐를 Spring(API 서버)에 두면 되지 않나?" 싶었다.

근데 곧바로 걸리는 부분이 있었다.

  • 대기열을 Spring이 들고 있다가
    "이전 작업 끝났다" 콜백이 오면 다음 요청을 다시 AI 서버로 보내야 함
    왕복이 늘고 느려진다
  • 요청이 급격히 쌓이면 최악의 경우
    Spring이 큐 관리/재전송 때문에 병목이 될 수도 있다

방법 2) 타임아웃 기반 재시도 로직을 구현한다 ❌

두 번째는 "응답이 일정 시간 안 오면 유실로 판단하고 재시도" 방식이다.

이 방식도 구현 자체는 쉬운데, 운영 관점에서 찝찝했다.

  • 트래픽이 몰릴 때 재시도가 연쇄적으로 발생하면 부하가 더 커질 수 있다
  • “먼저 요청한 사용자”가 오히려 재시도 때문에 나중에 처리되는 역전이 생길 수도 있다
  • 결국 재시도는 데이터 유실을 추정하는 방식이라 확신이 없다

방법 3) 외부 시스템이 요청 데이터를 보관한다 ✅

결론은 이거였다.

  • 요청 데이터는 AI 서버도, Spring도 들고 있지 않는다
  • 외부가 요청을 안전하게 보관하고
  • AI 서버는 “꺼내서 처리만” 한다
  • Spot이 죽어도 대기열은 남는다

이때부터 Message Queue(대기열 시스템) 도입이 답으로 보였다.


메시지 플랫폼 비교

여기서 후보가 3개로 좁혀졌다.

  • Redis
  • RabbitMQ
  • Kafka

근데 이걸 고를 때 중요한 건 "누가 더 유명한가"가 아니었다.
HairBe 상황에서 중요한건 아래과 같았다.

  • Spot 종료에도 요청이 유실되지 않을 것
  • 단일 GPU 환경에서 작업이 순서대로 안정적으로 처리될 것
  • 재시도/실패 처리(Dead Letter 등)가 운영적으로 깔끔할 것
  • 비용/운영 난이도가 과하지 않을 것 (혼자 인프라 백엔드 다..해야했었기에..)

Redis vs RabbitMQ vs Kafka 비교(내 입장 기준)

Redis (Pub/Sub)

Redis는 빠르고 가볍다. 이미 캐시로 쓰고 있어 더 매력적이었다

하지만 HairBe 상황에선 Pub/Sub가 치명적이었다.

  • Pub/Sub는 기본적으로 메시지를 저장하지 않는다
  • Subscriber가 잠깐 죽거나 없으면 메시지는 그냥 사라질 수 있다
  • 즉 “유실되면 안 되는 작업 큐”로 쓰기엔 불안하다

Redis Pub/Sub는 "신호/알림"에는 좋지만,
Stable Diffusion 작업 요청처럼 반드시 처리돼야 하는 큐에는 아쉬웠다.

(참고로 Redis Streams로 가면 얘기가 달라지긴 하는데, 그 순간부터는 “간단히 붙이기” 장점이 희석되는 느낌이 있었다)


Kafka

Kafka는 강력하다. 이벤트를 디스크에 저장하고, 재처리...파티션증가를 통한 처리량 향상도 가능하다.
대규모 트래픽/확장성 관점에서는 가장 좋은 기술 같았다.

  • 하지만 지금 필요한 건 “대규모 이벤트 스트림”이 아니라 단순 작업 대기열
  • 단일 GPU라서 처리량이 Kafka급으로 올라갈 구조도 아님
  • 운영 비용/러닝커브/오버스펙이 부담이었다

Kafka는 "언젠가 이벤트 중심 아키텍처"로 갈 때 멋진 선택이다.
하지만 당시의 내 문제는 "Spot 종료로 인한 유실"이었고,
그걸 해결하기엔 Kafka가 너무 무거웠다... 사실 혼자 다 해낼수 있을까?에 대한 두려움도 좀 있었다


RabbitMQ

RabbitMQ는 딱 작업 큐에 최적화된 느낌이었다.

  • 메시지를 큐에 넣고(Producer)
  • 소비자가 하나씩 가져가 처리(Consumer)
  • 처리 성공 시 ACK → 큐에서 제거
  • 실패 시 재시도 / DLQ로 분리 같은 운영 패턴이 깔끔함
  • 라우팅도 유연해서 확장 가능

HairBe 요구사항에 잘 맞았던 건 특히 이 지점이다.

  • 메시지 유실 방지(내구성)
  • ACK 기반 처리
  • 단일 소비자/단일 작업 처리 모델과 궁합이 좋음

결론: RabbitMQ

정리하면 필자는 이렇게 결론냈다.

  • Redis Pub/Sub는 너무 휘발성이 강했다
  • Kafka는 너무 오버스펙 + 비용 + 운영 부담이 컸다
  • RabbitMQ는 지금 내 문제(유실 + 단일 작업 큐)를 가장 현실적으로 해결해줬다

그리고 HairBe 특성상 단일 GPU 서버였기 때문에,

  • 큐는 길어질 수 있어도
  • 소비는 어차피 한 번에 하나만 하는 게 맞고
  • RabbitMQ는 그 모델을 자연스럽게 지원했다

SQS도 대안이 될 수 있지만, 당시엔 조금이라도 빠르게 처리하고 싶었고
라우팅+운영+확장 관점에서 RabbitMQ가 더 손에 잡히는 선택이었다.


적용 후 구조

도입 이후 구조는 이런 느낌이 됐다.

  • Spring 서버는 "요청을 큐에 넣고 바로 반환"
  • AI 서버는 "큐에서 하나씩 꺼내 처리"
  • Spot이 죽어도 "큐는 살아있다"
  • 새 Spot 인스턴스가 떠서 다시 소비하면 된다

결국 핵심은 이거였다.

대기열을 서버 밖으로 꺼내는 순간, Spot 종료는 장애가 아니라 일시중단이 된다.


사실 해결 방식 자체는 단순했다.
근데 문제를 정확히 파악하는 과정이 오래 걸렸다.

  • 처음엔 GPU 느려지는 문제만 보였고
  • 그 다음에 Spot 종료가 겹치면서 유실이 터졌고
  • 그제야 "큐를 어디에 두느냐"가 본질이라는 걸 알았다

RabbitMQ 적용 이후에는 최소한 이게 사라졌다.

  • “요청이 증발하는 공포”
  • “응답이 없어서 사용자가 떠나는 상황”
profile
매일은 아니더라도 꾸준히 올리자는 마음으로 시작하는 개발블로그😎

0개의 댓글