Redis 리스트(LPUSH, RPOP)를 메시지 큐로 사용하는 방법

Redis 리스트를 강력한 메시지 큐 시스템으로 변환하는 방법을 알아보세요. 이 튜토리얼에서는 필수적인 LPUSH 및 RPOP 명령어를 다루며, 작업을 큐에 추가하고 워커가 안정적으로 큐에서 제거하여 처리하는 방법을 설명합니다. 실용적인 Python 예제를 살펴보고, Redis를 사용하여 비동기 작업 처리를 위한 견고한 FIFO 기반 메시지 큐를 구축하기 위한 핵심 고려 사항을 알아보세요.

Redis 리스트(LPUSH, RPOP)를 메시지 큐로 사용하는 방법

Redis 리스트는 RabbitMQ, Kafka 또는 전체 백그라운드 작업 프레임워크보다 가벼운 것이 필요할 때 유용한 소규모 메시지 큐를 만들 수 있습니다. 일반적인 패턴은 간단합니다. 생산자는 LPUSH로 작업을 추가하고, 워커는 RPOP 또는 BRPOP로 작업을 가져옵니다.

이러한 단순함이 사람들이 이 방법을 사용하는 이유입니다. 웹 요청은 이메일 작업을 Redis에 넣고 빠르게 반환할 수 있습니다. 워커는 잠시 후에 해당 작업을 가져올 수 있습니다. 브로커 토폴로지, 교환, 토픽 또는 새로운 운영 스택이 필요하지 않습니다. 그러나 트레이드오프에 대해 솔직해야 합니다. 일반 RPOP는 워커가 작업을 완료하기 전에 메시지를 제거합니다. 워커가 잘못된 시간에 충돌하면, 그 작업은 주변에 승인 패턴을 구축하지 않는 한 사라집니다.

큐로서의 Redis 리스트 이해

Redis 리스트는 문자열의 정렬된 컬렉션입니다. 요소의 시퀀스로 볼 수 있으며, Redis는 리스트의 머리 또는 꼬리에서 요소를 추가하거나 제거하는 명령어를 제공합니다. 이러한 양방향 특성은 리스트를 큐 구현에 본질적으로 적합하게 만듭니다.

  • 큐에 추가 (메시지 추가): 리스트의 한쪽 끝에 메시지를 푸시하여 큐에 새 메시지를 추가할 수 있습니다. LPUSH 명령어는 리스트의 머리(왼쪽)에 요소를 푸시합니다.
  • 큐에서 제거 (메시지 처리): 리스트의 다른 쪽 끝에서 메시지를 팝하여 큐에서 메시지를 검색하고 제거할 수 있습니다. RPOP 명령어는 리스트의 꼬리(오른쪽)에서 요소를 팝합니다.

이 특정 조합(LPUSH로 큐에 추가하고 RPOP로 큐에서 제거)은 선입선출(FIFO) 큐를 생성하며, 이는 메시지 큐에서 가장 일반적이고 예상되는 동작입니다.

핵심 명령어: LPUSH 및 RPOP

Redis 메시지 큐의 백본을 형성하는 두 가지 기본 명령어를 자세히 살펴보겠습니다.

LPUSH key value [value ...]

LPUSH 명령어는 key에 저장된 리스트의 머리(왼쪽)에 하나 이상의 문자열 값을 삽입합니다. key가 존재하지 않으면 새 리스트가 생성되고 값이 삽입됩니다.

예시:

이메일 전송과 같이 처리해야 할 작업이 있다고 가정해 보겠습니다. 이 작업을 email_tasks라는 Redis 리스트에 메시지로 푸시할 수 있습니다.

# 단일 이메일 작업 푸시
LPUSH email_tasks "{'to': '[email protected]', 'subject': 'Welcome!', 'body': 'Thanks for signing up!'}"

# 다른 작업 푸시, 이전 작업 앞에 배치됩니다.
LPUSH email_tasks "{'to': '[email protected]', 'subject': 'New User Registration', 'body': 'A new user has registered.'}"

이 명령어 후에 email_tasks 리스트는 (머리에서 꼬리로) 다음과 같이 보입니다.

1) "{'to': '[email protected]', 'subject': 'New User Registration', 'body': 'A new user has registered.'}"
2) "{'to': '[email protected]', 'subject': 'Welcome!', 'body': 'Thanks for signing up!'}"

RPOP key

RPOP 명령어는 key에 저장된 리스트의 마지막 요소(꼬리, 오른쪽에서)를 제거하고 반환합니다. 리스트가 비어 있으면 nil을 반환합니다.

예시:

워커 프로세스는 RPOP를 사용하여 주기적으로 email_tasks 리스트에서 새 작업을 폴링할 수 있습니다.

# 워커가 작업 검색 시도
RPOP email_tasks

리스트가 비어 있지 않으면 RPOP는 푸시된 마지막 요소(꼬리에서 첫 번째 요소)를 반환합니다. 위의 예에서 RPOP를 처음 호출하면 다음이 반환됩니다.

"{'to': '[email protected]', 'subject': 'Welcome!', 'body': 'Thanks for signing up!'}"

후속 호출은 꼬리에서 다음 사용 가능한 작업을 검색합니다.

기본 메시지 큐 시스템 구축

LPUSHRPOP를 사용하는 간단한 메시지 큐의 일반적인 흐름을 간략히 설명하겠습니다.

1. 생산자 (작업 큐에 추가)

작업을 오프로드해야 하는 애플리케이션의 모든 부분은 생산자 역할을 할 수 있습니다. 메시지(종종 작업 세부 정보를 나타내는 JSON 문자열)를 구성하고 LPUSH를 사용하여 Redis 리스트에 푸시합니다.

생산자 로직 (개념적 Python 예제):

import redis
import json

r = redis.Redis(host='localhost', port=6379, db=0)

def send_email_task(to_email, subject, body):
    task_message = {
        'type': 'send_email',
        'payload': {
            'to': to_email,
            'subject': subject,
            'body': body
        }
    }
    # LPUSH는 'email_queue' 리스트의 머리에 추가합니다.
    r.lpush('email_queue', json.dumps(task_message))
    print(f"Pushed email task to queue: {to_email}")

# 사용 예시:
send_email_task('[email protected]', 'Hello from Producer', 'This is a test message.')
send_email_task('[email protected]', 'Important Update', 'New features available.')

2. 소비자 (작업 큐에서 제거 및 처리)

독립적으로 실행되는 워커 프로세스는 Redis 리스트에서 새 메시지를 지속적으로 모니터링합니다. RPOP를 사용하여 큐에서 메시지를 가져와 제거합니다.

소비자 로직 (개념적 Python 예제):

import redis
import json
import time

r = redis.Redis(host='localhost', port=6379, db=0)

def process_tasks():
    while True:
        # RPOP는 'email_queue' 리스트의 꼬리에서 메시지를 가져오려고 시도합니다.
        message_bytes = r.rpop('email_queue')
        if message_bytes:
            message_str = message_bytes.decode('utf-8')
            try:
                task = json.loads(message_str)
                print(f"Processing task: {task}")
                # 작업 처리 시뮬레이션
                if task.get('type') == 'send_email':
                    print(f"  -> Sending email to {task['payload']['to']}...")
                    # 실제 이메일 전송 로직으로 대체
                    time.sleep(1) # 작업 시뮬레이션
                    print(f"  -> Email sent to {task['payload']['to']}.")
                else:
                    print(f"  -> Unknown task type: {task.get('type')}")
            except json.JSONDecodeError:
                print(f"Error decoding JSON: {message_str}")
            except Exception as e:
                print(f"Error processing task {message_str}: {e}")
        else:
            # 메시지 없음, 다시 폴링하기 전에 잠시 대기
            # print("No tasks available, waiting...")
            time.sleep(0.5)

if __name__ == "__main__":
    print("Worker started. Waiting for tasks...")
    process_tasks()

생산자를 실행하면 메시지를 푸시합니다. 소비자를 실행하면 메시지를 가져와 처리하기 시작합니다. 처리 순서는 푸시된 순서(FIFO)와 일치합니다. LPUSH는 머리에 추가하고 RPOP는 꼬리에서 제거하기 때문입니다.

안정성 고려 사항

LPUSHRPOP는 기본적인 큐 메커니즘을 제공하지만, 프로덕션 큐를 구축하려면 워커가 충돌하거나, 작업이 실패하거나, 생산자가 잘못된 페이로드를 보낼 때 어떻게 해야 하는지 결정해야 합니다.

1. 처리 중 메시지 손실

워커 프로세스가 RPOP가 메시지를 제거한 처리를 완료하기 전에 충돌하면 해당 메시지는 손실됩니다. 이를 방지하려면:

  • 빠른 폴링 대신 BRPOP 사용: BRPOP는 리스트에 요소가 있거나 시간 초과가 발생할 때까지 차단됩니다. 이것만으로 처리를 안정적으로 만들지는 않지만, 워커가 빈 큐를 찾기 위해 몇 밀리초마다 깨어나는 것을 막습니다.
    # 오른쪽에서 차단 팝, 타임아웃 0 (무기한 차단)
    BRPOP email_queue 0
    
  • 승인을 위한 처리 리스트 사용: 일반적인 패턴은 메시지를 email_queue에서 email_processing으로 원자적으로 이동하고, 처리한 다음, 작업이 성공한 후에만 email_processing에서 제거하는 것입니다. 워커가 죽으면 별도의 리퍼 프로세스가 처리 리스트에서 오래된 항목을 찾아 기본 큐로 다시 이동할 수 있습니다. RPOPLPUSH는 이 패턴의 고전적인 명령어이며, 최신 Redis 버전에서는 LMOVE/BLMOVE도 제공합니다.

2. 실패한 작업 처리

처리 중에 작업이 실패하면(예: 일시적인 네트워크 문제 또는 잘못된 데이터로 인해) 어떻게 됩니까?

  • 재시도 메커니즘: 워커 내에서 재시도 로직을 구현합니다. 몇 번 실패한 후에는 수동 검사를 위해 작업을 'failed_tasks' 리스트로 이동합니다.
  • 데드 레터 큐(DLQ): 반복적으로 처리에 실패하는 메시지가 전송되는 전용 Redis 리스트(또는 다른 저장소)입니다. 이는 디버깅 및 복구에 중요합니다.

3. 여러 소비자

동일한 큐를 소비하는 여러 워커 인스턴스가 있는 경우 RPOP(및 BRPOP)는 각 메시지가 하나의 워커에 의해서만 처리되도록 보장합니다. 이는 RPOP가 요소를 원자적으로 제거하기 때문입니다.

4. 메시지 순서

LPUSHRPOP는 FIFO 큐를 생성하지만, 이 보장은 처리 로직만큼 강력합니다. 소비자가 적절한 처리 없이 실패한 메시지를 다시 큐에 넣거나 다른 작업을 도입하면 엄격한 FIFO 순서가 손상될 수 있습니다.

5. 페이로드 형식 및 멱등성

메시지 본문을 작은 계약으로 취급하십시오. JSON은 redis-cli에서 검사하기 쉽기 때문에 일반적이지만, Python 스타일의 작은따옴표 딕셔너리 대신 유효한 JSON을 사용하십시오.

{"type":"send_email","id":"email-1842","payload":{"to":"[email protected]","template":"welcome"}}

id 필드가 중요합니다. 워커가 시간 초과 후 재시도하거나, 처리 리스트에서 오래된 작업이 다시 큐에 추가되면 동일한 논리적 작업이 두 번 이상 실행될 수 있습니다. 중복이 무해하도록 핸들러를 설계하십시오. 이메일 워커의 경우, 전송 전에 애플리케이션 데이터베이스에 email-1842를 기록한 다음, 재시도가 다른 메시지를 보내기 전에 해당 레코드를 확인하는 것을 의미할 수 있습니다.

6. 큐 길이 및 역압

LLEN email_queue로 큐 길이를 관찰하십시오. 큐가 증가하는 것이 자동으로 나쁜 것은 아닙니다. 단순히 트래픽 급증 후 워커가 따라잡고 있음을 의미할 수 있습니다. 몇 시간 동안 증가하는 큐는 일반적으로 생산자가 소비자보다 빠르거나, 워커가 실패하거나, 하나의 느린 종속성이 모든 것을 지연시키고 있음을 의미합니다.

실제로는 길이뿐만 아니라 에이징(age)에 대해서도 경고하는 것을 좋아합니다. Redis 리스트는 큐에 추가된 시간을 별도로 저장하지 않으므로, 작업 에이징이 중요하다면 페이로드에 타임스탬프를 넣으십시오.

{"type":"resize_image","id":"img-991","created_at":"2026-05-24T08:15:00Z","payload":{"image_id":991}}

그러면 워커 로그를 통해 작업이 몇 초 늦게 처리되는지 또는 몇 시간 늦게 처리되는지 알 수 있습니다. 실제 인시던트를 디버깅할 때 길이만으로는 훨씬 더 유용합니다.

고급 기술 (간략히)

  • RPOPLPUSH: 한 리스트에서 메시지를 원자적으로 팝하여 다른 리스트(예: '처리' 리스트)로 푸시합니다. 이는 승인과 함께 안정적인 처리를 구현하기 위한 핵심 명령어입니다.
  • 여러 키를 사용한 BLPOP / BRPOP: 비어 있지 않은 첫 번째 리스트에서 차단하고 팝합니다. 여러 큐를 소비하는 데 유용합니다.
  • Lua 스크립팅: RPOPLPUSH가 다루지 않는 복잡한 원자적 작업의 경우 Lua 스크립트를 사용하여 중요한 명령어 시퀀스가 중단 없이 실행되도록 할 수 있습니다.

더 안정적인 워커 형태

최선 노력(best-effort) 알림보다 더 중요한 모든 것에 대해 일반적인 "팝하고 기도하기" 워커를 피하십시오. 더 안전한 형태는 다음과 같습니다.

RPOPLPUSH email_queue email_processing

워커는 메시지를 수신하고 Redis는 이미 메시지를 email_processing으로 이동했습니다. 이메일이 전송되고 애플리케이션이 성공을 기록한 후, 워커는 처리 리스트에서 해당 정확한 페이로드를 제거합니다.

LREM email_processing 1 '{"type":"send_email","id":"email-1842"}'

그래도 완벽한 엔터프라이즈 큐는 아닙니다. LREM은 페이로드와 일치해야 하며, 큰 처리 리스트는 다루기 어려워질 수 있고, 메시지가 재시도하기에 충분히 오래되었는지 알 수 있는 리퍼 프로세스가 필요합니다. 그러나 실패 모드를 유용한 방식으로 변경합니다. 워커 충돌이 더 이상 작업의 유일한 복사본을 삭제하지 않습니다.

이 접근 방식을 사용하는 경우 재시도 메타데이터를 메시지에 넣거나 메시지 ID 옆에 다른 키에 저장하십시오. 예를 들어, 리퍼는 처음 몇 번은 오래된 메시지를 email_queue로 다시 이동시킨 다음, 재시도 한도 후에 email_failed로 이동할 수 있습니다. 그러면 동일한 잘못된 페이로드가 영원히 실패하는 것을 지켜보는 대신 독 메시지를 검사할 수 있는 장소가 제공됩니다.

Redis 리스트가 잘못된 큐인 경우

Redis 리스트는 이해하기 쉽지만 항상 올바른 도구는 아닙니다. 지연된 작업, 작업 우선 순위, 예약된 재시도, 워크플로 가시성 또는 장기 감사 기록이 필요한 경우 작업 라이브러리 또는 전용 브로커가 결국 덜 복잡할 수 있습니다. Redis Streams도 고려해 볼 가치가 있습니다. 소비자 그룹 및 승인 의미 체계가 데이터 유형에 내장되어 있기 때문입니다.

저는 여전히 작은 내부 큐에 리스트를 사용하는 것을 좋아합니다: 썸네일 생성, 캐시 워밍업, 소스 시스템이 재시도할 수 있는 웹훅 팬아웃, 또는 하나의 애플리케이션이 소유한 간단한 백그라운드 작업. 여러 팀이 큐 계약에 의존하게 되면 전달 기대치를 기록하십시오. "최소 한 번", "최대 한 번", "최선 노력"은 인시던트 중에 학술적인 용어가 아닙니다. 중복이 허용되는지, 메시지 손실이 허용되는지, 얼마나 많은 복구 기계 장치가 필요한지 결정합니다.

Redis 리스트는 작고 이해하기 쉬운 큐가 필요하고 이미 Redis를 운영하고 있을 때 적합합니다. 간단한 백그라운드 작업을 위해 LPUSHBRPOP로 시작하십시오. 작업 손실이 중요해지면 처리 리스트, 재시도 횟수 및 데드 레터 리스트를 추가하십시오. 지연된 스케줄링, 우선 순위, 팬아웃, 긴 보존 또는 여러 서비스에 걸친 강력한 전달 보장이 필요하다면, 일반적으로 목적에 맞게 구축된 큐가 증가하는 Redis 규칙 더미보다 관리하기 쉬워지는 시점입니다.