서비스가 성장하면서 단순한 API 서버만으로는 감당하기 어려운 요구사항들이 하나둘씩 붙기 시작했다.
백엔드에 여러 분석 도구, 마케팅 캠페인, 실시간 지표 등 추가 기능을 기존 기능과 합쳐 계속 추가해야 했다.
- Mixpanel
- Appsflyer
- Notification
- Growthbook Experiments
문제는 이런 기능들이 대부분 기존 API 흐름 안에 있다는 것이었다.
초반에는 크기가 작아 이렇게 처리해도 충분히 컨트롤 가능했지만, 시간이 지날수록 코드 복잡도와 응답 지연, 운영 부담이 눈에 띄게 증가했다.
우리는 이 문제를 해결하기 위해 메시지 큐와 캐싱 레이어 도입을 고민했고, 일반적으로 많이 선택되는 Kafka와 Redis를 모두 검토했다.
하지만 최종적으로는 모든 케이스를 Redis 기반으로 해결하기로 결정했다. 이번 글에서는 그 판단의 배경과, Kafka가 항상 정답은 아니라고 생각하게 된 이유를 정리해보려 한다.
먼저 우리가 직면한 상황 / 문제는 아래와 같다.
1. API 안에 점점 쌓여가는 부가 기능들
서비스가 커지면서 회원 가입이나 결제 성공 등 여러 API에서 추가로 처리해야 할 작업들이 늘어났다.
- Appsflyer 이벤트 전송
- Email 발송
- Notification 전송
- 내부 분석 Log 적재
이 로직들이 기존 API 흐름에 추가되면 아래와 같은 문제가 예상된다.
- 부가 기능 중 하나라도 실패하면 API 전체가 실패할 수 있음
- API 코드가 비대해지고 가독성이 급격히 떨어짐
- 핵심 비즈니스 로직과 부가 로직이 강하게 결합됨
- 단일 책임 원칙(SRP)을 명확하게 위반
특히 회원 가입이라는 핵심 기능과 마케팅 이벤트 전송이라는 부가 기능이 같은 실패 경로를 공유하는 구조는 매우 위험하다.
따라서 이런 작업들은 이벤트 큐 기반으로 분리되어야 했다.
- API는 이벤트만 발행
- 실제 처리는 비동기 Consumer가 담당
- 실패해도 사용자 요청에는 영향 없음
2. Browsing API의 지연
Browsing API는 메인 화면에서 사용자가 가장 먼저 호출하는 핵심 API다.
- 추천 캐릭터 목록
- 검색 결과
- 메인 화면 콘텐츠
하지만 이 API는 여러 요소로 쉽게 느려지곤 하였다.
- 추천 알고리즘 서버(Gorse)의 연산 비용
- 특정 시간대 트래픽 집중
- 외부 의존성 증가
Browsing은 사용자가 우리 서비스를 사용할 때 처음 경험하는 Feature이기 때문에, 메인 페이지 진입 시 지연이 발생하면, 서비스 전체가 느리다고 인식된다.
그래서 캐싱 전략을 세워서 지연을 줄이고 항상 일관된 속도로 결과를 가져올 수 있어야 했다.
3. 실시간 사용자 상태를 보여줘야 했다
서비스가 Active해 보이기 위해서 지금 누가 쓰고 있는지를 실시간으로 보여주는 기능이 필요했다.
- 현재 대화 중인 사용자 수
- 특정 캐릭터와 대화 중인 유저 존재 여부
- 실시간 상태 표시

이 데이터들은 강한 정합성이 필요하지 않은 휘발성 데이터이고, 빠른 응답이 최우선 되어야 했기 때문에 DB에 저장하기엔 과하고, 메시지 브로커로 처리하기에도 맞지 않는 영역이었다.
우리는 이 문제들을 해결하기 위해 Kafka와 Redis를 함께 고민했다
우리가 해결해야 할 문제는 크게 세 가지였다.
- 이벤트 기반 비동기 처리 (Queue)
- 응답 속도를 보장하기 위한 캐싱 (Cache)
- 실시간 상태 관리 (In-Memory State)
일반적으로 많은 기업과 서비스에서는 이벤트 기반 비동기 처리를 위해 Kafka를 선택한다.
하지만 우리는 고민 끝에 이 모든 문제를 모두 Redis 기반으로 해결하기로 결정했다.
- 우리 팀은 Kakfa 도입을 어떻게 생각하고 있는가?
- 기술적으로 우리가 Kakfa가 아닌 Redis를 선택한 이유
- Redis를 백엔드에서 어떻게 사용했는지
- 실제로 우리 서비스에서 Redis가 어떻게 사용되었는지
1. 우리 팀은 Kakfa 도입을 어떻게 생각하고 있는가?
검증된 표준 서비스이지만 러닝커브 및 비용 증가
Kafka는 이미 검증된 메시징 플랫폼이고, 대규모 이벤트 스트리밍과 복잡한 데이터 파이프라인을 구성하는 데 있어 사실상 표준에 가깝다.
하지만 직접 서비스를 운영해보니 우리 같이 빠르게 성장해야 하는 팀 입장에서 새로운 기술을 도입할 때는 비용과 러닝 커브를 최소화하여 운영해야 한다.
그리고 우리가 Kafka를 검토하면서 가장 먼저 고민한 것은 기술의 우수성보다는 운영 방식과 팀의 현실적인 부담이었다.
그리고 우리는 관리 비용 뿐만 아니라, 러닝 커브로 인한 개발 속도 저하도 하나의 비용으로 생각하고 있었다.
물론 이번 기회에 Kafka를 도입해보자고 할 수 있지만, 빠르게 개발해야 하는 입장에서 Redis를 배제하고 Kafka를 도입하는 것은 위험 부담이 높다고 판단하였다.
어차피 Redis 도입은 필수다
우리가 해결하려던 문제를 보면, 캐싱은 Kakfa의 영역과는 거리가 멀다.
Kafka는 메시지 스트리밍과 이벤트 처리에 특화된 도구이지, 응답 속도를 줄이기 위한 캐싱이나 인메모리 상태 관리를 담당할 수 있는 시스템은 아니다.
그리고 우리는 Inference에서 이미 Stop 기능을 위해 Redis Pub/Sub을 사용하고 있었다.
어차피 캐싱 문제를 해결하기 위해서는 Redis 사용이 필수이고, 이미 Redis를 사용하고 있는 입장에서 굳이 이벤트 큐와 Pub/Sub 등 Kakfa가 제공하는 기능을 Redis도 모두 제공하는데, Kafka를 추가로 인프라를 구축해서 운영할 필요까지는 없다고 생각했다.
Over-Enginnering 방지
물론 Kafka를 도입하면 분명 더 강력한 이벤트 스트리밍 환경을 만들 수 있다. 하지만 그 강력함이 지금 우리 문제를 해결하는 데 꼭 필요한 수준이었을까?
우리의 이벤트 처리 요구사항은 다음과 같았다.
- 도메인 이벤트 기반의 비동기 처리
- 이메일, 알림, 분석 로그 적재
- 작업 실패 시 재시도 가능
- 높은 처리량보다는 안정성과 단순함이 중요
이 요구사항은 Kafka가 아니어도 Redis 기반 Queue로 충분히 가능하다고 생각했다.
우리가 모든 문제를 Redis로 모두 해결할 수 있다면 인프라 구조를 유지하면서 운영 부담은 없이 문제들을 다룰 수 있을 것이라고 생각했다.
그래서 결론은?
“그냥 지금 쓰고 있는 Redis를 쓰면 되고 충분히 가능한데, 왜 굳이 Kafka를 도입해야 하는 이유가 뭐지?”
위 질문에 모두가 납득할 명확한 Kafka 도입의 Rationale를 찾지 못 했다.
2. 기술적으로 우리가 Kakfa가 아닌 Redis를 선택한 이유
MSA가 아닌, Mono-Repo로 바뀌었다
우리는 AWS 비용 절감 프로젝트의 일환으로 FastAPI(Python)에서 Nest.js(Typescript)로 Migration 하면서 여러 백엔드 서버를 합쳐 하나의 서버(Mono-Repo)로 운영하게 되었다.
- Auth, Backend, IAM, Embedding, RAG 등 여러 서버를 합쳐서 하나의 서버에서 동작하도록 하였다.
- Inference는 Python을 유지하였다. 즉, Backend, Inference 두 개의 서버로 서비스가 운영되도록 바꾸었다.
Mono-Repo 형태로 변경하면서 우리에게 MSA에 대한 요구사항이 없어져 강력한 내결함성, 대규모 스트리밍을 필요로 하지 않았다.
Kafka는 대규모 이벤트 스트리밍을 전제로 설계된 분산 시스템이다.
Kafka는 생산자와 소비자를 느슨하게 분리하면서도 대량의 데이터를 안정적으로, 순서대로, 오래 보관하는 데 있다.
Kafka가 대규모에도 안전한 이유는 하나의 서버가 아니라 클러스터 단위로 동작하기 때문이다.
하나의 클러스터는 여러 개의 브로커(Broker)로 구성되고, 각 브로커는 서로 다른 노드에 배치되어, 장애가 발생해도 전체 시스템이 멈추지 않아 비교적 안전하다.
그리고 Kafka에서 데이터는 Topic과 Partition으로 구성되는데, Partition은 병렬 처리와 확장성을 도와 서로 다른 브로커에 분산 저장될 수 있어서 높은 처리량과 병렬 처리에 특화돼 있고, 좋은 내결함성을 보여준다.
Redis는 In-Memory NoSQL로, 단순한 구조와 빠른 응답 속도에 특화돼 있다.
Redis는 Kafka 처럼 Partition 단위 분산보다는 단순한 복제 구조이기 때문에 빠르지만 Kafka 만큼의 내결함성은 갖추지 못 한다.
MSA에서는 API의 플로우가 여러 서비스에 걸쳐 처리되기 때문에 이벤트로 전달된 이벤트가 제대로 처리되지 않으면 위험하다.
그래서 MSA의 경우에는 대규모 병렬 처리가 가능하고, 내결함성이 좋은 Kafka 사용이 필수라고 생각한다.
Kafka/Redis로 처리할 이벤트의 종류가 MSA에서는 메인 이벤트이지만, 우리의 경우에는 이메일 보내기, 알림 보내기 같은 부가 이벤트였기 때문에, 처리하는 기능에 비해 과한 기능을 갖고 있다고 판단했다.
Pull / Push 구조는 고려 대상이 아니었다
데이터베이스 트랜잭션과 메인 스트림 비즈니스 로직 모두 Service 계층에서 일어나고, 우리가 보낼 이벤트는 모두 데이터베이스 트랙잭션이 끝난 뒤 발생한다.
- 당연한 것인게, 문제로 인해 롤백되는 경우가 있을 수 있으므로, 반드시 트랜잭션이 끝난 뒤 이벤트를 발행해야 한다.
- 즉, 200 응답을 받아도 괜찮은 상황에서 부가 이벤트 처리가 일어나야 한다.
Kafka는 Pull 구조이기 때문에 대기열에 저장해두었다가 클라이언트가 Pull 해서 처리하는 구조이다.
Pull 구조에서는 처리 성공 후 오프셋을 커밋하여 다음 메시지를 읽을 수 있고, 실패 시 커밋을 하지 않아 재시도 또는 다른 경로로 리디렉션이 가능하다.
또한 Kafka Connect API를 사용하면 특정 오류 발생 시 커넥터 작업을 자동으로 다시 시작할 수 있다.
따라서 MSA에서는 성공 여부 / 재시도 처리 등이 매우 매우 중요하므로 Kafka가 필수라고 생각한다.
Redis는 Push 구조이기 때문에 성공 여부에 대해 신경쓰지 않는다.
MSA 구조에서 재고 차감 등 매우 중요한 이벤트가 실패해도 알 방법이 없으므로 이 경우에는 Redis가 적합하지 않다고 생각한다.
하지만 우리는 이벤트가 모두 부가 기능 처리였기 때문에 Pull 구조인 Kafka와 Push 구조인 Redis 모두 처리 가능하다고 판단했다.
데이터는 즉시 삭제되어도 된다
Kafka에서 메시지는 소비 여부와 무관하게 디스크에 로그 형태로 저장된다.
즉, 소비자가 메시지를 읽었다고 해서 Kafka가 해당 메시지를 삭제하지 않는다.
대신 Kafka는 보존 기간(retention time), 로그 크기 등 정책으로 메시지를 삭제한다.
이 덕분에 소비 중 장애가 나거나 데이터가 유실돼도 다시 파티션에서 데이터를 읽어올 수 있다.
Redis의 Pub/Sub에서는 위에서도 언급했지만 Push 구조이기 때문에 메시지가 발행되면 연결된 구독자에게만 전달해버리고 메시지는 즉시 사라진다.
이 경우에는 만약 구독자가 잠시 끊겨 있거나 아직 연결되지 않았다면 메시지는 영원히 유실된다.
물론 처리 되는 게 일반적인 경우지만, 우리는 모두 부가 이벤트이기 때문에 처리가 되지 않아도 치명적인 문제는 발생하지 않기 때문에 메시지를 못 받더라도 큰 문제는 발생하지 않는다.
부가 이벤트이기 때문에 속도나 용량은 고려 대상이 아니다
Kafka와 Redis는 모두 짧은 지연 시간으로 데이터 처리가 가능하다.
굳이 비교하자면, Redis는 주로 RAM에서 데이터를 읽고 쓰기 때문에 속도면에서 Kafka보다 빠르다.
- Redis는 밀리초 단위, Kafka의 메시징 시간은 평균 수십 밀리초 정도이다.
하지만 Redis는 대용량 메시지를 처리하는 작업에는 적합하지 않기 때문에 처리할 때 대용량 메시지의 경우 지연 시간이 매우 짧은 데이터 작업을 계속해서 진행하는 것은 어려울 수 있다.
Kafka는 데이터 지속성을 위해 서로 다른 물리적 드라이브에 파티션을 복제하는 데 시간을 쓰기 때문에 메시지 전송 시간에 오버헤드가 가중된다.
Kafka 최적화를 위해 Kafka 메시지를 압축하여 지연 시간을 줄일 수 있지만 생산자와 소비자는 메시지를 압축 해제하는 데 더 많은 시간이 필요하다.
우리는 이벤트를 받아 외부 서비스로 위임하기 때문에 큰 데이터를 보낼 필요도 없다.
그리고 알림이나 이벤트가 데이터베이스의 상태에 관여하는 것이 아니므로, 즉시 처리될 필요도 없어 속도도 고려할 필요가 없었다.
3. Redis를 백엔드에서 어떻게 사용했는지
우리 백엔드에서 모든 비동기 처리와 상태 변화는 Event Emitter에서 시작되고, 그 이벤트를 Redis 기반 시스템들이 각자의 역할로 소비한다.
즉, 전체 흐름은 아래와 같다.

먼저 트랜잭션에서 모든 로직을 처리하고, 이벤트를 발행한다.
그러면 Event Emitter에서 이벤트를 받아 해당 이벤트를 처리할 수 있는 리스너가 이벤트를 처리한다.
그 이벤트에서 Redis로 실제로 이벤트를 처리한다.
1. Event Emitter
Event Emitter는 같은 프로세스 내에서 아주 가볍게 이벤트를 전달하는 이벤트 버스다.
Event Emitter는 그냥 어떤 이벤트가 일어났는지 알려주는 역할만 하게 된다.
대략적인 구조는 아래와 같다.
이벤트가 발생하면 Event Emitter는 중개자 역할로 등록돼 있는 Listener가 해당 이벤트를 처리하도록 한다.

아래와 같이 서비스 계층에서 이벤트를 발생시킨다.
@Injectable()
export class ChatroomService {
constructor(private readonly eventEmitter: EventEmitter2) {}
async create(data: CreateData): Promise<Chatroom> {
const chatroom = await this.prisma.chatroom.create({ data });
// 채팅방 생성 이벤트를 발생시킨다.
this.eventEmitter.emit(
GlobalEventNames.CHATROOM_CREATED,
new ChatroomCreatedEvent(chatroom.id, data.userId),
);
return chatroom;
}
}이 시점에서 이메일을 보낼지, 알림을 보낼지, 분석 이벤트를 보낼지는 서비스 계층에서는 모른다.
이 구조 덕분에 기능 간 결합도가 급격히 낮아져서 유지보수가 쉬워진다.
이 이벤트를 처리하는 핸들러는 OnEvent 어노테이션으로 처리할 수 있는데, 아래 코드에서는 Queue에 메시지를 추가해 실제 이벤트 처리를 위임하는 것을 볼 수 있다.
@OnEvent(GlobalEventNames.SUBSCRIPTION_COMPLETED)
async handleSubscriptionCompleted(
event: SubscriptionCompletedEvent,
): Promise<void> {
try {
const job = await this.emailCampaignQueue.add(
EmailCampaignJobNames.SEND_TRIAL_REMINDER_EMAIL,
{
userId: event.userId,
email: event.email,
plan: event.plan,
},
{
jobId: `trial-reminder-${event.userId}`,
removeOnComplete: true,
},
);
} catch (error) {
// Don't throw - not to break other event listeners
this.logger.error(`Failed to schedule job:`, error);
}
}2. Queue – Redis Stream 기반 BullMQ
Event Emitter와 Listener를 통해 어떤 일이 발생했다는 사실을 분리했고, 이 일을 어떻게 실행할 것인가 정의해야 한다.
BullMQ는 Redis Stream을 기반으로 한 작업 큐 시스템으로, Event Emitter 이후 실제 실행을 담당한다.
BullMQ를 거쳐 메시지를 처리하는 구조는 아래와 같다.

여기서 Queue는 실제 Redis에 생기게 되고, Worker는 Redis Queue를 직접 polling 하는 구조로 처리한다.
BullMQModule에서 BullMQ 설정을 할 수 있다.
BullModule.forRootAsync({
useFactory: (configService) => ({
connection: {
host: REDIS_HOST,
port: REDIS_PORT,
password: REDIS_PASSWORD,
username: REDIS_USERNAME,
db: REDIS_DB,
},
defaultJobOptions: {
attempts: 3,
backoff: { type: 'exponential', delay: 2000 },
},
}),
});모든 Queue는 동일한 Redis 연결을 사용하게 되어서 개별 모듈이 Redis 설정을 알 필요가 없다.
그리고 Job에 Retry와 Backoff 설정을 자세하게 할 수 있다.
아래와 같이 큐를 등록할 수 있다.
BullModule.registerQueue(
{ name: QueueNames.EMAIL_CAMPAIGN },
{ name: QueueNames.ANALYTICS },
{ name: QueueNames.NOTIFICATIONS }
),그리고 이 큐에 적재된 Job을 실제로 Polling 하여 처리하는 주체가 Worker다.Processor 데코레이터로 특정 큐를 지정하고, WorkerHost를 상속받아 BullMQ Worker를 정의할 수 있다.
@Processor(QueueNames.NOTIFICATIONS)
export class NotificationWorker extends WorkerHost {
@Trace()
async process(job: Job<NotificationJobData>): Promise<void> {
try {
if ('notificationConfigId' in job.data) {
await this.handleEngagementNotification(job.data);
}
} catch (error) {
throw error; // Let BullMQ handle retry logic
}
}
}3. Caching – Redis Basic Usecase
ICacheService를 구현하는 RedisCahceProvider , CacheService 클래스를 만들어 Redis를 간편하게 컨트롤 할 수 있도록 하였다.
아래는 RedisCacheProvider와 CacheService의 일부이다.
// ========================================
// [RedisCacheProvider]
@Injectable()
export class RedisCacheProvider
implements ICacheService, OnModuleInit, OnModuleDestroy
{
public async onModuleInit(): Promise<void> {
await this.initializeRedis();
}
public async onModuleDestroy(): Promise<void> {
const client = this.getClient();
if (client) {
await client.quit();
}
}
// ICacheService implementation
public async get(key: string): Promise<string | null> {
const client = this.getClient();
if (!client) {
return null;
}
try {
return await client.get(key);
} catch (error) {
return null; // eslint-disable-line unicorn/no-null
}
}
// ...
}
// ========================================
// [Cache Service]
@Injectable()
export class CacheService implements ICacheService {
constructor(
@Inject(CACHE_SERVICE_TOKEN)
private readonly cacheProvider: ICacheService,
) {}
async get(key: string): Promise<string | null> {
return this.cacheProvider.get(key);
}
// ...
}Event Handler에서는 CacheService에 정의된 함수로 Redis에 캐싱 데이터를 저장하면 된다.
@OnEvent(GlobalEventNames.CHATROOM_ENTERED)
async handleChatroomEntered(event: ChatroomEnteredEvent): Promise<void> {
// ...
await this.cacheService.set(
keysToSet,
600,
);
}4. 실제로 우리 서비스에서 Redis가 어떻게 사용되었는지
Event Emitter
아래와 같이 src/common/events/global-events.ts에 GlobalEventName타입과 함께 여러 이벤트가 정의되어 있는데, 우리의 경우에는 현재 50개 정도의 이벤트가 정의되어 있다.
export const GlobalEventNames = {
USER_CREATED: 'user.created',
USER_UPDATED: 'user.updated',
USER_DELETED: 'user.deleted',
USER_FOLLOW: 'user.follow',
USER_DEVICE_REGISTERED: 'user.device.registered',
USER_DEVICE_UPDATED: 'user.device.updated',
USER_DEVICE_REMOVED: 'user.device.removed',
CONTENT_CREATED: 'content.created',
CONTENT_UPDATED: 'content.updated',
CONTENT_PUBLISHED: 'content.published',
CONTENT_REJECTED: 'content.rejected',
CONTENT_FAILED: 'content.failed',
// ...
} as const;
export type GlobalEventName =
(typeof GlobalEventNames)[keyof typeof GlobalEventNames];
Queue
src/providers/queue/constants/queue.ts에 정의되어 있다.
export const QueueNames = {
EMAIL_CAMPAIGN: 'email-campaign',
ANALYTICS: 'analytics',
NOTIFICATIONS: 'notifications',
} as const;EMAIL_CAMPAIGN은 회원가입 시, 결제 시, 며칠 안 들어왔을 때, 새로운 기능이 나왔을 때 등등 여러 케이스에서 이메일을 보내고 있다.
내부적으로는 AWS SES로 처리하고 있다.
ANALYTICS는 Appsflyer, Mixpanel 관련 트래킹 관련 이벤트들을 처리하고 있다.
NOTIFICATIONS는 캐릭터에 댓글 / 좋아요 등이 달렸을 때, 새로운 기능이 달렸을 때, Retention을 위한 주기적 알림 등을 처리하고 있다.
내부적으로는 Firebase Messaging 으로 처리하고 있다.
Pub/Sub
Inference Stop 기능에 활용하고 있다.
Inference Stop 기능은 토큰이 나오던 중 중지(Stop) 버튼을 눌렀을 때 토큰 Output을 멈추는 기능이다.
Caching
[기능 1] Someone is Chatting…
- 사용자가 채팅방에 진입하면
CHATROOM_ENTERED이벤트를 발생시킨다. status:character:active:{characterId}을 Key로 하여 Redis에 저장한다. (TTL은 30분)- Browsing 같은 캐릭터 조회 서비스에서 Batch Find로 캐릭터들의 활성화 여부를 판단한다.
- 30분 후에는 Redis가 자동으로 해당 키를 삭제하여, 비활성화 상태로 바뀐다.
- Cron Job으로 1시간마다 Redis를 조회하여 Trending Score를 업데이트한다.
[기능 2] Trending Score
- 사용자가 콘텐츠에 좋아요, 저장, 조회 등의 활동을 수행하면 정보들을 조합하여 해시값을 생성하여 저장한다. (TTL은 일주일)
- 후에 같은 행동을 했을 때 존재하면 이미 기록된 활동이므로 처리를 중단하고 Trending Feedback을 넣지 않는다.
- 중복 Trending Scoring을 방지한다.
- 최신 콘텐츠인 경우 더 위에 뜰 수 있도록 추가 보너스 점수를 적용한다.
[기능 3] Rate Limiting
특정 API 엔드포인트에 대한 사용자별 요청 횟수를 제한하여 남용을 방지한다.
- 사용자가 업로드 API를 호출 시
UploadRateLimitGuard가 요청을 가로챈다. - 사용자 ID를 기반으로 고유한 캐시 키를 생성합니다.
- 예시 :
limit:upload:{userId}
- Redis에서 해당 키의 현재 값을 조회하여 제한을 초과한 경우
429 Too Many Requests에러를 반환한다.
[기능 4] Browsing Optimization
- 브라우징 요성 시 요청 파라미터와 사용자 정보를 조합하여 고유한 캐시 키를 생성합니다.
- 예시:
count:character:popular:category:{categoryId}:rating:{rating}:user:{userId} - 동일한 필터 조건은 항상 같은 키를 생성하도록 한다.
- (Look-aside 전략) Redis에서 해당 키의 값을 조회하고, 키가 존재하고 값이 있으면 바로 반환한다. 없으면 캐시 미스로 Redis에 저장하고 반환한다.
- 12시간 후 Redis가 자동으로 캐시를 삭제한다.
마무리
Kafka는 분명 훌륭한 기술이다.
대규모 이벤트 스트리밍, 높은 내결함성, 장기 로그 보관, 복잡한 데이터 파이프라인을 다뤄야 하는 환경에서는 Kafka를 반드시 사용해야 한다고 생각한다.
하지만 우리는 도입을 앞두고 아래 사항을 고민했다.
- 지금 우리가 겪고 있는 문제의 본질은 무엇인가?
- 이 문제를 해결하는 데 Kafka의 모든 장점이 정말 필요한가?
- 도입 비용, 운영 복잡도, 개발 속도까지 포함해서 봤을 때 가장 합리적인 선택은 무엇인가?
우리의 이벤트는 대부분 부가 이벤트였고, 이 이벤트들이 실패했다고 해서 결제 정합성이 깨지지는 않는다.
또한, 빠르게 분리할 수 있어야 했고 API 응답에 영향을 주지 않아야 했으며 우리가 필요로 하는 기능을 모두 갖춘 것이 무엇인가 생각해 보았을 때 Redis라는 선택지가 더 매력적이고 합리적으로 보였다.
이 프로젝트에서 무엇보다 중요한 건, 지금 우리 서비스 구조와 팀 상황에서 가장 잘 맞는 도구를 선택했다는 것이다.
하지만 미래에 서비스를 MSA로 철저히 분리해야 해야 한다면 그때는 Kafka를 도입하는 것이 당연한 선택이 될 것이다.
앞으로 상황이 바뀌면 선택도 바뀔 수 있다. 무조건 대세인 기술을 따라가기 보다는 상황에 따라 유연하게 결정할 수 있어야 하고, 그게 건강한 기술 선택이라고 생각한다.


댓글 남기기