주문 버튼을 누르면 결제·재고차감·알림발송·정산기록이 모두 동기로 줄줄이 실행됩니다. 평소엔 괜찮은데, 이벤트 세일에 주문이 폭주하자 알림 서버가 느려지면서 주문 응답 전체가 멈춥니다. 알림 때문에 주문이 안 되는 황당한 상황입니다. 옆 서비스는 주문만 처리하고 알림·정산은 '이벤트'로 큐에 던져둡니다. 알림이 느려도 주문은 멀쩡합니다. 동기/비동기와 이벤트 기반 구조를 알면, "왜 한 부분이 느린데 전체가 멈추나"를 이해하고 그 결합을 끊는 설계를 판단할 수 있습니다.
- 1동기와 비동기 호출의 차이를 "응답을 기다리는가"로 설명할 수 있다
- 2메시지 큐가 생산자-소비자를 분리·완충·격리하는 원리를 설명할 수 있다
- 3전달 보장(at-least-once 등)과 멱등 처리·DLQ의 필요성을 설명할 수 있다
- 4동기 체인의 장애 전파를 비동기로 끊는 설계를 판단할 수 있다
동기 vs 비동기
기다리느냐, 맡기고 가느냐
동기(synchronous): 응답을 기다린 뒤 다음
주문 → [결제 대기] → [재고 대기] → [알림 대기] → [정산 대기] → 응답
→ 한 단계라도 느리면(알림 서버 지연) 전체 응답이 멈춤. 장애가 전파됨.
비동기(asynchronous): 맡기고 즉시 다음
주문 → 결제(동기, 꼭 필요) → 응답!
└─ 이벤트 발행: "주문생성됨" → 큐
├─ 알림 소비자(자기 속도로)
├─ 정산 소비자
└─ 재고 소비자
→ 알림이 느려도 주문 응답은 빠름. 결합이 끊김.
핵심 판단: 사용자 응답에 꼭 필요한 것(결제 성공)만 동기로, 나머지(알림·정산·집계)는 비동기로 뺍니다. 이는 [[architecture-patterns]]의 MSA에서 서비스 간 결합을 끊는 핵심 수단이며, [[twelve-factor-app]]의 무상태·확장성과도 맞물립니다.
메시지 큐 — 분리·완충·격리
생산자와 소비자 사이의 버퍼
메시지 큐(Kafka·RabbitMQ·SQS·Redis)는 생산자(이벤트 발행)와 소비자(처리) 사이에 버퍼를 둡니다.
| 용어 | 뜻 | 비고 |
|---|---|---|
| Producer / Consumer | 메시지 발행자 / 소비자 | 둘이 분리(decouple) |
| Topic / Queue | 메시지가 쌓이는 채널 | 주제별 분리 |
| Partition / Offset | 병렬 처리 단위 / 읽은 위치 | Kafka의 확장 핵심 |
| Consumer Group | 소비자 묶음(부하 분산) | 병렬 소비 |
| Broker | 큐 서버 | 보통 클러스터로 이중화 |
세 가지 이점:
- 분리(decouple): 생산자는 소비자가 누군지·살았는지 몰라도 됨.
- 완충(buffer): 스파이크를 큐가 쌓아두고 소비자가 자기 속도로 처리.
- 격리(isolation): 소비자가 죽어도 메시지가 큐에 남아 유실 안 됨, 복구 후 처리.
깊은 운영(파티션·보존기간·디스크 산정)은 [[tech-stack-reading]]에서 본 것처럼 별도 인프라 투자가 필요합니다. 큐는 '또 하나의 서버'가 아니라 클러스터·디스크·모니터링이 따르는 상태 저장 시스템입니다.
전달 보장과 멱등 — 비동기의 함정
중복은 정상이다 — 그래서 멱등이 필수
비동기의 가장 흔한 함정은 중복입니다. 대부분의 큐는 전달을 이렇게 보장합니다.
at-most-once : 최대 한 번(유실 가능, 중복 없음) — 손실 OK인 지표 등
at-least-once : 최소 한 번(중복 가능, 유실 없음) — 가장 흔함
exactly-once : 정확히 한 번(이상적이나 비싸고 제약 많음)
대부분 'at-least-once' → 같은 메시지가 2번 올 수 있다!
"주문 생성" 이벤트 중복 → 멱등 처리 안 하면 결제 2번/알림 2번
대응:
- 멱등 처리(idempotent): Idempotency Key·중복 체크로 "같은 메시지 두 번 처리해도 결과 1번"([[requirements-prd]]).
- DLQ(Dead Letter Queue): N번 실패한 메시지를 격리 보관 → 정상 흐름을 안 막고 원인 분석·재처리.
- 재시도(retry) + 백오프: 일시 장애는 점점 간격을 늘려 재시도.
PM·인프라 관점: "비동기로 빼자"는 결정엔 중복·실패·순서라는 새 문제가 따라옵니다. 멱등·DLQ·재시도 설계 없이 큐만 도입하면 중복 사고가 납니다.
동기 체인 장애 전파 진단 — 직접 확인
"한 곳이 느린데 전체가 멈춘다"가 의심되면, 요청 경로의 동기 호출 체인과 비핵심 작업의 위치를 점검합니다.
점검: 주문 API 한 번에 동기로 무엇이 실행되나?
[결제] ← 사용자 응답에 필수(동기 OK)
[재고차감] ← 보통 필수
[알림발송] ← 비핵심인데 동기? → 알림 지연이 주문을 막음(끊어야 함)
[정산기록] ← 비핵심인데 동기? → 비동기로 뺄 후보
[추천갱신] ← 비핵심 → 비동기
진단 신호(관측성):
- 주문 API p95 지연이 알림 서버 지연과 함께 치솟나? → 동기 결합 증거
- 외부 호출(알림 API) 타임아웃이 주문 실패로 번지나? → 격리 필요
트레이스 분석 예([[glossary-observability]]):
POST /orders 총 3200ms
├ 결제 120ms
├ 재고 40ms
├ 알림발송 2900ms ← 외부 알림 API 지연이 주문 응답을 지배!
└ 정산 140ms
→ 알림·정산을 이벤트로 빼면 주문 응답 ~300ms로 단축 + 알림 장애 격리
echo '요청 처리 경로의 동기 호출 체인과 외부 의존 확인'- 분산 트레이스에서 한 요청의 단계별 소요를 본다([[glossary-observability]]) — 비핵심 작업(알림·정산)이 전체 시간을 지배하면 동기 결합 증거. 비동기로 분리 후보
- 외부 서비스(알림·결제사) 호출이 동기 체인에 있으면 그 외부 장애가 우리 핵심 기능을 멈춘다 → 비동기화 또는 서킷브레이커·타임아웃으로 격리
- 비동기로 뺐다면 중복 도착에 멱등 처리가 됐는지 확인 — 같은 이벤트 2번에 결제/알림이 2번 나가면 at-least-once 함정에 빠진 것
- DLQ가 쌓이고 있으면 특정 메시지가 반복 실패 중 → 그 메시지 형식·소비자 버그 조사. DLQ 적체는 "조용히 실패가 누적되는" 위험 신호
상황: 주문 처리에서 알림 발송을 동기로 호출하는데, 외부 알림(SMS/푸시) 게이트웨이가 느려지자 주문 API 응답이 함께 멈추고 타임아웃이 번져 주문 자체가 실패합니다. 정작 결제는 멀쩡한데 알림 때문에 매출이 막힙니다.
원인: 비핵심 작업을 핵심 흐름에 동기로 결합했습니다. 외부 의존(알림 API)의 장애가 호출 체인을 타고 핵심 기능(주문)으로 전파된 것입니다.
진단:
□ 분산 트레이스에서 주문 지연의 대부분이 알림 단계인가? (그렇다면 확정)
□ 알림 호출에 타임아웃·서킷브레이커가 없나? (없으면 무한 대기)
해결: (1) 알림·정산·집계 같은 비핵심 작업을 이벤트로 분리 — 주문은 "주문생성됨" 이벤트만 큐에 발행하고 즉시 응답, 알림 소비자가 비동기로 처리. (2) 부득이 동기로 남길 외부 호출엔 타임아웃 + 서킷브레이커로 장애 격리. (3) 비동기 전환 시 멱등 처리·DLQ·재시도를 함께 설계해 중복·실패에 대비. "무엇이 사용자 응답에 진짜 필요한가"를 기준으로 동기/비동기를 가르는 것이 핵심입니다.
인프라/SRE로서 비동기·이벤트 기반 도입은 큰 운영 책임을 동반합니다 — 메시지 브로커(Kafka 등) 클러스터 이중화·디스크/보존기간 산정·소비자 지연(consumer lag) 모니터링·DLQ 적체 알람을 설계해야 합니다([[glossary-batch-async]]). "비동기로 빼자"는 한마디가 새로운 인프라(브로커·모니터링)와 새로운 실패 모드(중복·순서·DLQ)를 가져온다는 것을 의사결정에 올립니다. PM은 동기/비동기 경계를 "사용자 응답에 필수인가"로 판단해, 비핵심 작업의 지연·장애가 핵심 매출 흐름을 막지 않도록 요구사항([[requirements-prd]])과 설계를 정렬합니다.
이것으로 Phase 5(아키텍처)를 마칩니다. 다음 Phase에서는 이 모든 것을 떠받치는 품질·관측·운영 문화를 다룹니다.