한 줄 핵심
Redis와 Celery는 완전히 다른 소프트웨어다.
- Redis = 인메모리 데이터 서버(메시지를 보관하는 장소)
- Celery = 분산 작업 큐 프레임워크(메시지를 꺼내서 실행하는 주체)
Redis는 단독으로도 쓸 수 있지만, Celery는 반드시 Redis나 RabbitMQ 같은 **Message Broker(메시지 중개자)**를 필요로 한다. 즉 Celery가 Redis를 빌려 쓰는 관계임.
포함 관계와 전체 구도
┌──────────────────────────────────────────────────────┐
│ 전체 구조 │
│ │
│ FastAPI Redis Celery │
│ (웹 서버) (메시지 보관함) (작업 실행기) │
│ │
│ "주문 접수" → "주문서 보관" → "요리 실행" │
│ │
│ Python C 언어 기반 Python │
│ 웹 프레임워크 인메모리 DB 작업 큐 │
│ 프레임워크 │
└──────────────────────────────────────────────────────┘
| 관점 | Redis | Celery |
|---|---|---|
| 정체 | 인메모리 데이터 서버 | 분산 작업 큐 프레임워크 |
| 구현 언어 | C로 작성된 독립 서버 | Python 라이브러리 |
| 역할 | 메시지를 보관하는 장소 | 메시지를 꺼내서 실행하는 주체 |
| 비유 | 우체통 | 우체부 |
| 단독 사용 | 가능(캐시/DB/잠금 등) | 불가. 반드시 Redis/RabbitMQ 같은 Broker 필요 |
Redis의 본질
Redis는 **인메모리 데이터 구조 서버(In-Memory Data Structure Server)**다. “캐시”는 Redis의 여러 기능 중 하나일 뿐임.
Redis = 초고속 인메모리 데이터 구조 서버
│
├─ 1. 캐시(Cache)
│ "API 응답 24시간 저장해두고 재사용"
│
├─ 2. 메시지 큐(Message Queue) ← Celery가 쓰는 기능
│ "할 일 목록을 줄 세워서 Worker에게 전달"
│
├─ 3. Pub/Sub(실시간 메시징)
│ "채팅방처럼 구독자에게 메시지 브로드캐스트"
│
└─ 4. 상태 저장소(State Store)
"세션, 잠금(Lock), 카운터 등"
1. 메시지 큐(Message Queue)로서의 Redis
Redis에는 LPUSH / BRPOP이라는 리스트 자료구조(List Data Structure) 명령이 있다. 이게 곧 큐(Queue, 선입선출 대기열) 역할을 함.
[생산자 - FastAPI] [Redis 리스트] [소비자 - Worker]
사용자 A 요청 ──LPUSH──→ ┌─────────────────┐
│ 작업 C (대기) │
사용자 B 요청 ──LPUSH──→ │ 작업 B (대기) │ ──BRPOP──→ Worker가 꺼내서 실행
│ 작업 A (대기) │
사용자 C 요청 ──LPUSH──→ └─────────────────┘
번호표 대기열과 동일
| 명령 | 의미 | 비유 |
|---|---|---|
LPUSH | 리스트 왼쪽에 작업 넣음 | 번호표 발급 |
BRPOP | 리스트 오른쪽에서 작업 꺼냄 (Blocking, 작업 올 때까지 대기) | 다음 번호 호출 |
Celery와 Redis의 관계
Celery = 작업 분배 프레임워크 (매니저)
Redis = 작업 대기열을 저장하는 장소 (게시판)
┌──────────┐ ┌──────────┐ ┌──────────┐
│ FastAPI │──→──→│ Redis │──→──→│ Celery │
│ "작업 │ 등록 │ "대기열 │ 꺼냄 │ Worker │
│ 요청" │ │ 보관" │ │ "실행" │
└──────────┘ └──────────┘ └──────────┘
Celery는 Redis 말고 RabbitMQ 같은 다른 Broker도 쓸 수 있다. Redis는 **교체 가능한 부품(Message Broker)**임. 다만 이미 캐시로 Redis를 쓰는 프로젝트가 많아 큐까지 Redis 하나로 해결하는 경우가 흔하다.
2. Pub/Sub (Publish / Subscribe, 발행-구독)
“방송국과 라디오” 모델이다.
Publisher (방송국) Subscriber (라디오)
│ │
│ "대치동 보고서 완성됨!" │
│──── PUBLISH ────→ [Redis 채널] ────→ 듣고 있는 모든 구독자에게 전달
"report_done" │
├─ 웹소켓 서버 → 사용자 브라우저에 알림
├─ 이메일 서버 → 발송 트리거
└─ 로그 서버 → 기록
메시지 큐와 뭐가 다른가
둘 다 “메시지를 보낸다”는 점은 같지만 배달 방식이 완전히 다르다.
[메시지 큐 - 택배]
Worker 1 ← 작업 A (1명만 받음)
FastAPI → Redis 리스트 → BRPOP → Worker 2 ← 작업 B (1명만 받음)
Worker 3 ← 작업 C (1명만 받음)
→ 작업 1개를 Worker 1명만 가져감 (1:1 배달)
[Pub/Sub - 라디오 방송]
구독자 1 ← "보고서 완성!" (전원 수신)
Publisher → Redis 채널 → PUBLISH → 구독자 2 ← "보고서 완성!" (전원 수신)
구독자 3 ← "보고서 완성!" (전원 수신)
→ 메시지 1개를 모든 구독자가 동시에 받음 (1:N 방송)
| 관점 | 메시지 큐(List) | Pub/Sub |
|---|---|---|
| 비유 | 택배(수령인 1명) | 라디오 방송(청취자 전원) |
| 수신자 | Worker 1개만 꺼내감 | 구독자 전부 수신 |
| 보관 | 꺼낼 때까지 Redis에 남아 있음 | 보관 안 함. 그 순간 안 듣고 있으면 유실 |
| 용도 | 작업 분배(무거운 처리) | 이벤트 알림(가벼운 통지) |
실제 프로젝트 적용 예
현재: 사용자가 GET /status/{job_id}로 "끝났나?" 반복 조회 (Polling)
브라우저: 끝났나? → 아니오
끝났나? → 아니오
끝났나? → 아니오 ← 30분간 반복... 서버 부담
끝났나? → 네!
Pub/Sub 적용 후: 끝나면 서버가 알려줌
브라우저: "report_done" 채널 구독 후 대기
서버: 보고서 완성 → PUBLISH "report_done" "작업 완료!"
브라우저: 알림 수신 → 결과 표시 ← 불필요한 반복 조회 제거
한 줄 정리
Pub/Sub은 “작업을 시키는 것”이 아니라 **“일어난 일을 알리는 것”**이다. 메시지 큐가 택배(1명 배달)라면, Pub/Sub은 라디오 방송(전원 전달)임.
3. Redis의 상태 저장소(State Store) 기능
세션, 잠금, 카운터처럼 **“잠깐 기억해둬야 하는 것들”**을 처리한다.
3-1. 세션(Session)
“이 사용자가 로그인한 상태인지” 기억하는 것.
사용자 로그인 → 서버가 세션 토큰 발급 → 어딘가에 저장해야 함
[저장 방식 비교]
DB에 저장: 로그인 확인할 때마다 DB 조회 → 느림
Redis에 저장: 메모리에서 즉시 조회 → 빠름 (만료 시간 설정 가능)
Redis: { "session:abc123": "user_id=42, login_time=..." } ← TTL 30분 후 자동 삭제
3-2. 잠금(Lock)
**“지금 누가 쓰고 있으니 건드리지 말라”**는 표시다.
사용자 A: "강남구 대치동" 보고서 생성 요청
사용자 B: "강남구 대치동" 보고서 생성 요청 (동시에)
Lock 없이:
Worker 1 → 대치동 보고서 생성 시작 (30분)
Worker 2 → 대치동 보고서 생성 시작 (30분) ← 동일 작업 중복 실행. 자원 낭비
Lock 있으면:
Worker 1 → Redis에 "대치동_보고서" Lock 획득 → 실행
Worker 2 → Lock 확인 → "이미 누가 하고 있네" → 대기 or 스킵
Redis: SET "lock:대치동_보고서" "worker1" EX 1800 ← 30분간 잠금, 자동 해제
3-3. 카운터(Counter)
**“몇 번 일어났는지”**를 세는 것. Rate Limit(요청 횟수 제한)에 자주 쓰인다.
[Rate Limit 예시]
사용자가 API를 1분에 10번까지만 호출 가능하게 제한할 때:
요청 1 → Redis: INCR "rate:user42" → 1 ✅ 통과
요청 2 → Redis: INCR "rate:user42" → 2 ✅ 통과
...
요청 11 → Redis: INCR "rate:user42" → 11 ❌ 차단
1분 후 → 키 자동 만료 → 카운터 리셋
INCR은 Redis의 원자적(Atomic, 중간에 끊기지 않는) 증가 명령이다. 동시에 100개 요청이 와도 숫자가 꼬이지 않음.
왜 DB가 아니라 Redis에 넣나
세 가지의 공통점: 수명이 짧고, 빈번하게 읽고 쓰며, 정확해야 함.
| 관점 | DB(PostgreSQL 등) | Redis |
|---|---|---|
| 저장 매체 | 디스크(Disk) | 메모리(RAM) |
| 속도 | ms(밀리초) 단위 | μs(마이크로초) 단위, 약 1000배 빠름 |
| 자동 만료(TTL) | 직접 구현해야 함 | TTL 설정 한 줄 |
| 원자적 연산 | 트랜잭션 필요 | INCR, SETNX 등 내장 |
| 적합한 데이터 | 영구 보관(회원정보, 보고서) | 휘발성(세션, 잠금, 카운터) |
한 줄 정리
세션/잠금/카운터 모두 “잠깐만 빠르게 기억해야 하는 것”이다. DB에 넣기엔 과하고 변수에 넣기엔 서버 재시작하면 사라지니, 그 중간 지점인 Redis에 저장함.
Celery 기본 구조
구성 요소
[FastAPI / Django] [Broker] [Celery Worker]
│ (Redis) │
│ │ │
│ ── send_task() ──→ │ │
│ 작업 대기열 │
│ │ ── 꺼냄 ──→ │
│ │ │ 작업 실행
│ │ │ (예: 보고서 생성)
│ │ │
│ [Result Backend] │
│ ← 결과 조회 ── (Redis) ← 결과 저장 ── │
- Broker: 작업 메시지를 대기열에 보관(Redis 리스트)
- Worker: 별도 프로세스로 상주하면서
BRPOP으로 작업을 꺼내 실행한다 - Result Backend: 실행 결과를 저장하는 곳(Redis 또는 DB). 웹 서버가 나중에 결과를 조회할 때 사용
왜 Celery가 필요한가
FastAPI/Django는 요청-응답 주기가 짧아야 한다 (보통 수백 ms 이내). 보고서 생성 같은 30분짜리 작업을 동기로 처리하면 HTTP 타임아웃이 나고 다른 사용자도 못 받는다.
[동기 처리 - 나쁨]
요청 → 30분 대기 → 응답 ← 브라우저 타임아웃, 서버 블로킹
[Celery 비동기 처리 - 좋음]
요청 → job_id 즉시 반환 ← 응답 0.01초
│
└─ 백그라운드 Worker가 30분짜리 작업 처리
└─ 완료 후 결과 저장 or Pub/Sub으로 알림
최소 코드
# celery_app.py
from celery import Celery
app = Celery(
"tasks",
broker="redis://localhost:6379/0",
backend="redis://localhost:6379/1",
)
@app.task
def generate_report(region: str) -> str:
# 무거운 작업
return f"{region} 보고서 완료"
# producer (FastAPI 쪽)
job = generate_report.delay("대치동") # Redis 큐에 등록
print(job.id) # 사용자에게 job_id 반환큐잉에 대한 흔한 오해
- “큐잉 = 느려지는 것”이 아니다
- 큐잉은 Worker 수로 동시 처리량(Throughput)을 제어하는 장치임
- Worker 1개면 순차 처리, 10개 띄우면 10배 동시 처리
직접 확인
# Redis 서버 띄우기 (Docker)
docker run -p 6379:6379 redis:7
# redis-cli 접속해서 리스트 동작 확인
docker exec -it <container_id> redis-cli
> LPUSH queue "job1"
> LPUSH queue "job2"
> BRPOP queue 0 # 오른쪽 끝에서 꺼냄, 없으면 무한 대기한마디 요약
Redis는 “캐시 전용”이 아니라 인메모리 데이터 구조 서버다. 그 자료구조 중 리스트(List)가 곧 메시지 큐 역할을 함. Celery는 그 큐에서 작업을 꺼내 Worker에 분배하는 작업 실행 프레임워크이고, 둘은 완전히 별개이며, Celery가 Redis를 우체통처럼 빌려 쓰는 관계다.