| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- 임베딩
- 메세지브로커
- 장애복구
- 레디스스트림
- redis
- 메시지브로커
- jedis
- retry
- 코사인
- 배치처리
- 비동기처리
- Kafka
- god object
- 자연어캐싱
- blockingqueue
- DLT
- aof
- redissearch
- 백엔드
- 테스트코드
- SaaS
- rdb
- 객체지향적사고
- 시맨틱캐싱
- 레디스
- OOP
- 마케팅 #퍼플카우 #새스고딘 #혁신 #독서 #이북
- 데이터유실방지
- springboot
- redisstreams
- Today
- Total
pandaterry's 개발로그
[SaaS 개발로그] Redis Pub/Sub이면 충분한 줄 알았습니다 본문
Kafka로 전환하기 전, 직접 실험해봤습니다

SaaS에서 비동기 메시지는 선택이 아니라 필수입니다
제가 구축하고 있는 SaaS 시스템은 마케팅/비즈니스 부서가 자연어로 데이터를 요청하면, 그 요청을 백엔드에서 SQL로 변환하고 엑셀로 응답해주는 플랫폼입니다.
문제는 여기서 끝나지 않았습니다.
- 사용자의 요청은 내부 시스템에서 백엔드 작업자에게 전송돼야 하고
- 동시에 로그로 남겨져야 하며
- 실패 시 재시도도 가능해야 합니다
처음엔 Redis Pub/Sub으로 충분하다고 생각했습니다.
하지만 “요청이 처리됐는지 안 됐는지조차 확인할 수 없다면, 그건 SaaS가 아니라 알 수 없는 상자”가 되어버리더군요.
그래서 Kafka를 검토하게 되었고,
“진짜로 필요한가?”라는 의문에 답하기 위해 직접 실험을 진행했습니다.
실험 목표: 메시지 유실 가능성과 재처리 구조 검증
단순 메시지 전달이 아닌, SaaS 플랫폼으로서 요청이 실패하더라도 반드시 재처리되거나 기록에 남는 구조가 가능한지를 비교해봤습니다.
공통 실험 조건
- 총 5개 메시지를 전송 (
event-0~event-4) - 소비자는
event-2수신 시 강제 종료 - 그 사이에 발행된 메시지가 유실되지 않고
재시작 후 다시 수신 가능한지를 확인
Redis Pub/Sub 실험
발행자 코드
@Slf4j
@Component
@RequiredArgsConstructor
public class RedisPublisher {
private final RedisTemplate<String, String> redisTemplate;
@Qualifier("redisChannel")
private final String channel;
public void publish(String message) {
log.info("Redis Publisher: 메시지 발행 시작 - {}", message);
redisTemplate.convertAndSend(channel, message);
log.info("Redis Publisher: 메시지 발행 완료 - {}", message);
}
}
구독자 코드
@Slf4j
@Component
public class RedisSubscriber implements MessageListener {
@Override
public void onMessage(Message message, byte[] pattern) {
String receivedMessage = new String(message.getBody());
log.info("Redis Subscriber: 메시지 수신 - {}", receivedMessage);
if (receivedMessage.equals("event-2")) {
log.error("Redis Subscriber: event-2 수신으로 인한 종료 예정");
log.error("Redis Subscriber: event-3, event-4는 유실될 예정입니다.");
log.error("Redis Subscriber: 메시지 유실 확인을 위해 애플리케이션을 재시작해도 event-3, event-4는 수신되지 않습니다.");
System.exit(1);
}
}
}
테스트 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 실험: 모든 메시지 발행 완료");
}
1회 호출 - 콘솔 로그(발행은 다 되었고 event-2까지 수신되고 서버가 종료됨)

2회 호출(재시작) - 콘솔 결과(재시작하면 event-3부터는 수신된 흔적이 없음)

결과
- 구독자가 살아있는 동안만 메시지를 수신
event-3,event-4는 구독자가 죽은 사이 전송되었기 때문에 완전히 유실됨- 메시지 재처리 불가
- 어떤 메시지가 전달됐는지도 확인 불가
Kafka 실험
프로듀서 코드
public void send(String message) {
log.info("Kafka Producer: 토픽[{}]에 메시지 발행 시작 - {}", topic, message);
kafkaTemplate.send(topic, message)
.whenComplete((result, ex) -> {
if (ex == null) {
log.info("Kafka Producer: 토픽[{}]에 메시지 발행 완료 - {}", topic, message);
} else {
log.error("Kafka Producer: 토픽[{}]에 메시지 발행 실패 - {}", topic, message, ex);
}
});
}
컨슈머 코드
@KafkaListener(topics = "${app.kafka.topic}", groupId = "${spring.kafka.consumer.group-id}")
public void consume(String message, Acknowledgment ack) {
log.info("Kafka Consumer: 토픽[{}]에서 메시지 수신 - {}", topic, message);
if (message.equals("event-2")) {
log.error("Kafka Consumer: event-2 수신으로 인한 종료 예정");
log.error("Kafka Consumer: event-3, event-4는 Kafka에 저장되어 있음");
log.error("Kafka Consumer: 컨슈머 그룹[{}]의 오프셋이 저장되어 있어 재시작 시 event-3, event-4를 이어서 처리합니다.", groupId);
ack.acknowledge();
try {
// 커밋이 완료될 시간을 확보
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.error("Kafka Consumer: 오프셋 커밋 후 종료");
System.exit(1);
}
ack.acknowledge();
}
1회 호출 - 콘솔 로그(발행은 다 되었고 event-2까지 수신되고 서버가 종료됨)

2회 호출(재시작) - 콘솔로그(재시작하자마자 API 호출없이 event-3부터 수신)

결과
event-3,event-4는 컨슈머가 죽은 동안에도 Kafka에 저장되어 있음- 재시작 시, 동일 consumer group으로 그대로 다시 소비 가능
- offset 조절을 통해 한 번 소비한 메시지도 재처리 가능
비교 요약
| 항목 | Redis Pub/Sub | Kafka |
|---|---|---|
| 메시지 유실 여부 | 구독자 중단 시 즉시 유실 | 저장됨 |
| 재처리 가능 여부 | 불가 | 가능 (offset 기반) |
| 소비 상태 추적 | 불가능 | consumer group offset으로 가능 |
| 장애 복구 구조 | 없음 | 보장 (디스크 로그 기반) |
| 적합한 역할 | 실시간 알림, 단순 이벤트 | 요청 추적, 로그 보존, 재처리 가능한 시스템 |
SaaS 시스템에서 Kafka가 필요한 순간
처음에는 Redis Pub/Sub이 훨씬 간편했습니다.
하지만 실시간 응답을 넘어서 요청의 책임을 가져야 하는 구조로 발전하면서 Kafka가 필요해졌습니다.
- Redis Pub/Sub은 "누가 받았는지"조차 모릅니다
- Kafka는 “누가 언제 무엇을 받았는지”를 기록합니다
SaaS 플랫폼은 요청이 언제든 실패할 수 있다는 걸 전제로 해야 합니다.
그래서 중요한 건 "전달"이 아니라, "기록과 복구"입니다.
그래서 결론은?
Kafka는 느리고 무겁습니다.
하지만 “요청을 다시 보낼 수 없는 구조”라면, 유일하게 믿을 수 있는 선택입니다.
Redis Pub/Sub은 여전히 훌륭합니다.
하지만 SaaS에서 신뢰할 수 있는 비동기 처리 시스템을 만들고 싶다면,
Kafka는 비용이 아니라 보험처럼 생각해야 합니다.
TODO (후속 실험 제안)
- Redis Streams를 활용한 재처리 보완 구조 실험
- Kafka retention 정책 단축 시 유실 임계 확인
- SaaS에서 Kafka와 DB 트랜잭션을 안전하게 묶는 방식 실험
이 글은 SaaS 구조에서 메시지 브로커 선택의 실질적인 기준을 세우기 위한 실험 기록입니다.
이후 글에서는 Redis Streams와 Kafka의 소비 그룹 차이,
그리고 Spring 기반 구조에서 트랜잭션과 어떻게 묶을 수 있는지도 다뤄볼 예정입니다.
'개발 > 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로 Kafka 없이 유실 없는 메시지 큐를 만들 수 있을까? (1) | 2025.06.12 |