이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
공부 › 아키텍처와 운영 › 메시지와 비동기
메시지 큐 기본
직접 부르지 않고 쪽지를 남긴다
주문이 들어오면 확인 메일을 보내야 합니다. 주문 서버가 메일 서버를 직접 호출해 끝날 때까지 기다릴 수도 있고, "메일 보내 주세요"라는 메시지를 큐에 넣어 두고 바로 응답할 수도 있습니다.
개념 모형 · 개념 모형(실제 메시지 큐로 확인하지 않음)
이 실습에 나오는 개념
- 메시지 큐
- 보내는 쪽(생산자)과 받는 쪽(소비자) 사이에서 메시지를 보관했다가 전달하는 중간 저장소다. 생산자는 메시지를 넣고 바로 다음 일을 하며, 소비자는 자신의 속도로 꺼내 처리한다. 두 시스템이 서로를 직접 호출하지 않아 결합이 낮아지고, 순간적으로 몰린 요청을 쌓아 두었다가 처리할 수 있다.
- 동기와 비동기
- 동기는 작업을 요청한 쪽이 결과를 받을 때까지 기다린 뒤 다음으로 진행하는 방식이다. 비동기는 작업을 요청해 두고 결과를 기다리지 않고 다음 일을 진행하며, 결과는 나중에 콜백이나 Future 등으로 전달받는 방식이다.
- Pub/Sub
- 발행자가 특정 주제(채널)에 메시지를 보내면 그 주제를 구독하고 있는 모든 구독자가 메시지를 받는 방식이다. 발행자와 구독자는 서로를 알 필요가 없다. Redis의 Pub/Sub은 메시지를 저장하지 않아 그 순간 연결되어 있지 않은 구독자는 메시지를 받지 못한다.
- 메시지 전달 보장
- 메시지가 몇 번 전달되는지에 대한 보장 수준이다. at-most-once는 최대 한 번 전달되어 중복은 없지만 유실될 수 있다. at-least-once는 최소 한 번 전달되어 유실은 없지만 중복될 수 있다. exactly-once는 정확히 한 번 전달된다. 실무에서는 at-least-once로 두고 소비자가 중복을 처리(멱등)하도록 만드는 경우가 많다.
- 최종 일관성(Eventual Consistency)
- 데이터를 변경한 직후에는 일부 노드가 이전 값을 보여 줄 수 있지만, 추가 변경이 없으면 시간이 지나 모든 노드가 같은 값을 갖게 된다는 보장이다. 즉시 일관성을 포기하는 대신 가용성과 확장성을 얻는다. 분산 데이터베이스와 마이크로서비스 간 데이터 동기화에서 흔히 채택된다.
이렇게 바꿔 보세요
- 처음 설정으로 — 직접 호출. 주문 응답이 메일 발송 시간만큼 늦어집니다.
- 큐에 넣으면 — 주문은 바로 응답하고 메일은 뒤에서 차례로 나갑니다.
- 메일 서버가 죽었을 때 직접 호출 — 메일 때문에 주문까지 실패합니다.
- 메일 서버가 죽었을 때 큐 — 주문은 성공하고 메시지는 쌓였다가 복구 뒤 처리됩니다.
- 메일이 아주 느릴 때 — 3초짜리 작업. 두 방식의 주문 응답 시간을 비교해 보세요.
처음 설정으로 실행하면
- 주문 3건이 차례로 들어옵니다. 주문을 DB에 저장하는 데 0.05초, 확인 메일 한 통을 보내는 데 1초가 걸립니다.
- 주문 1: 저장(0.05초)한 뒤 메일 서버를 호출하고 끝날 때까지 기다립니다(1초). 사용자는 1.05초 뒤에야 응답을 받습니다.
- 주문 2: 저장(0.05초)한 뒤 메일 서버를 호출하고 끝날 때까지 기다립니다(1초). 사용자는 1.05초 뒤에야 응답을 받습니다.
- 주문 3: 저장(0.05초)한 뒤 메일 서버를 호출하고 끝날 때까지 기다립니다(1초). 사용자는 1.05초 뒤에야 응답을 받습니다.
실행 결과
- 직접 호출하면 응답 시간에 상대의 처리 시간이 그대로 더해집니다. 메일이 느려지면 주문도 느려집니다. 대신 응답을 받은 시점에 메일까지 끝났다는 것이 확실합니다.
- 큐가 어울리는 일은 "응답에 결과가 필요 없고 조금 늦어도 되는 일"입니다. 메일, 알림, 통계 집계가 대표적입니다. 결제 승인처럼 결과를 받아야 다음으로 갈 수 있는 일은 직접 호출합니다.
코드
@PostMapping("/orders")
OrderDto order(@RequestBody OrderRequest req) {
Order order = orderService.save(req); // 0.05초
mailClient.sendConfirm(order); // 메일 서버를 직접 호출하고 기다린다
return OrderDto.of(order); // 메일이 끝나야 응답
}
이 모형의 전제
개념을 설명하기 위한 모델이며 실제 메시지 큐를 실행해 확인한 결과는 아닙니다. 시간 값(저장 0.05초, 큐에 넣기 0.01초, 타임아웃 3초)은 예시입니다. 직접 호출에서도 타임아웃과 예외 처리를 넣어 주문 실패를 막을 수 있고, 큐 자체가 죽는 경우와 메시지 유실, 중복 전달, 순서 문제는 여기서 다루지 않았습니다. 코드의 @RabbitListener는 RabbitMQ를 쓸 때의 예시입니다.
앞 실습: 요청 수 제한 · 다음 실습: Kafka 파티션과 컨슈머