Spring boot, Redis Stream을 사용한 로그 시스템 구성 (작성중)

Spring boot, Redis Stream
김주혁's avatar
Apr 15, 2025
Spring boot, Redis Stream을 사용한 로그 시스템 구성 (작성중)
 
 
회사의 특정 대시보드를 위해 로그 적재 시스템으로 Redis Pub/Sub을 사용하고 있습니다. 이는 실시간 로그 처리가 가능하고 기존 Redis 인프라를 활용할 수 있다는 장점이 있었습니다. 하지만 운영 과정에서 다음과 같은 문제점들이 발견되었습니다.
 
  1. 로그 유실 문제
  1. 분산 환경에서의 중복 처리
  1. 로그 추적 한계
 
이로 인한 문제를 해결하고자 Redis Stream이 제안 됐습니다.
 

What is Redis Stream


 
Redis Stream은 Redis 5.0에 추가된 append-only 로그처럼 작동하는 데이터 구조이지만, 일반적인 append-only 로그의 한계를 극복하기 위한 여러 작업을 구현해주는 기능입니다.
append-only ?
💡
append only는 '추가만 가능한' 방식을 의미합니다.
  • 특징
    • 데이터 수정 불가
      • 기존 데이터를 변경하거나 삭제할 수 없습니다
    • 데이터 추가만 가능
      • 새로운 데이터는 항상 끝에만 추가됩니다
      • 순차적으로 데이터가 쌓이는 구조입니다
  • 장점
    • 데이터 일관성이 보장됩니다
    • 이전 상태를 그대로 보존할 수 있습니다
    • 시스템 장애 시 복구가 용이합니다
    • 동시성 처리가 단순해집니다
  • 단점
    • 데이터가 계속 누적되어 저장 공간을 많이 차지합니다
    • 삭제가 불가능하므로 공간 확보가 어렵습니다
    • 데이터가 증가할수록 조회 성능이 저하될 수 있습니다
    • 인덱싱이나 검색이 복잡해질 수 있습니다
    • 실수로 입력된 데이터를 수정할 수 없습니다
    • 데이터 수정이 필요한 비즈니스 로직 구현이 복잡해집니다
  • 사용 사례
    • 로그 파일 시스템
    • 트랜잭션 기록
    • 이벤트 저장소
    • 감사(audit) 기록
    • 예 : 은행 거래 내역
 
O(1) 시간의 임의 접근과 Consumer(소비자) 그룹과 같은 복잡한 consumption strategies(소비 전략)이 포함됩니다. 스트림을 사용하여 실시간으로 이벤트를 기록하고 동시에 배포할 수 있습니다.
  • Event Sourcing(이벤트 소싱: 사용자 행동, 클릭 등 추적)
  • Sensor monitoring(센서 모니터링: 현장의 장치에서 읽은 데이터)
  • Notifications (알림: 각 사용자의 알림 기록을 별도의 스트림에 저장)
 
Redis는 각 스트림 항목에 대해 고유한 ID를 생성합니다. 이러한 ID를 사용하여 나중에 관련 항목을 검색하거나 스트림의 모든 후속 항목을 읽고 처리할 수 있습니다. 이러한 ID는 시간과 관련이 있기 때문에, 여기에 표시된 ID는 다를 수 있으며 사용자의 Redis 인스턴스에서 보는 ID와 다를 것입니다.
 
Redis 스트림은 여러 가지 트리밍 전략(스트림이 무한정 증가하는 것을 방지하기 위해)과 둘 이상의 소비 전략(XREAD, XREADGROUP, XRANGE)을 지원합니다.
XREAD / XREADGROUP / XRANGE
 
  • XREAD
    • 기본적인 스트림 읽기 명령어
    • 하나 이상의 스트림에서 새로운 메시지를 읽을 수 있습니다
    • 블로킹/논블로킹 모드를 지원합니다
    • XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...] XREAD COUNT 2 STREAMS mystream 0 # mystream에서 처음부터 2개 메시지 읽기 XREAD BLOCK 5000 STREAMS mystream $ # 새 메시지를 5초간 대기하며 읽기
  • XREADGROUP
    • Consumer Group을 위한 읽기 명령어입니다
    • 여러 Consumer가 협력하여 메시지를 처리할 수 있습니다
    • 각 메시지는 한 번만 처리됩니다
    • XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...] XREADGROUP GROUP mygroup consumer1 STREAMS mystream > # 새 메시지 읽기 XREADGROUP GROUP mygroup consumer1 STREAMS mystream 0 # 미처리 메시지 읽기
  • XRANGE
    • 특정 ID 범위의 메시지를 읽는 명령어입니다
    • 시간 기반 조회가 가능합니다
    • 정방향 조회에 사용됩니다 (XREVRANGE는 역방향)
    • XRANGE key start end [COUNT count] XRANGE mystream - + # 모든 메시지 읽기 XRANGE mystream 1638512960000-0 + # 특정 시점 이후 메시지 XRANGE mystream - + COUNT 10 # 처음부터 10개 메시지
 

Redis Pub/Sub vs Redis Stream ?


 

1. Redis Pub/Sub

기존에도 Redis에는 Pub/Sub이 존재 했고, 일반적으로 Redis로 메세지를 사용한다고 하면 Pub/Sub을 떠올리실 수 있습니다. Redis Pub/Sub은 메세지 큐를 이용하여 Topic을 구독(Subscribe)하는 구독자(Subscriber)에게 메세지를 발행하면 모든 구독자가 메세지를 받는 단순한 구조입니다.
 
한편, Pub/Sub의 메세지는 영속성이 없고 구독자에 문제가 발생한 상태라면 메세지가 손실되는 문제가 존재했습니다. 또, 여러 구독자가 Scale Out 등의 사유로 여러개 일 때 같은 메세지를 중복 처리하는 문제도 발생하게 됩니다.
 
정리하면 이렇습니다.
  • 메세지에 영속성이 없습니다.
  • 구독자에 문제가 발생하면 메세지가 손실됩니다.
  • 여러 구독자가 같은 메세지를 수신하기 때문에 중복 처리 문제가 발생하게 됩니다.
  • 메세지 발행자(Publisher)가 구독자를 알 수 없습니다.
  • 메세지 필터링이나 그룹 소비 기능이 없습니다.
따라서 이벤트 소싱이나 로그 수집 등의 데이터의 영속성이 요구되는 측면에서 사용하기에 데이터 전달 흐름을 위한 파이프라인으로는 부적합하다고 생각됩니다.
notion image
 
이런 문제를 개선하기 위해, 여러 회사들은 Apache Kafka나 RabbitMQ, AWS Kinesis 등을 사용하고 있습니다. 처리량 측면에서는 Kafka가 압도적이고, 관리 복잡도 면에서는 완전 관리형 서비스인 Kinesis가 제일 편하고 확장성 측면에서도 Kafka나 Kinesis가 더 효율적입니다.
 
하지만, 인프라 리소스를 추가적으로 사용하기 어렵고 팀에서 이미 Redis를 많이 사용하고 있다면 Redis Stream도 좋은 선택지가 될 수 있습니다.
 

2. Redis Stream

 
Redis Stream은 위에서 설명했던 것 처럼 append-only 로그 파일처럼 작동하는 구조로 동작합니다. 기본적으로는 생성된 스트림에 메세지가 발행되고, 해당 Stream Id를 구독하는 소비 그룹(Consumer Group)이 Stream에 담긴 메세지를 소비하는 형태입니다.
notion image
여기서 중요한 점은 Pub/Sub과 달리, 영속성을 가지고 있다는 것 입니다. 이는 로그나 이벤트 소싱에서 소실되는 데이터로 인해 장애가 발생하는 것을 방지하여 Pub/Sub보다 더 안정적으로 Application을 운영할 수 있게 됩니다.
 
Redis Stream은 Pub/Sub과 달리, Kafka 같은 도구에서 얻을 수 있는 메세지 컨트롤 기능들을 제공합니다.
Kafka와 Redis Stream을 비교함으로 살펴보면,
notion image
 
  • Message 위치 추적
    • Kafka: 각 파티션별로 offset 번호를 통해 소비자의 메시지 처리 위치 추적
    • Redis Stream: 메시지 ID를 통해 소비자 그룹의 마지막 처리 위치 추적
  • Redis Stream Message Id VS Kafka Offset
    • 소비자가 다운되어도 마지막 처리 위치부터 재시작
  • Consumer Group
    • Kafka : 소비자 그룹이 파티션의 오프셋을 관리
    • Redis Stream : 소비자 그룹이 메시지 ID를 기반으로 처리 상태 관리
  • 메시지 순서 보장
    • Kafka: 파티션 내에서 오프셋 순서대로 메시지 처리
    • Redis Stream: 메시지 ID의 시간 순서대로 처리
  • 장애 복구
    • Kafka: 오프셋을 통해 장애 발생 시점부터 메시지 재처리
    • Redis Stream: 메시지 ID를 통해 미처리된 메시지 식별 및 재처리
 
위 비교를 통해 Redis Stream도 메제시 처리 위치를 추적 가능하며, 소비자가 실행 불능 상태가 됐을 때와 Consumer Group이 있다는 점 등 메세지 처리 프로세스를 제공하는 것을 알 수 있습니다.
 
무엇보다 시계열로 된 로그 데이터를 손실 없이 관리할 수 있다는 점에서 큰 장점이 있다고 생각됩니다.
 

Redis Stream 세부 특성


 

1. 시계열 기반 데이터 처리

  • Entry ID
    • Redis Stream은 스트림 Entry(항목)에 대해 고유한 ID를 생성합니다. 별도 설정이 ID를 지정해 주지 않으면, 스트림 내의 고유 ID인 Entry ID는 두 부분으로 구성됩니다.
      <millisecondsTime>-<sequenceNumber> // <밀리초시간>-<시퀀스번호>
밀리초 시간 부분은 스트림 ID를 생성하는 로컬 Redis 노드의 현재 시간입니다. 현재 밀리초 시간이 이전 엔트리 시간보다 작은 경우에는 이전 엔트리 시간이 대신 사용됩니다.
 
시퀀스 번호는 동일한 밀리초 내에 생성된 엔트리들을 구분하는 데 사용됩니다. 시퀀스 번호는 64비트 폭을 가지므로, 실제로 동일한 밀리초 내에 생성할 수 있는 엔트리 수에는 제한이 없습니다.
 
이런 Entry ID 형식이 이상하다고 느낄 순 있으나, Redis 스트림이 ID 기반의 범위 쿼리를 지원하기 때문에 생성 시간과 연관이 있어 시간 범위 쿼리를 추가적인 비용 없이 실행할 수 있는 장점을 가질 수 있습니다.
XRANGE mystream - + # 모든 메시지 읽기 XRANGE mystream 1638512960000-0 + # 특정 시점 이후 메시지 XRANGE mystream - + COUNT 10 # 처음부터 10개 메시지
  • XADD
    • 지정된 키의 스트림에 지정된 스트림 항목을 추가하는 커맨드입니다. 키가 없으면 이 명령을 실행하는 부작용으로 스트림 값으로 키가 생성됩니다. 스트림 키 생성은 옵션으로 비활성화할 수 있습니다.
       
      각 Entry는 key-value 쌍으로 구성 됩니다. 스트림을 읽는 명령은 XRANGE필드 XREAD와 값을 에서 추가한 순서대로 반환하도록 보장됩니다.
       
      XADD는 스트림에 데이터를 추가할 수 있는 유일한 Redis 명령어 입니다.
  • XREAD
    • 기본적인 스트림 읽기 명령어입니다. 시계열 기반으로 지정된 시작점부터 시간상 앞으로(최신 방향으로) 읽으며, 하나 이상의 데이터를 읽어오는 명령어입니다.
       
      즉, Entry ID 순서대로 데이터를 반환합니다.
 
위 Entyry ID와 XADD와 XREAD 특성등을 통해 Redis Stream은 시간 기반정렬된 데이터를 만약 동일 시간이라면 시퀀스 번호를 비교하여 처리 순서를 보장할 수 있습니다.
 

2. 영속성 보장

명시적으로 삭제하기 전까지는 데이터가 보존됩니다. append-only 구조이기 때문에 수정이 불가능해 일관된 데이터 흐름을 보여주나, 데이터를 삭제하지 않기 때문에 데이터 보존 전략과 메모리 관리 전략이 필요합니다.
 

3. 멱등성 보장

Redis Stream의 멱등성 보장방식은 다음의 기능을 통해 실행됩니다.
  • 컨슈머 그룹에서 메세지 소비 후 처리된 메세지에 표시를 할 수 있습니다.
    • XACK mystream mygroup <Entry-ID>
  • Pending Entries List (PEL)인, 미처리 메세지 확인 기능등을 사용하여 중복 처리를 예방할 수 있습니다.
  • 정상처리 : 메세지 읽기 → 비즈니스 로직 처리 → 처리 완료 표시
    • notion image
 
  • 장애상황 : 처리 실패 감지 → 재처리 결정 → 처리 이력 추적
 
 

구현


 
 

결론


 
단순하게 추가적인 도구 도입으로 인한 비용과 러닝커브 대신, 기존에 사용하던 도구를 더 잘 사용함으로서 얻을 수 있는 시간적, 비용적 장점이 있다고 생각합니다.
 

참고


Share article

vlogue