| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
- rdb
- 배치처리
- 메세지브로커
- 메시지브로커
- 백엔드
- 비동기처리
- 자연어캐싱
- blockingqueue
- redissearch
- 객체지향적사고
- 레디스스트림
- redisstreams
- 시맨틱캐싱
- OOP
- aof
- 임베딩
- 장애복구
- jedis
- SaaS
- 레디스
- DLT
- springboot
- Kafka
- 코사인
- redis
- 마케팅 #퍼플카우 #새스고딘 #혁신 #독서 #이북
- god object
- retry
- 테스트코드
- 데이터유실방지
- Today
- Total
pandaterry's 개발로그
[Saas 개발로그] Redis로 Kafka 없이 유실 없는 메시지 큐를 만들 수 있을까? 본문
이전 글(https://pandaterry.tistory.com/9)에서는 Redis Pub/Sub과 Kafka를 비교하며 메시지 유실·재처리 이슈를 검증했습니다.
이번 글에서는 “Redis만으로도 신뢰형 메시지 큐를 구현할 수 있지 않을까?”라는 의문으로 다음 방식으로 직접 비교·실험해보겠습니다.

왜 Pub/Sub만으로는 부족했을까?
현재 개발중인 SaaS 서비스는 “사용자의 Saas 기능 사용 -> 사용량 측정”이라는 흐름을 비동기로 처리합니다.
이 구조에서 가장 먼저 선택한 메시지 브로커는 Redis Pub/Sub이었습니다.
설정이 간단하고, 즉각적인 전달이 가능했기 때문입니다.
하지만 곧 문제를 마주하게 됩니다.
"만약 메시지를 구독 중이던 서버가 죽는다면?"
"그 사이에 발행된 메시지는 어떻게 되는가?"
실제 실험 결과는 이랬습니다:
- 구독자는 event-2 수신 후 강제 종료
- event-3, event-4는 유실
- 서버 재시작 후에도 다시 수신 불가
이 실험은 다음 사실을 명확히 보여줬습니다.
Redis Pub/Sub은 메시지를 보존하지 않으며,
구독자가 죽은 시점 이후의 메시지는 영원히 사라진다.
그럼 Redis로도 메시지 유실을 막을 수 있을까?
Kafka나 Redis Streams 같은 시스템으로 가기 전에 가장 간단한 방법부터 실험해보기로 했습니다.
"메시지를 Redis의 List에 저장하고, 직접 꺼내 처리하면 어떨까? AOF와 RDB를 적용하면 유실되지 않으니까!"
단 하나의 Consumer 클래스만 추가해
Pub/Sub 구조를 커스텀 큐 구조로 바꿔봤습니다.
실험시작
0. Redis AOF, RDB 설정
: AOF 설정으로 디스크에 데이터가 기록이 됩니다. 그리고 RDB 설정은 기본값을 따랐습니다.

1. Redis Publisher 코드
: 이전과 동일합니다. 발행에는 문제가 없으니까요.
public class RedisPublisher {
private final RedisTemplate<String, String> redisTemplate;
@Qualifier("redisListKey")
private final String listKey;
public void publish(String message) {
log.info("Redis Publisher: 메시지 발행 시작 - {}", message);
redisTemplate.opsForList().leftPush(listKey, message);
log.info("Redis Publisher: 메시지 발행 완료 - {}", message);
}
}
2. Redis Consumer 코드
: 이번 구조는 이전과 다른 점이 명확합니다. sub 기능을 자체 구축해야한다는 점과 pub/sub과 별개로 Redis에 저장되는 데이터를 기반으로 자체 구현하는 겁니다. 왜냐면 AOF나 RDB 모두 Redis 에 저장되는 메모리 데이터에 적용되는 것이기 때문이죠.
특징 설명
- ExecutorService : 이전과 다르게 일반적인 구독을 사용하는게 아닌 내부 메모리 자료구조를 읽어오는 로직을 자첵 구현해야하기 때문에 스레드처리를 분리합니다.
- listKey : kafka의 queue에서 topic으로 처리하는 것처럼 listKey를 사욜합니다.
- @PostConstruct : listKey가 주입되고 이벤트를 소비할 수 있도록 합니다.
동작 원리
- leftPush로 발행된 메시지를
rightPop으로 FIFO 처리 - 소비자 중단 시점까지 꺼내지 않은 메시지는 리스트에 그대로 남아 재시작 후 자동 이어 처리
public class RedisConsumer {
private final RedisTemplate<String, String> redisTemplate;
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
@Qualifier("redisListKey")
private final String listKey;
@PostConstruct
public void start() {
executorService.submit(this::consume);
}
private void consume() {
while (!Thread.currentThread().isInterrupted()) {
try {
String message = redisTemplate.opsForList().rightPop(listKey);
if (message != null) {
log.info("Redis Consumer: 메시지 수신 - {}", message);
if (message.equals("event-2")) {
log.error("Redis Consumer: event-2 수신으로 인한 종료 예정");
log.error("Redis Consumer: event-3, event-4는 Redis List에 저장되어 있음");
log.error("Redis Consumer: 애플리케이션 재시작 시 event-3, event-4를 이어서 처리합니다.");
System.exit(1);
}
}
} catch (Exception e) {
log.error("Redis Consumer: 메시지 처리 중 오류 발생", e);
}
}
}
}
3. 테스트 API
@PostMapping("/redis")
public void runRedisExperiment() throws InterruptedException {
log.info("Redis 실험 시작");
log.info("event-2 수신 시 애플리케이션이 종료되며, 이후 메시지는 유실됩니다.");
for (int i = 0; i < 5; i++) {
String message = "event-" + i;
log.info("Redis 실험: 메시지 발행 예정 - {}", message);
redisPublisher.publish(message);
Thread.sleep(200);
}
log.info("Redis 실험: 모든 메시지 발행 완료");
}
4. 이벤트 전체 발행 및 부분 수신
: 발행은 모두 잘되었고, event-2를 수신하자마자 시스템이 다운되도록 함. 그래서 수신은 event-2까지만 수신완료.

5. Redis 재시작
: 사실 AOF, RDB 검증을 위해선 이게 정말 디스크에 잘 들어가있는지 확인이 되어야합니다. 그래서 redis를 재시작을 해주고 서버를 재기동해줍니다.
6. 서버 재기동
: tomcat이 실행되는 속도보다 빨리 처리했음. 콘솔로그를 보면 event-3, event-4가 수신된 걸 확인가능

실험 시나리오 및 결과
- 시나리오
- 5개 메시지(event-0~event-4) 발행
- event-2 처리 시 애플리케이션 강제 종료
- 재시작 후 남은 메시지 처리 여부 확인
- 결과 비교
| 항목 | Redis Pub/Sub | Kafka | Redis AOF/RDB |
| 메시지 유실 | 구독자 중단 시 즉시 유실 | 저장·오프셋 관리로 보존 | 리스트에 남아 보존 |
| 재처리 지원 | 불가 | 가능 (offset 기반) | 가능 (리스트 잔여 메시지) |
| 순서 보장 | 불명확 | 파티션 내 보장 | FIFO 보장 |
| 처리 메타 추적 | 불가 | 가능 | 불가 |
| 운영 복잡도 | 최저 | 높음 | 중 |
Redis Consumer만으로도 Pub/Sub의 유실 문제를 해결하고 Kafka와 유사한 메시지 영속성을 확보했습니다.
시사점과 한계: 정말 유실이 막힌 걸까?
이번 실험에서 Redis Pub/Sub 구조를 커스텀 Consumer로 바꾸기만 해도 메시지 유실을 방지할 수 있다는 것을 확인했습니다.
하지만 여기서 멈추면 안 됩니다. “유실이 없다”는 말은, Redis 서버가 멀쩡히 살아 있는 경우에만 성립합니다.
1. Redis 서버가 살아 있을 땐 유실 없음
커스텀 Consumer는 leftPush로 리스트에 메시지를 쌓고, rightPop으로 꺼내 처리합니다.
Consumer가 중간에 죽더라도 Redis 서버 자체가 살아 있다면 아직 꺼내지 않은 메시지는 리스트에 그대로 남아 재시작 후 이어서 처리됩니다.
이 구조만으로도 Pub/Sub의 유실 문제는 완전히 해결됩니다.
2. AOF와 RDB, 설정을 안 하면 “있으나 마나”
단순히 AOF를 켰다고 해서 모든 메시지가 디스크에 바로 저장되는 건 아닙니다.
예시: 기본 AOF 설정
appendonly yes
appendfsync everysec
이건 Redis가 1초에 한 번씩만 디스크에 기록하도록 설정한 것입니다.
즉, 메시지를 발행하고 1초 이내에 Redis가 죽으면 AOF 파일에 기록되기 전이라서 메시지가 유실됩니다.
RDB는 더 극단적입니다.
save 60 10000
- 60초에 한 번, 1만 번의 write가 발생했을 때만 스냅샷을 찍습니다.
- 그 사이에 죽으면 메시지는 날아갑니다.
3. 실무에서 Redis를 쓴다면 반드시 확인해야 할 것들
| 항목 | 기본값 | 실무 권장 설정 |
| appendonly | no | yes |
| appendfsync | everysec | always (디스크 IO 여유 시) |
| save | 60 10000 | 최소화 또는 비활성화 후 AOF로 대체 |
everysec은 성능과 안전성의 균형이지만, 실시간 큐 시스템에서는 여전히 유실 리스크가 존재합니다.
결론: 유실을 막기 위한 진짜 기준
Redis로도 유실 없는 구조를 만들 수 있습니다.
하지만 그건 단순히 리스트에 쌓는 걸로는 부족합니다.
"언제 디스크에 쓰는가?"가 실제 신뢰성을 결정합니다.
그래서...
- Pub/Sub → 유실 발생 (기본 구조상)
- List 큐 → Redis 살아있으면 안전 (메모리 기반)
- AOF/RDB → Redis 죽어도 안전하려면, 설정이 핵심
- Kafka → 설계부터 디스크 기반, 유실이 원천 차단
다음 단계: Redis Streams vs Kafka
단일 List 큐로 내구성을 확보했지만, 메타 추적과 컨슈머 그룹 관리까지 필요하다면 Redis Streams나 Kafka를 검토해야 합니다.
다음 글에서는 Redis Streams 구현 코드와 Kafka 대비 성능·운영 관점 심층 비교를 다룹니다.
'개발 > Saas 개발로그' 카테고리의 다른 글
| [Saas 개발로그] LLM 호출비 줄이는 법, Redis Search로 검증해봤습니다 (2) | 2025.06.21 |
|---|---|
| [Saas개발로그] JVM 2GB로 1,000만 행 Excel 처리는 가능한가? 직접 실험해봤습니다 (0) | 2025.06.14 |
| [Saas 개발로그] Redis Streams~ 너도 kafka처럼 복구해봐 (1) | 2025.06.13 |
| [SaaS 개발로그] Redis Pub/Sub이면 충분한 줄 알았습니다 (0) | 2025.06.11 |
