보내는 쪽과 받는 쪽을 비동기로 떼어놓아 결합도를 낮춥니다. 상대가 느리거나 잠시 죽어도 요청이 큐에 남습니다.
메시지 큐 기초
왜 메시지 큐가 필요한가?
- 비동기 처리: 이메일 발송, 알림 등 즉시 응답 불필요한 작업
- 디커플링: 서비스 간 직접 의존 제거
- 버퍼링: 트래픽 급증 시 요청을 큐에 저장 후 처리
주요 메시지 브로커
| 브로커 | 특징 | 적합 사례 |
|---|
| Kafka | 고처리량, 로그 기반 | 이벤트 스트리밍, 로그 수집 |
| RabbitMQ | 유연한 라우팅, AMQP | 작업 큐, RPC |
| SQS | AWS 관리형 | 간단한 비동기 처리 |
큐를 넣으면 새로 생기는 것
받는 쪽과 보내는 쪽을 떼어 놓는 대신 네 가지를 감수합니다.
같은 메시지를 두 번 받을 수 있습니다. 대부분의 큐는 적어도 한 번 전달을 약속하고 정확히
한 번은 약속하지 않습니다. 넘긴 뒤 확인이 유실되면 다시 보냅니다. 그래서 받는 쪽이 두 번
처리해도 결과가 같도록 만들어야 합니다.
순서가 보장되지 않습니다. 나눠서 처리하는 순간 도착 순서가 보낸 순서와 달라집니다.
순서가 필요하면 같은 대상의 메시지를 한 줄로 모아야 하고, 그러면 그 줄만큼만 병렬이 됩니다.
실패한 메시지가 갈 곳이 필요합니다. 계속 실패하는 하나가 줄 앞을 막으면 뒤가 전부
멈춥니다. 정해진 횟수를 넘기면 따로 빼 두고, 그 자리를 사람이 봐야 합니다.
큐 자체가 장애 지점이 됩니다. 큐가 죽으면 보내는 쪽이 무엇을 해야 하는지 정해 두어야
합니다. 요청을 거절할지, 일단 받아 두고 나중에 넣을지의 선택입니다.
언제 넣지 않는가
| 상황 | 왜 |
|---|
| 결과를 바로 돌려줘야 한다 | 큐는 언제 처리될지 약속하지 않는다 |
| 처리량이 부족하다 | 큐는 몰림을 흡수하고 처리량을 늘리지 않는다 |
| 흐름이 단순하고 그대로일 것이다 | 직접 부르는 것이 읽기 쉽다 |
두 번째가 가장 흔한 오해입니다. 평균 유입이 처리 속도를 넘으면 큐 길이는 무한히 자랍니다.
큐를 크게 잡는 것은 시간을 버는 것이고 문제를 없애는 것이 아닙니다.
실무 포인트
- At-least-once vs Exactly-once: 대부분 At-least-once → 멱등성 보장 필요
- 메시지 순서 보장이 필요한가? → Kafka 파티션 키 활용
- DLQ(Dead Letter Queue)로 실패 메시지 관리
Q.메시지 큐를 왜 쓰나요?
호출하는 쪽과 처리하는 쪽을 시간적으로 떼어내기 위해서입니다.
| 얻는 것 | 내용 |
|---|
| 응답 시간 단축 | 즉시 필요 없는 일을 뒤로 넘긴다. 이메일, 썸네일 생성 |
| 부하 완충 | 갑작스러운 유입을 큐가 받아 두고 소비자가 자기 속도로 처리한다 |
| 장애 격리 | 소비자가 죽어도 메시지는 남는다 |
| 결합도 감소 | 발행자가 소비자를 몰라도 된다 |
| 확장 | 소비자를 늘려 처리량을 올린다 |
| 재처리 | 실패한 작업을 다시 처리할 수 있다 |
두 번째가 큐의 고유한 값입니다. 초당 1만 건이 몰려도 소비자가 초당 1천 건만 처리할 수 있다면, 동기 호출로는 실패하지만 큐가 있으면 시간을 늘려 전부 처리합니다.
세 번째도 중요합니다. 동기 호출에서 상대가 죽으면 그 요청은 사라지지만, 큐에 넣었으면 상대가 복구된 뒤 처리됩니다.
흔한 실수: 성능을 위해 쓴다고만 답하는 것. 전체 처리 시간은 오히려 늘어납니다. 짧아지는 것은 사용자가 기다리는 시간입니다. 그리고 그 대가로 즉시 결과를 알 수 없게 됩니다.
Q.메시지 큐를 쓰면 안 되는 경우는 언제인가요?
| 상황 | 이유 |
|---|
| 결과를 즉시 알려줘야 한다 | 로그인 검증, 잔액 조회. 비동기로 만들 수 없다 |
| 순서가 엄격히 중요하다 | 큐는 파티션 단위로만 순서를 보장한다 |
| 호출 한 번으로 끝난다 | 큐를 두면 운영할 것만 늘어난다 |
| 강한 트랜잭션이 필요하다 | DB 와 큐를 원자적으로 묶을 수 없다 |
| 지연을 감당할 수 없다 | 큐를 거치면 최소 수십 밀리초가 더 든다 |
세 번째가 실무에서 가장 흔한 오용입니다. 서비스 두 개 사이의 단순한 조회에 큐를 두면 응답을 되받기 위한 구조를 또 만들어야 하고, 그것은 그냥 동기 호출보다 복잡합니다.
큐를 도입하면 함께 따라오는 것들도 있습니다.
| 따라오는 비용 | 내용 |
|---|
| 중복 처리 대비 | 멱등성을 만들어야 한다 |
| 실패 처리 | 재시도와 실패 큐를 설계해야 한다 |
| 관측 | 트레이스가 끊기고 지연을 따로 재야 한다 |
| 운영 | 브로커 자체가 장애 지점이 된다 |
흔한 실수: 결합도를 낮추려고 모든 통신을 큐로 바꾸는 것. 흐름이 보이지 않아 장애 때 어디까지 진행됐는지 알기 어려워집니다.
Q.메시지 유실을 어떻게 방지하나요?
유실될 수 있는 구간이 세 곳입니다. 각각을 막아야 합니다.
| 구간 | 유실 상황 | 대응 |
|---|
| 발행 | 브로커에 도착 전에 실패 | 저장 확인 응답을 기다린다. 재시도한다 |
| 브로커 | 저장 전에 노드가 죽는다 | 복제본을 두고 과반 저장을 확인한다 |
| 소비 | 처리 전에 완료 표시를 한다 | 처리 성공 후에 표시한다 |
세 번째가 가장 흔한 원인입니다. 메시지를 받자마자 완료로 표시하고 처리 중에 죽으면 그 메시지는 사라집니다. 처리가 끝난 뒤 표시해야 합니다.
첫 번째에서는 DB 쓰기와 발행을 묶는 문제가 함께 옵니다. 커밋과 발행 사이에 죽으면 데이터는 있고 이벤트는 없으므로, 이벤트도 같은 트랜잭션에 저장해 두고 별도 프로세스가 발행하는 구조를 씁니다.
| 설정에서 볼 것 | 내용 |
|---|
| 발행 확인 수준 | 리더만 받았는지, 복제본까지 받았는지 |
| 복제 수와 최소 동기 복제 수 | 하나가 죽어도 남는지 |
| 소비자 완료 표시 시점 | 자동 표시를 끄고 처리 후 수동으로 |
흔한 실수: 유실을 0으로 만들려는 것. 대가는 지연과 처리량입니다. 어떤 데이터는 유실을 감수하는 편이 합리적이고, 그 판단이 먼저입니다.
Q.컨슈머 랙이 계속 쌓이면 어떻게 대응하나요?
쌓인다는 것은 처리 속도가 유입 속도보다 느리다는 뜻입니다. 원인을 나눠 봅니다.
| 확인 | 내용 |
|---|
| 유입이 늘었는가 | 발행량 지표를 본다 |
| 처리가 느려졌는가 | 메시지당 처리 시간을 본다 |
| 소비자가 죽었는가 | 인스턴스 수와 재시작 이력 |
| 특정 파티션만 쌓이는가 | 키가 한쪽에 몰렸거나 그 소비자만 문제 |
| 재시도가 반복되는가 | 처리 못 하는 메시지가 앞을 막고 있다 |
네 번째와 다섯 번째가 자주 놓치는 원인입니다. 전체 랙만 보면 원인이 안 보입니다.
| 대응 | 내용 |
|---|
| 소비자 증설 | 파티션 수까지만 효과가 있다 |
| 파티션 증설 | 그 위로 늘리려면 필요하다 |
| 처리 최적화 | 건별 DB 접근을 묶음 처리로 |
| 막는 메시지 격리 | 실패가 반복되면 실패 큐로 보내고 넘어간다 |
| 발행 조절 | 급하지 않은 발행을 줄인다 |
첫 번째의 한계가 중요합니다. 파티션 수가 소비자 수의 상한이라, 파티션 4개에 소비자 10대를 붙이면 6대는 아무것도 하지 않습니다.
흔한 실수: 랙 숫자만 보고 판단하는 것. 건수보다 따라잡는 데 걸릴 시간이 실제 지표입니다. 100만 건이 쌓였지만 처리 속도가 유입의 두 배면 곧 사라지고, 1만 건이지만 처리가 더 느리면 계속 늘어납니다.
먼저 스스로 답해보고 아래 답변과 견줘보세요. 막히는 부분은 문제로 확인할 수 있어요.
읽었으면 문제로 확인해보세요
메시징 문제를 풀면 틀린 문제가 자동으로 노트에 쌓입니다. 가입 없이 5문제를 먼저 풀어볼 수도 있어요.