[소프트웨어 개발자 면접] 시스템 디자인 - 큐 대기열 Queue
대규모 분산 시스템에서 응답성과 내결함성을 동시에 끌어올리는 큐를 다룹니다. 멱등 소비자·순서 보장·백프레셔·데드레터 큐를 실전 면접 각도에서 정리합니다.
제가 분산 환경에서 큐를 처음 다뤄 봤을 때
저는 처음에 큐를 “데이터를 잠깐 담아 두는 통” 정도로만 이해했습니다. 그래서 분산 시스템에서 처리 순서를 보장해야 한다는 요구를 받았을 때, 단순한 FIFO 큐를 그대로 떠올렸고, 그대로 제안했었습니다. 그런데 다중 인스턴스 환경에서 같은 메시지를 두 컨슈머가 동시에 꺼내가는 바람에 같은 작업이 두 번 실행되는 현상이 발생했고, 단순히 “큐에 넣었다 뺐다” 수준의 설계가 운영 단계에서 곧바로 무너진다는 사실을 뼈저리게 겪었습니다. 그때 멱등성이라는 개념을 제대로 받아들이지 못했던 것이 가장 큰 원인이었고, 이후로는 큐 소비자 측에서 멱등성을 보장하거나 메시지에 고유 식별자를 두어 중복을 막는 것이 필수라는 점을 절실히 배웠습니다.
면접에서도 “같은 요청이 두 번 와도 결과는 같아야 합니다”라는 멱등성을 단어 한 줄로 짚어 주면, 단순한 큐 사용과 운영 수준의 큐 사용을 구분하는 신호가 됩니다. 이후 내용에서는 제가 그 시행착오를 거쳐 정리한 흐름, 즉 순서 보장과 멱등성을 바르게 짚고 백프레셔와 데드레터 큐까지 단계적으로 풀어 드리겠습니다.
제가 자주 반복한 실수: “큐는 무조건 FIFO 라고 가정하기”
분산 환경의 큐를 처음 다루는 분들이 거의 예외 없이 빠지는 함정이 있습니다. “Kafka 도 큐니까 순서를 보장해 주는 거 아니야?”라는 전제입니다. 실제로는 파티션 단위로 순서를 보장하기 때문에, 컨슈머 인스턴스가 여러 개이거나 파티션 키를 잘못 고르면 같은 키에 한해서만 순서가 유지되고 그 외에는 순서가 흐트러집니다. 면접에서는 “어떤 경우에 순서를 보장하고, 어떤 경우에 보장하지 않는지”를 짚어야 정확한 답으로 인정받습니다.
제가 첫 설계에서 반복했던 또 다른 실수는 “메시지를 한 번 받았으면 그걸로 끝”이라는 가정이었습니다. 메시지는 라이브러리·네트워크 재시도·컨슈머 재시작 때문에 같은 항목이 두 번 이상 도착할 수 있고, 이때 멱등성이 없으면 결제 중복이나 메일 중복 발송 같은 사고로 직결됩니다. 면접관은 보통 “만약 회사가 결제 도메인에 큐를 도입한다면 중복 결제는 어떻게 막을 계획인가요?”라는 후속 질문을 던집니다. 그 자리에서 멱등성 토큰·데이터베이스의 INSERT ... ON CONFLICT DO NOTHING 같은 실제 메커니즘을 짚어 주면 합격점이 됩니다.
직장에서 멱등성을 박을 때의 코드 패턴
아래는 제가 처음 마주친 단순한 큐 소비자 코드와, 멱등성을 도입한 개선 예시입니다.
// Before: 단순한 큐 소비자 (중복 처리 가능성이 남아 있습니다)
while (true) {
Message msg = queue.receive();
process(msg); // 중복 도착 시 부작용이 그대로 발생합니다
}
// After: 메시지 고유 ID 를 활용해 이미 처리한 작업을 건너뛰는 소비자
Set<String> processedIds = new HashSet<>();
while (true) {
Message msg = queue.receive();
if (processedIds.contains(msg.getId())) {
continue; // 이미 처리한 메시지는 건너뜁니다
}
processedIds.add(msg.getId());
process(msg); // 처음 보는 메시지일 때만 작업을 수행합니다
}
개선 전 코드는 큐에서 메시지를 꺼낸 즉시 바로 처리하기 때문에, 같은 메시지가 두 번 도착하면 작업도 두 번 실행됩니다. 결제·메일 발송처럼 부작용이 큰 도메인이라면 이 차이가 곧 사고로 이어집니다.
개선 후 코드는 메시지 고유 ID 를 Set 에 모아 두고, 이미 본 ID 라면 다음 루프로 넘어갑니다. 면접에서는 “Set 이 무한히 커지는 문제는 어떻게 해결하실 건가요?”라는 후속 질문을 받을 수 있으며, 그 자리에서 Redis 같은 외부 저장소나 데이터베이스의 INSERT ... ON CONFLICT DO NOTHING 같은 DB 차원의 멱등성 패턴까지 이야기해 주면 충분한 인상을 남길 수 있습니다. 실무에서 저는 이 패턴을 도입한 뒤로 같은 메시지가 여러 번 도착해도 결과가 어긋나지 않는다는 점에서 안심이 되었으며, 면접관 입장에서 “운영 사고를 직접 겪은 사람”이라는 인상을 받는 데 효과적이었습니다.
백프레셔와 데드레터 큐를 짚어야 면접이 살아납니다
밀린 메시지를 그냥 쌓아 두면 결국 메모리·디스크 한계에 부딪힙니다. 그래서 큐에는 “이 이상은 더 받지 않는다”라는 신호를 컨슈머가 프로듀서에게 알려 주는 백프레셔가 필요합니다. 프로듀서가 그 신호를 받으면 메시지를 늦추거나 임시로 보관하고, 큐 자체가 위험해지기 전에 흐름을 다시 안정시킵니다.
반면 아무리 잘 만들어도 “처리 자체가 불가능한” 메시지는 항상 존재합니다. 스키마가 어긋난 경우·외부 API 가 영구적으로 실패한 경우·비즈니스 규칙을 위반한 경우입니다. 이런 메시지를 일반 큐에 그대로 두면 무한 재시도로 자원을 낭비하므로, 별도의 데드레터 큐(DLQ)로 옮겨 두는 것이 표준입니다. 데드레터 큐로 옮긴 메시지는 운영자가 한 번에 모아서 확인하고, 원인을 수정한 뒤 다시 일반 큐로 보내거나 폐기합니다.
저는 면접에서 “데드레터 큐는 어떻게 운영하실 건가요?”라는 질문을 받았을 때, “알람을 받아서 한 건 한 건 확인한 뒤, 수정해서 다시 보내거나 폐기하는 배치 운영을 둡니다”라는 답이 합격점을 만들었다는 인상을 받은 적이 있습니다. 큐 본체를 이야기하는 분은 많지만, 데드레터 큐를 짚는 분은 상대적으로 적기 때문입니다.
큐의 본질은 “시스템 간 시간 차이”를 흡수하는 장치입니다
큐와 만난 지 몇 년이 지나고 나서 제가 큐를 대하는 시각이 바뀌었습니다. 큐는 단순한 데이터 저장소가 아니라, 보낸 쪽이 받는 쪽의 속도를 기다리지 않도록 도와 주는 장치이며, 받는 쪽이 일시적으로 느려져도 보낸 쪽이 계속 움직일 수 있도록 만들어 줍니다. 면접에서는 이 한 줄을 덧붙여 주시면 정의를 단순히 외운 답변이 아니라 운영 관점에서 이해하고 있다는 인상을 받으실 수 있습니다.
대규모 분산 시스템에서 요청을 효과적으로 대기시키고 관리하는 역할을 담당하는 것이 바로 큐입니다. 작은 처리 부하와 작은 데이터베이스를 가진 작은 시스템에서는 쓰기 속도가 충분히 빠르다고 예상할 수 있지만, 규모가 커진 시스템에서는 쓰기 시간이 분명히 길어지고 응답 지연이 누적됩니다. 데이터가 서로 다른 서버나 인덱스의 여러 위치에 동시에 기록되어야 한다거나, 시스템이 높은 부하를 받아 한 번에 처리가 어려운 경우가 해당합니다.

개별 쓰기 작업에 오랜 시간이 걸릴 수 있는 경우, 고성능과 가용성을 달성하기 위해선 시스템의 다른 구성 요소가 비동기 방식으로 작동해야 합니다. 일반적으로 큐 대기열이 그 방법이라고 이해할 수 있습니다.
각 클라이언트가 원격 서버에서 처리할 작업을 요청하는 시스템이 있다고 가정해 보겠습니다. 클라이언트는 각각 서버에 요청을 보내고, 서버는 가능한 한 빠르게 작업을 끝내어 결과를 반환하려고 합니다. 처리할 요청의 수가 한 서버의 용량보다 적고 시스템이 작은 상황에서는 이 방식이 충분히 잘 작동합니다.
그러나 서버가 처리할 수 있는 것보다 많은 요청을 한꺼번에 수신하게 되면, 각 클라이언트는 자신의 응답이 도착하기 전까지 다른 클라이언트의 요청이 끝나기를 기다릴 수밖에 없습니다.
이러한 동기식 동작은 클라이언트의 성능을 심각하게 떨어뜨립니다. 클라이언트는 응답이 도착할 때까지 아무 일도 하지 못한 채로 기다리게 되며, 자원을 비효율적으로 소모합니다. 부하를 해결하기 위해 단순히 서버를 늘린다고 해서 문제가 사라지는 것도 아닙니다. 효과적인 부하 분산을 시도하더라도 작업의 공정하고 균형 잡힌 분배를 보장하는 것은 쉬운 일이 아닙니다.
또한 요청을 처리하던 서버가 일시적으로 사용 불가 상태가 되거나 실패하면, 업스트림에 있던 클라이언트도 함께 실패하게 됩니다. 이 문제를 효과적으로 풀어 내려면, 클라이언트의 요청과 실제 작업 수행 사이에 적절한 추상화 계층을 두는 것이 바람직합니다.

큐 처리 대기열은 생각보다 단순합니다. 들어오는 모든 작업은 큐에 추가되며, 작업자가 여유를 갖는 즉시 큐에서 작업을 꺼내어 처리합니다. 이 작업은 데이터베이스에 대한 간단한 쓰기일 수도 있고, 문서의 미리 보기 이미지를 생성하는 것처럼 다소 무거운 작업일 수도 있습니다.
큐 대기열은 비동기 통신 프로토콜에서 구현됩니다. 즉, 클라이언트가 작업을 큐에 제출할 때 더 이상 결과를 기다릴 필요가 없으며, 요청이 정상적으로 수신되었다는 확인만 받으면 충분합니다. 그 확인 정보는 나중에 클라이언트가 작업 결과를 요청할 때 참고 자료로 활용할 수 있습니다. 큐 대기열에는 한 번에 전송될 수 있는 데이터 크기와 큐에 보관될 수 있는 요청 수에 대해 암묵적이거나 명시적인 제한이 존재합니다.
큐 대기열은 서비스 중단과 장애로부터 어느 정도를 보호하는 기능을 제공하기 때문에 내결함성(fault tolerance)에도 자주 사용됩니다. 예를 들면 일시적인 시스템 장애로 실패한 서비스 요청을 재시도하도록 돕는 안정적인 큐를 구성할 수 있으며, 클라이언트에게 간헐적이고 불안정한 서비스를 그대로 노출시켜 복잡한 클라이언트 측 에러 처리를 강요하는 것보다 큐를 사용해 서비스의 품질을 보장하는 쪽이 훨씬 바람직합니다.

큐 대기열은 대규모 분산 시스템의 서로 다른 부분 사이에서 분산 통신을 관리하는 데 중요한 역할을 합니다. 이를 구현하는 방식은 여러 가지이며, RabbitMQ, ZeroMQ, ActiveMQ, BeanstalkD 처럼 바로 활용할 수 있는 오픈 소스 구현도 풍부하게 존재합니다.
면접에서 큐를 말할 때 제가 챙기는 체크리스트
제가 시스템 디자인 면접에서 큐를 다루는 자리에 놓일 때, 다음 다섯 가지 한 줄씩을 머릿속에서 미리 정리한 뒤 답변을 시작합니다. 답변 초입부터 “순서·멱등성·백프레셔·데드레터 큐·이중화” 다섯 마디를 빠지지 않게 짚으면, 면접관 입장에서는 “이 분은 운영 관점까지 알고 있구나”라는 인상을 받게 됩니다.
- 순서 보장 범위: 어느 단위까지 순서를 보장할지, 파티션 키를 어떻게 잡을지
- 멱등 소비자: 같은 메시지가 두 번 와도 결과가 같은지, 어떻게 보장할지
- 백프레셔: 큐가 가득 찼을 때 어떤 신호를 보낼지, 어떻게 흐름을 늦출지
- 데드레터 큐: 처리가 불가능한 메시지를 어디로 보낼지, 누가 알람을 받을지
- 이중화/장애 대응: 브로커 한 대가 죽었을 때 어떻게 복구할지, 데이터 유실 없이 failover 할 수 있는지
이 다섯 가지가 충족되면 면접관은 보통 깊은 후속 질문으로 들어옵니다. “파티션 키를 잘못 잡으면 어떤 일이 벌어지나요?”, “데드레터 큐로 보낸 메시지는 어떻게 추적할 건가요?” 같은 질문이 핵심을 파고드는 자리이며, 제가 실제로 운영에서 만났던 사례를 한 줄씩 곁들이면 자연스럽게 신뢰가 쌓입니다.
마지막으로 한 가지 말씀드리자면, 큐는 면접 답변에서 “정의와 장점”을 한 줄로 풀어 내는 데서 끝나지 않습니다. “저는 운영에서 같은 메시지가 두 번 와서 결제가 두 번 나가던 사고를 직접 겪고 나서, 그제서야 멱등성이 왜 필수인지 받아들였습니다”처럼, 작은 일화를 한 줄 자연스럽게 끼워 넣는 것만으로 일반론 답변과 운영 경험 답변이 명확히 구분됩니다. 큐를 다루실 때 이 작은 일화를 미리 준비해 두시면 면접의 신뢰도가 한 단계 올라가실 것입니다.