
안녕하세요, QueryDaily 팀입니다.
"회원가입할 때 이메일 발송이 왜 이렇게 오래 걸리죠?"
혹시 이런 경험 해보신 적 있으신가요? 회원가입 버튼을 눌렀는데 5초, 10초씩 로딩이 돌아가는 상황. 알고 보니 이메일 발송, 알림톡 전송, 포인트 지급까지 모든 작업이 동기적으로 처리되고 있었던 거죠.
이런 문제를 해결하기 위해 등장한 것이 바로 메시지 큐(Message Queue)와 비동기 처리입니다.
면접에서도 단골 주제입니다. "메시지 큐 써보셨어요?"라는 질문 뒤에는 어김없이 꼬리 질문이 따라옵니다.
"왜 메시지 큐를 썼나요?"
"메시지가 유실되면 어떻게 하죠?"
"Kafka와 RabbitMQ의 차이는요?"
오늘은 메시지 큐가 왜 필요한지, 어떻게 동작하는지, 그리고 면접에서 자주 나오는 핵심 질문까지 완벽하게 정리해 드리겠습니다.

먼저 동기와 비동기의 차이를 명확히 이해해야 합니다.
@Transactional
public void signUp(SignUpRequest request) {
// 1. 회원 저장 (50ms)
User user = userRepository.save(new User(request));
// 2. 환영 이메일 발송 (3000ms) - 외부 API 호출
emailService.sendWelcomeEmail(user.getEmail());
// 3. 알림톡 발송 (2000ms) - 외부 API 호출
notificationService.sendKakaoNotification(user.getPhone());
// 4. 가입 축하 포인트 지급 (100ms)
pointService.giveSignUpBonus(user.getId());
// 총 소요 시간: 약 5150ms (5초 이상!)
}
사용자는 회원가입 버튼을 누르고 5초 이상 대기해야 합니다. 이메일 서버가 느리거나 장애가 나면? 회원가입 자체가 실패합니다.

@Transactional
public void signUp(SignUpRequest request) {
// 1. 회원 저장 (50ms) - 핵심 로직만 동기 처리
User user = userRepository.save(new User(request));
// 2. 나머지는 메시지 큐에 발행 (5ms)
messageQueue.publish(new UserSignedUpEvent(user.getId()));
// 총 소요 시간: 약 55ms
}
// 별도의 Consumer가 비동기로 처리
@EventListener
public void handleUserSignedUp(UserSignedUpEvent event) {
emailService.sendWelcomeEmail(event.getUserId());
notificationService.sendKakaoNotification(event.getUserId());
pointService.giveSignUpBonus(event.getUserId());
}
사용자는 55ms 만에 응답을 받습니다. 이메일 발송이 실패해도 회원가입은 정상 처리됩니다.
메시지 큐는 Producer(생산자)와 Consumer(소비자) 사이에서 메시지를 임시로 저장하고 전달하는 중간 저장소입니다.
[Producer] --> [Message Queue] --> [Consumer]
생산자 중간 저장소 소비자
1. 비동기 통신
Producer는 메시지를 큐에 넣고 바로 다음 작업을 수행합니다. Consumer가 언제 처리하든 상관없습니다.
2. 디커플링 (Decoupling)
Producer와 Consumer가 서로를 직접 알 필요가 없습니다. 서비스 간 의존성이 낮아집니다.
3. 버퍼링
트래픽이 급증해도 큐가 메시지를 쌓아두고, Consumer가 자신의 처리 속도에 맞춰 소비합니다.
4. 내구성 (Durability)
메시지를 디스크에 저장하여 시스템 장애 시에도 유실을 방지합니다.
[Before] 회원가입 API: 5000ms
[After] 회원가입 API: 55ms
사용자 경험이 극적으로 개선됩니다.
// Before: 강한 결합
public void signUp() {
userService.save();
emailService.send(); // EmailService 장애 = 회원가입 장애
notificationService.send(); // NotificationService 장애 = 회원가입 장애
}
// After: 느슨한 결합
public void signUp() {
userService.save();
messageQueue.publish(event); // 다른 서비스 장애와 무관
}
초당 요청 수:
[Before] API 서버가 직접 처리 -> 1000 req/s 넘으면 서버 다운
[After]
- 10,000 req/s가 와도 큐에 쌓임
- Consumer가 500 req/s 속도로 안정적 처리
- 서버는 죽지 않고, 처리가 조금 늦어질 뿐
메시지 처리 실패 시 재시도할 수 있습니다. Dead Letter Queue(DLQ)에 실패한 메시지를 모아 나중에 분석하거나 재처리할 수 있습니다.
특징:
- AMQP 프로토콜 기반
- 다양한 라우팅 패턴 지원 (Direct, Topic, Fanout)
- 메시지 단위 ACK/NACK
- 상대적으로 낮은 처리량, 높은 기능성
적합한 상황:
- 복잡한 라우팅이 필요한 경우
- 메시지 단위 확인 응답이 중요한 경우
- 트랜잭션 지원이 필요한 경우
특징:
- 분산 로그 기반 아키텍처
- 매우 높은 처리량 (초당 수백만 메시지)
- 메시지 영속성 (디스크에 저장, 일정 기간 보관)
- Consumer Group을 통한 확장성
적합한 상황:
- 대용량 데이터 스트리밍
- 이벤트 소싱, 로그 수집
- 실시간 데이터 파이프라인
특징:
- 완전 관리형 서비스 (인프라 관리 불필요)
- 무제한 확장
- 표준 큐 / FIFO 큐 선택 가능
적합한 상황:
- AWS 환경에서 빠른 도입이 필요한 경우
- 인프라 관리 리소스가 부족한 경우
| 항목 | RabbitMQ | Kafka | AWS SQS |
|---|---|---|---|
| 처리량 | 중간 | 매우 높음 | 높음 |
| 메시지 보관 | 소비 후 삭제 | 일정 기간 보관 | 소비 후 삭제 |
| 순서 보장 | 가능 | 파티션 내 보장 | FIFO 큐만 가능 |
| 라우팅 | 다양함 | 단순함 | 단순함 |
| 운영 부담 | 중간 | 높음 | 없음 |
// Producer: 발행 확인
rabbitTemplate.convertAndSend(exchange, routingKey, message,
new CorrelationData(UUID.randomUUID().toString()));
// Consumer: 수동 ACK
@RabbitListener(queues = "order.queue", ackMode = "MANUAL")
public void handleOrder(Order order, Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) long tag) {
try {
processOrder(order);
channel.basicAck(tag, false); // 처리 성공 시 ACK
} catch (Exception e) {
channel.basicNack(tag, false, true); // 실패 시 재큐잉
}
}
네트워크 문제로 같은 메시지가 중복 전달될 수 있습니다.
public void processPayment(PaymentMessage message) {
// 이미 처리된 메시지인지 확인
if (paymentRepository.existsByMessageId(message.getMessageId())) {
log.info("이미 처리된 메시지: {}", message.getMessageId());
return;
}
// 결제 처리
Payment payment = paymentService.process(message);
// 처리 완료 기록
paymentRepository.save(payment);
}
Kafka의 경우 같은 키를 가진 메시지는 같은 파티션으로 보내 순서를 보장합니다.
// 같은 userId를 가진 메시지는 항상 같은 파티션으로
kafkaTemplate.send("user-events", userId.toString(), event);
처리 실패한 메시지를 별도 큐에 보관합니다.
# RabbitMQ 설정 예시
x-dead-letter-exchange: dlx.exchange
x-dead-letter-routing-key: dlq.routing.key
x-message-ttl: 60000 # 1분 후 DLQ로 이동
Q1. "메시지 큐를 왜 사용하나요?"
핵심 답변: 크게 세 가지 이유가 있습니다. 첫째, 비동기 처리로 응답 시간을 개선합니다. 둘째, 시스템 간 결합도를 낮춰 장애 전파를 막습니다. 셋째, 트래픽 급증 시 버퍼 역할을 하여 시스템 안정성을 높입니다.
Q2. "Kafka와 RabbitMQ의 차이는?"
핵심 답변: Kafka는 분산 로그 기반으로 대용량 데이터 스트리밍에 적합하고, 메시지를 일정 기간 보관합니다. RabbitMQ는 AMQP 기반으로 복잡한 라우팅과 메시지 단위 확인 응답이 필요한 경우에 적합합니다. 처리량은 Kafka가 월등히 높고, 기능 다양성은 RabbitMQ가 우세합니다.
Q3. "메시지가 유실되면 어떻게 하나요?"
핵심 답변: 여러 계층에서 방지합니다. Producer 측에서는 발행 확인(Confirm)을 사용하고, 브로커는 메시지를 디스크에 영속화합니다. Consumer는 처리 완료 후에만 ACK를 보내고, 실패 시 재시도하거나 DLQ로 보내 나중에 처리합니다.
Q4. "같은 메시지가 두 번 처리되면 어떻게 되나요?"
핵심 답변: 이를 방지하기 위해 Consumer를 멱등하게 설계합니다. 메시지에 고유 ID를 부여하고, 처리 전에 이미 처리된 ID인지 확인합니다. 또는 데이터베이스의 unique constraint를 활용하여 중복 처리를 원천 차단합니다.
Q5. "메시지 큐 도입 시 단점은 없나요?"
핵심 답변: 있습니다. 시스템 복잡도가 증가하고, 메시지 큐 자체가 SPOF(Single Point of Failure)가 될 수 있어 고가용성 구성이 필요합니다. 또한 디버깅이 어려워지고, 트랜잭션 경계가 모호해져 데이터 정합성 관리가 복잡해집니다. 최종적 일관성(Eventual Consistency)을 수용해야 합니다.
오늘 배운 내용을 정리하면 이렇습니다:
메시지 큐는 단순히 "비동기로 처리하고 싶어서" 도입하는 기술이 아닙니다. 시스템의 복잡도, 운영 비용, 데이터 정합성 등 다양한 트레이드오프를 고려해야 합니다. 면접관은 바로 이러한 깊이 있는 이해를 확인하고자 합니다.
면접은 내가 아는 것을 자랑하는 자리가 아니라, 면접관의 질문 의도를 파악하고 논리적으로 설명하는 자리입니다. 이력서에 적힌 'Kafka 사용 경험' 한 줄에서 시작될 수 있는 수많은 꼬리 질문들을 QueryDaily와 함께 미리 대비해 보세요.
tags: MessageQueue Kafka RabbitMQ 비동기 백엔드면접