[인프라/백엔드] Redis Streams의 재처리와 중복처리 문제

2026. 9. 21. 17:11·공부/인프라

0. 들어가며

프로젝트 당시 메시지 큐로 Redis Streams를 사용했던 적이 있습니다. 처음에는 Kafka와 RabbitMQ를 고려하고 있었지만 간단한 비동기 작업큐로 사용하기에는 너무 무겁다고 생각했었습니다. 다른게 있나 찾아보다가 Redis Stream를 알게되었는데 이미 Redis를 Refresh Token 저장을 위해 사용하고있기도 하고 이후 협업기능 추가할때도 Redis Pub/Sub을 사용할 계획이였기 때문에, 운영인프라를 더 추가안해도 되면서 적절한 솔루션이라 생각했습니다.

 

Redis Pub/Sub도 메시지 큐 인데 왜 Redis Streams인가

더보기

Redis Pub/Sub은 채널을 구독한 모든 구독자에게 메시지를 전달하는 시스템 입니다. 다만 메시지를 별도로 저장하거나 수신을 확인하지 않는 단순한 구조로 되어있습니다. 구독자가 연결되어 있지 않거나 메시지를 처리하지 못한 경우에도 해당 메시지를 다시 전달받기 어렵습니다. 이런 특성 때문에 일시적인 메시지 유실이 치명적이지 않은 실시간 알림이나 이벤트 전파 같은 용도에는 잘 맞습니다.

 

하지만 작업큐의 경우 상황이 다릅니다.

 

예를 들어 AI이미지 생성 요청이 왔는데 Worker가 일시적으로 장애가 발생했다고 가정해보겠습니다. 이때 해당 작업이 유실된다면 사용자는 생성 요청을 보냈지만 결과를 받지 못하게 됩니다. 

 

작업 큐는 단순히 메시지 전달 뿐 아니라 다음과 같은 기능도 필요했습니다.

 

  • 작업이 처리될 때까지 메시지를 유지할 수 있어야 한다.
  • 어떤 Consumer가 어떤 메시지를 처리 중인지 확인할 수 있어야 한다.
  • 작업 처리 성공 여부를 확인할 수 있어야 한다.
  • Consumer 장애 시 처리되지 않은 작업을 다시 처리할 수 있어야 한다.

Redis Streams는 메시지를 Stream에 저장하고, Consumer Group과 Pending Entries List를 통해 메시지의 처리 상태를 관리할 수 있습니다. 또한 XACK을 통해 Consumer가 작업 처리를 완료했음을 명시적으로 확인할 수 있고, 처리되지 않은 메시지는 다시 가져와 재처리할 수도 있습니다.

 

따라서 단순한 이벤트 전달이 목적이라면 Redis Pub/Sub도 충분하지만, 작업 유실을 방지하고 실패한 작업을 재처리해야 하는 비동기 작업 큐에는 Redis Streams가 더 적합하다고 판단했습니다.

 


1. Redis Streams란

https://dtstation.tistory.com/31#2.%20Redis%20Streams-1-3

 

[백엔드/인프라/CS] Redis와 Redis Streams

0. 주제 선정 배경프로젝트를 하면서 JWT 토큰 관리를 위해 Redis를 사용하긴 했지만, 인메모리 캐싱 용도라는 정도만 알고 있었습니다. 이번에 다른 프로젝트를 진행하면서 Redis와 Redis Streams를 함

dtstation.tistory.com

 

Redis Streams는 Redis에서 제공하는 append-only log 형태의 데이터 구조입니다. 메시지를 순서대로 저장하고, Consumer가 해당 메시지를 읽어 처리할 수 있습니다. 또한 Consumer Group을 사용하면 여러 Consumer가 메시지를 분산 처리할 수 있습니다.

 

Redis Streams의 기본 개념과 Kafka, RabbitMQ와의 차이는 이전 글에서 자세히 정리한 적이 있어 이번 글에서는 간단히만 다루겠습니다. 이번 글에서는 Redis Streams를 실제 작업 큐로 사용할 때 중요한 Consumer Group, 재처리, 전달 보장 방식, 중복 처리 문제​를 중심으로 살펴보겠습니다.


 

2. Consumer Group과 메시지 처리

Kafka에서는 하나의 Topic이 여러 Partition으로 나뉘어 있습니다. 같은 Partition 안의 메시지는 순서대로 저장되고 읽히기 때문에, Kafka는 Partition 단위로 순서를 유지하면서 병렬 처리할 수 있습니다(Partition 0 의 경우 A -> B -> C).

 

Redis Streams에도 Consumer Group이 있지만 Kafka와 메시지를 나누는 방식은 다릅니다. Redis Streams에는 Kafka의 Partition과 같은 구조가 기본적으로 존재하지 않습니다. 하나의 Stream에 메시지가 순서대로 저장되고, 같은 Consumer Group에 속한 여러 Consumer가 해당 메시지를 나눠 처리합니다.

 

Redis Streams도 여러 Stream을 나누어 Kafka의 Partition과 유사한 구조를 만들 수 있습니다. 하지만 Kafka처럼 하나의 Partition을 Consumer에게 독점적으로 할당해 순서를 보장하는 구조가 기본 제공되는 것은 아닙니다. 따라서 Stream 단위 순서 처리가 필요하다면 Consumer 수를 제한하거나 별도의 순서 보장 로직을 설계해야 합니다.


3. Redis Stream의 재처리

Consumer Group을 통해 여러 Consumer가(이하 Worker)가 메시지를 나눠 처리할 수 있다는 점을 확인했습니다. 그렇다면 Worker가 메시지를 가져간 뒤 작업을 완료하지 못하고 장애가 발생하면 해당 메시지는 어떻게 될까요? Redis Streams는 이런 상황을 처리하기 위해 Pending 상태와 ACK 기반의 재처리 구조를 제공합니다. 명령어와 함께 보도록 하겠습니다.

 

XREADGROUP과 PEL

Consumer Group에 속한 Consumer는 XREADGROUP 명령을 사용해 Stream의 메시지를 읽습니다. 예를 들어 generation_jobs Stream에서 generation-workers 그룹의 worker-1 Consumer가 새 메시지를 읽는다면 다음과 같이 사용할 수 있습니다.

XREADGROUP GROUP generation-workers worker-1 STREAMS generation_jobs >

 

 

 

  • generation-workers → Consumer Group
  • worker-1 → 메시지를 읽는 Consumer
  • generation_jobs → Stream
  • > → 해당 Consumer Group에 아직 전달되지 않은 새 메시지

 

추가적으로 Redis Streams는 Consumer에게 전달되었지만 아직 처리 완료가 확인되지 않은 메시지를 PEL(Pending Entries List) 에 기록합니다. 

Stream > XREADGROUP > Consumer(worker-1)가 Job A 메시지 수신 > PEL에 pending 상태로 등록

 

이후 Consumer가 실제 작업을 처리합니다.

Consumer(worker-1)가 Stream 메시지 JobA 메시지 수신 + PEL 등록 > JobA 비즈니스 로직 수행

 

XACK

작업이 정상적으로 완료되면 Consumer는 XACK 명령을 사용해 Redis에 처리 완료를 알립니다.

XACK generation_jobs generation-workers <message-id>

 

XACK이 수행되면 해당 메시지는 PEL에서 제거됩니다.

XREADGROUP > 메시지 수신 및 PEL 등록 > 작업 처리 > XACK > PEL에서 제거

 

따라서 재처리를 사용하려면 Consumer가 작업을 정상적으로 완료한 경우에만 XACK을 수행하도록 구현해야 합니다. 작업 중 예외가 발생한 경우에는 ACK하지 않고 메시지를 PEL에 남겨 이후 재처리할 수 있도록 합니다.

 

실제 Spring Data Redis 코드 예시 입니다.

try {
    jobExecutor.execute(jobId);

    // 성공한 경우에만 ACK
    redisTemplate.opsForStream().acknowledge(streamKey, group, message.getId());

} catch (Exception e) {
    log.error("Job failed", e);

    // ACK 하지 않음
    // → PEL에 남음
    // → 나중에 재처리 가능
}

 

코드 형태는 다르지만 의미는 Redis의 XACK 명령어와 같습니다. Redis 명령어로는 XACK <streamKey> <group> <messageId> 형태지만, Spring Data Redis에서는 이를 직접 문자열로 실행하지 않고 redisTemplate.opsForStream().acknowledge(streamKey, group, messageId) 메서드로 호출합니다.

 

Pending 메시지 다시 가져오기

ACK되지 않은 메시지는 PEL에 남아 있기 때문에 아래의 XPENDING 명령을 통해 Pending 상태의 메시지를 확인할 수 있습니다.

XPENDING generation_jobs generation-workers

 

XPENDING은 어떤 메시지가 Pending 상태인지, 어떤 Consumer가 해당 메시지를 처리 중인지 등의 정보를 확인할 때 사용할 수 있습니다. 기존 Consumer가 장애 등으로 인해 작업을 완료하지 못한 경우에는 XCLAIM 또는 XAUTOCLAIM을 사용해 일정 시간 이상 Pending 상태에 머문 메시지의 소유권을 다른 Consumer로 넘기고 다시 처리할 수 있습니다.

 

최종 재처리 흐름

XREADGROUP → PEL 등록 → 작업 처리 → 성공 시 XACK → 실패 시 PEL 유지 → XCLAIM / XAUTOCLAIM → 재처리

 

이처럼 ACK되지 않은 메시지를 다시 처리할 수 있기 때문에 메시지 유실 가능성을 줄일 수 있습니다. 하지만 같은 메시지가 두 번 이상 처리될 가능성도 생깁니다. 이를 이해하기 위해 Redis Streams의 전달 보장 방식을 살펴보겠습니다.


4. Redis Streams의 전달 보장 방식

메시징 시스템에서는 메시지를 Consumer에게 어느 정도까지 전달할 것인지에 따라 전달 보장 방식을 구분합니다.

대표적으로 At-Most-Once, At-Least-Once, Exactly-Once가 있습니다.

At-Most-Once

메시지를 최대 한 번만 전달하는 방식입니다.

메시지 전달
   ↓
처리 실패
   ↓
재전달하지 않음
 

중복 처리는 발생하지 않지만, 재전달을 하지 않기 때문에 Consumer가 메시지를 정상적으로 처리하지 못한 경우 메시지가 유실될 수 있습니다.

At-Least-Once

메시지가 최소 한 번 이상 처리될 수 있도록 하는 방식입니다.

Consumer가 메시지를 처리한 뒤 ACK을 보내지 못했다면 해당 메시지를 다시 처리할 수 있습니다.

메시지 전달
   ↓
ACK 확인
 ┌─┴─────────┐
ACK 성공    ACK 없음
   ↓           ↓
 처리 완료    재처리
 

메시지 유실 가능성을 줄일 수 있지만, 같은 메시지가 여러 번 처리될 가능성이 있습니다. Redis Streams에서 Consumer Group, PEL, XACK, 재처리를 함께 사용하는 일반적인 구조가 이러한 At-Least-Once 특성을 가집니다.

Exactly-Once

메시지를 정확히 한 번만 처리하는 방식입니다.

메시지 전달
   ↓
정확히 한 번 처리
 

가장 이상적으로 보이지만 실제 분산 시스템에서는 구현이 쉽지 않습니다. Redis가 메시지의 ACK 상태는 관리할 수 있어도 DB 저장, 외부 API 호출과 같은 비즈니스 로직의 성공 여부까지 하나의 원자적인 작업으로 묶어 처리하기는 어렵기 때문입니다.

 

예를 들어 다음과 같은 상황이 발생할 수 있습니다.

Job A 처리 성공
   ↓
DB 저장 완료
   ↓
XACK 전 Consumer 장애
   ↓
Redis에서는 아직 Pending
   ↓
Job A 재처리
   ↓
중복 처리 가능
 

실제 비즈니스 로직은 성공했지만 Redis는 ACK을 받지 못했기 때문에 같은 메시지를 다시 처리할 수 있습니다. 즉 Redis Streams의 재처리 구조를 사용하면 메시지 유실 가능성은 줄일 수 있지만, 그 대신 중복 처리 가능성을 고려해야 합니다.

 

전달 방식 특징 장점 단점
At-Most-Once 최대 한 번 전달 중복 처리 없음 메시지 유실 가능
At-Least-Once 최소 한 번 이상 전달 메시지 유실 가능성 감소 중복 처리 가능
Exactly-Once 정확히 한 번 처리 중복·유실 방지 구현이 복잡함

따라서 At-Least-Once 구조를 사용하는 경우에는 메시지가 여러 번 전달되더라도 같은 결과를 만들 수 있도록 Consumer를 설계할 필요가 있습니다. 다음으로는 이러한 중복처리를 막는 방법과 멱등성에 대해 설명하겠습니다.

 

6. 중복처리 문제와 멱등성

중복처리 문제의 대표적인 경우는 이전 예시처럼 비즈니스 로직은 정상적으로 완료되었지만 XACK을 보내기 전에 Consumer가 장애로 종료되는 경우입니다.

 

Redis 입장에서는 실제 비즈니스 로직이 성공했는지 알 수 없기 때문에 ACK되지 않은 메시지는 다시 처리 대상이 될 수 있습니다. 이 과정에서 같은 작업이 다시 실행되면 다음과 같은 문제가 발생할 수 있습니다.

DB 데이터 중복 저장
외부 API 중복 호출
알림 중복 발송
결제 중복 요청
파일 중복 생성
 

따라서 At-Least-Once 방식에서는 중복 메시지가 들어오더라도 결과가 한 번 처리된 것과 같도록 만드는 설계가 필요합니다. 동일한 요청이나 메시지가 여러 번 처리되더라도 최종 결과가 한 번 처리된 것과 동일하도록 만드는 성질을 멱등성(Idempotency) 이라고 합니다. 

Job ID를 이용한 중복 처리 방지

작업마다 고유한 jobId를 두고 해당 Job의 상태를 확인하면 이미 완료된 작업이 다시 실행되는 것을 방지할 수 있습니다.

Job 1001 수신
     ↓
Job 상태 확인
     ↓
이미 COMPLETED?
 ┌──────┴──────┐
YES             NO
 ↓               ↓
처리하지 않음    작업 수행
                 ↓
              결과 저장

 

동일한 메시지가 다시 전달되더라도 Job이 이미 COMPLETED 상태라면 실제 비즈니스 로직을 다시 실행하지 않는 방식입니다.

조건부 상태 변경을 통한 동시 실행 방지

다만 단순히 상태를 조회하는 것만으로는 모든 중복 실행을 막을 수 없습니다.

 

예를 들어 두 Worker가 거의 동시에 같은 Job을 처리하게 되면, 두 Worker 모두 Job이 아직 PENDING 상태라고 확인한 뒤 작업을 실행할 수 있습니다.

Worker A → Job 상태 확인 → PENDING
Worker B → Job 상태 확인 → PENDING

Worker A → 작업 실행
Worker B → 작업 실행

 

따라서 동시에 여러 Worker가 같은 Job을 실행하는 상황까지 방지하려면 상태 변경 자체도 원자적으로 처리할 필요가 있습니다. 예를 들어 Job의 상태를 PENDING → RUNNING으로 조건부 변경하고, 상태 변경에 성공한 Worker만 실제 작업을 수행하도록 만들 수 있습니다.

Job 1001 수신
     ↓
PENDING → RUNNING 상태 변경 시도
     ↓
변경 성공?
 ┌──────┴──────┐
YES             NO
 ↓               ↓
작업 수행      처리하지 않음

 

이를 통해 이미 완료된 Job의 재실행뿐 아니라 동일한 Job이 여러 Worker에 의해 동시에 실행되는 상황도 방지할 수 있습니다. 결국 At-Least-Once 방식에서는 중복 자체가 절대 발생하지 않도록 만드는 것보다, 중복이 발생하더라도 안전하게 처리할 수 있도록 Consumer와 비즈니스 로직을 멱등하게 설계하는 것이 중요합니다.

정리

  • Redis Streams는 Consumer Group, PEL, XACK을 통해 메시지 처리 상태를 관리할 수 있습니다.
  • ACK되지 않은 메시지는 PEL에 남기 때문에 XCLAIM, XAUTOCLAIM 등을 이용해 재처리할 수 있습니다.
  • 이러한 구조는 메시지 유실 가능성을 줄일 수 있지만, 동일한 메시지가 여러 번 처리될 수 있는 At-Least-Once 특성을 가집니다.
  • 따라서 작업 큐를 설계할 때는 중복 처리가 발생할 수 있다는 점을 고려해야 합니다.
  • Job ID와 상태값을 이용해 이미 완료된 작업의 재실행을 막을 수 있습니다.
  • PENDING → RUNNING과 같은 조건부 상태 변경을 이용하면 여러 Worker가 같은 작업을 동시에 실행하는 것도 방지할 수 있습니다.
  • 중요한 것은 중복 자체를 완전히 없애는 것이 아니라, 중복이 발생하더라도 안전하게 처리할 수 있도록 멱등성을 고려해 설계하는 것입니다.

이번 글에서는 Redis Streams의 재처리 구조와 중복 처리 문제, 멱등성에 대해 정리했습니다. 원래는 실제 진행했던 프로젝트에서 Redis Streams를 어떻게 사용했고, 왜 재처리를 사용하지 않았는지까지 함께 다루려고 했지만 글이 길어져 다음 글에서 별도로 정리해보려고 합니다.

 

 

📌 참고 자료
  • Redis Stream 적용기
  • Redis Streams
  • [Redis] Stream 사용방법
  • Redis Stream (레디스 스트림) 기본 정리
저작자표시 비영리 변경금지 (새창열림)

'공부 > 인프라' 카테고리의 다른 글

[인프라/DevOps] Jenkins CI/CD Pipeline을 만들어보자  (0) 2026.08.30
[인프라/CS] 무중단 배포(Rolling Update, Blue-Green, Canary)  (4) 2026.03.12
'공부/인프라' 카테고리의 다른 글
  • [인프라/DevOps] Jenkins CI/CD Pipeline을 만들어보자
  • [인프라/CS] 무중단 배포(Rolling Update, Blue-Green, Canary)
디티
디티
여행 기록, 공부 기록
  • 디티
    디티 휴게소
    디티
  • 전체
    오늘
    어제
    • 휴게소 (37)
      • 공부 (26)
        • 자료구조 (0)
        • 코딩테스트 (11)
        • 인프라 (3)
        • 백엔드 (7)
        • CS (5)
      • 여행 (8)
        • 제주도 (0)
        • 일본 (8)
      • 잡동사니 (1)
        • 블로그 관련 (1)
      • 프로젝트 (1)
        • 학기중 (1)
      • SSAFY (1)
        • 일상 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    일본 교토 여행
    JPA 장점 단점
    MyBatis와 JPA 차이
    일본여행
    교토 여행
    JDBC API 실행 흐름
    파이썬 dp
    JDBC 단점
    MyBatis 장점 단점
    Connection Pool 장점
    교토
    Github Webhook
    아라시야마
    일본
    redis streams
    스프링 부트
    python dp
    redis
    Redis Pub/Sub Redis Streams 차이
    JDBC 장점
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
디티
[인프라/백엔드] Redis Streams의 재처리와 중복처리 문제
상단으로

티스토리툴바