동기 호출이 감당 못 하는 지점에서 큐를 쓴다
동기 호출이 무너지는 지점
사용자가 회원가입을 하면 서버는 보통 이런 일을 순서대로 합니다. DB에 계정을 만들고, 환영 이메일을 보내고, 추천 알고리즘을 위한 초기 프로필을 생성하고, 분석 시스템에 이벤트를 기록합니다. 이 네 가지를 전부 동기로, 한 요청 안에서 처리하면 어떻게 될까요. 이메일 발송 API가 3초간 응답이 없으면 사용자는 회원가입 버튼을 누르고 3초를 그냥 기다립니다. 더 나쁜 경우, 이메일 발송 서비스가 아예 장애가 나면 계정 생성 자체가 실패합니다. 회원가입이 이메일 발송 서비스의 가용성에 발목 잡히는 겁니다.
이 네 가지 작업 중 사용자가 즉시 결과를 알아야 하는 건 "계정이 만들어졌다"는 것뿐입니다. 나머지 세 가지는 몇 초, 심지어 몇 분 늦어져도 사용자는 알아채지 못합니다. 이게 메시지 큐를 쓰는 이유의 핵심입니다. 즉시 필요한 응답과, 나중에 처리해도 되는 부수 작업을 분리하는 것입니다.
큐를 넣으면 무엇이 바뀌는가
sequenceDiagram participant U as 사용자 participant S as 서버 participant Q as 메시지 큐 participant W as 워커 U->>S: 회원가입 요청 S->>S: DB에 계정 생성 S->>Q: "가입 완료" 이벤트 발행 S-->>U: 즉시 응답 (성공) Q->>W: 이벤트 전달 W->>W: 이메일 발송, 프로필 초기화, 분석 기록
서버는 큐에 메시지를 넣는 순간 사용자에게 응답할 수 있습니다. 이메일 발송이 느리든, 분석 시스템이 잠깐 죽어있든 사용자 경험에는 영향이 없습니다. 워커는 자기 속도대로 큐에서 메시지를 꺼내 처리합니다. 트래픽이 몰려 워커가 밀리면 큐에 메시지가 쌓일 뿐, 서버가 느려지거나 요청이 실패하지 않습니다. 이 "쌓이는 것으로 버틴다"는 성질이 큐의 핵심 가치입니다.
큐를 쓰면 얻는 것과 반드시 잃는 것
메시지 큐를 도입하는 순간 시스템은 정합성 모델이 바뀝니다. 동기 호출에서는 "계정 생성과 이메일 발송이 둘 다 성공했다"를 하나의 트랜잭션처럼 (완벽하진 않아도) 취급할 수 있었습니다. 큐를 넣으면 계정 생성은 이미 끝났는데 이메일 발송은 아직 큐 어딘가에 대기 중인 상태가 정상적으로 존재합니다. 이걸 최종 일관성(eventual consistency)이라고 부르는데, 이름이 편해 보이지만 실제로는 "지금 이 순간 무엇이 아직 처리되지 않았는지 알 수 없다"는 불편함을 감수하는 겁니다.
또 하나, 큐는 메시지를 최소 한 번(at-least-once) 전달하는 경우가 흔합니다. 워커가 메시지를 처리한 직후, 큐에 "처리 완료"를 알리기 전에 죽으면 같은 메시지가 다시 전달됩니다. 그러면 환영 이메일이 두 번 발송될 수 있습니다. 이 문제를 막으려면 워커 쪽 로직이 멱등적이어야 합니다 — 같은 메시지를 두 번 처리해도 결과가 같아야 합니다.
def handle_signup_event(event):
# 멱등 키로 이미 처리한 이벤트인지 먼저 확인
if redis.sismember("processed_events", event.id):
return # 이미 처리됨, 조용히 종료
send_welcome_email(event.user_id)
init_recommendation_profile(event.user_id)
log_analytics_event(event)
redis.sadd("processed_events", event.id)이 패턴 — 처리 전에 이미 처리했는지 확인하는 것 — 은 큐를 쓰는 시스템이라면 거의 필수로 등장합니다. 인터뷰에서 "큐를 쓰겠습니다"까지만 말하고 멱등성을 언급하지 않으면, 십중팔구 면접관이 "중복 메시지는 어떻게 처리하나요"라고 되묻습니다.
큐 vs 발행-구독, 그리고 언제 쓰지 않는가
큐(작업 큐)와 발행-구독(pub-sub)은 자주 헷갈립니다. 작업 큐는 메시지 하나를 여러 워커 중 하나가 가져가 처리합니다 — 작업을 분산시키는 것이 목적입니다. 발행-구독은 메시지 하나를 구독 중인 모든 소비자가 각자 받습니다 — 이벤트를 여러 시스템에 알리는 것이 목적입니다. 회원가입 이벤트를 이메일 시스템과 분석 시스템이 각각 독립적으로 받아야 한다면 발행-구독에 가깝고, 이미지 리사이징처럼 작업 하나를 여러 워커 중 하나가 나눠 맡으면 되는 경우는 작업 큐에 가깝습니다. Kafka는 이 둘의 성격을 함께 가진 도구로 자주 언급되지만, 인터뷰에서는 "정확히 어떤 도구"보다 "왜 이 통신 방식이 필요한가"를 설명하는 게 더 중요합니다.
큐가 만능은 아닙니다. 사용자가 결과를 즉시 알아야 하는 작업 — 로그인 성공 여부, 결제 승인 결과 — 을 큐에 넣고 "나중에 처리됩니다"라고 하면 그건 시스템 설계가 아니라 사용자 경험을 망가뜨리는 결정입니다. 큐는 "지금 몰라도 되는 것"에만 씁니다. 이 경계선을 명확히 긋는 것이, 어디에나 큐를 끼워 넣는 것보다 훨씬 좋은 답변입니다.