infra
Platform

모듈 맵

[SW Eng] 동기 vs 비동기 — 메시지 큐와 이벤트 기반 구조

0 / 38 완료

펼치기
0 / 38 완료0%

PM·SRE를 위한 소프트웨어 엔지니어링 · 21 / 38

[SW Eng] 동기 vs 비동기 — 메시지 큐와 이벤트 기반 구조

동기/비동기 호출의 차이, 메시지 큐(Kafka·RabbitMQ·SQS)·이벤트 기반·전달 보장·DLQ를 PM·인프라 관점에서 정리합니다

🚨INCIDENT ALERT
HIGH

주문 버튼을 누르면 결제·재고차감·알림발송·정산기록이 모두 동기로 줄줄이 실행됩니다. 평소엔 괜찮은데, 이벤트 세일에 주문이 폭주하자 알림 서버가 느려지면서 주문 응답 전체가 멈춥니다. 알림 때문에 주문이 안 되는 황당한 상황입니다. 옆 서비스는 주문만 처리하고 알림·정산은 '이벤트'로 큐에 던져둡니다. 알림이 느려도 주문은 멀쩡합니다. 동기/비동기와 이벤트 기반 구조를 알면, "왜 한 부분이 느린데 전체가 멈추나"를 이해하고 그 결합을 끊는 설계를 판단할 수 있습니다.

이번 챕터에서 배울 것
  • 1동기와 비동기 호출의 차이를 "응답을 기다리는가"로 설명할 수 있다
  • 2메시지 큐가 생산자-소비자를 분리·완충·격리하는 원리를 설명할 수 있다
  • 3전달 보장(at-least-once 등)과 멱등 처리·DLQ의 필요성을 설명할 수 있다
  • 4동기 체인의 장애 전파를 비동기로 끊는 설계를 판단할 수 있다

동기 vs 비동기 — 기다리는가 큐에 맡기는가확대 위 그림처럼 동기 처리는 모든 단계를 기다려 응답이 느리지만, 비동기는 큐에 이벤트를 발행하고 즉시 응답해 UX를 개선합니다.

동기 vs 비동기

💡개념

기다리느냐, 맡기고 가느냐

핵심 판단: 사용자 응답에 꼭 필요한 것(결제 성공)만 동기로, 나머지(알림·정산·집계)는 비동기로 뺍니다. 이는 모놀리식 vs 마이크로서비스의 MSA에서 서비스 간 결합을 끊는 핵심 수단이며, 12-Factor App의 무상태·확장성과도 맞물립니다.

메시지 큐 — 분리·완충·격리

💡개념

생산자와 소비자 사이의 버퍼

메시지 큐(Kafka·RabbitMQ·SQS·Redis)는 생산자(이벤트 발행)와 소비자(처리) 사이에 버퍼를 둡니다.

용어비고
Producer / Consumer메시지 발행자 / 소비자둘이 분리(decouple)
Topic / Queue메시지가 쌓이는 채널주제별 분리
Partition / Offset병렬 처리 단위 / 읽은 위치Kafka의 확장 핵심
Consumer Group소비자 묶음(부하 분산)병렬 소비
Broker큐 서버보통 클러스터로 이중화

세 가지 이점:

  • 분리(decouple): 생산자는 소비자가 누군지·살았는지 몰라도 됨.
  • 완충(buffer): 스파이크를 큐가 쌓아두고 소비자가 자기 속도로 처리.
  • 격리(isolation): 소비자가 죽어도 메시지가 큐에 남아 유실 안 됨, 복구 후 처리.

깊은 운영(파티션·보존기간·디스크 산정)은 기술스택 읽기에서 본 것처럼 별도 인프라 투자가 필요합니다. 큐는 '또 하나의 서버'가 아니라 클러스터·디스크·모니터링이 따르는 상태 저장 시스템입니다.

메시지 큐 흐름 — 프로듀서·큐·컨슈머 역할 분리확대 위 그림처럼 메시지 큐는 프로듀서와 컨슈머 사이의 완충제로, 각 컨슈머가 독립적으로 소비하고 장애 시 재전달이 보장됩니다.

전달 보장과 멱등 — 비동기의 함정

💡개념

중복은 정상이다 — 그래서 멱등이 필수

비동기의 가장 흔한 함정은 중복입니다. 대부분의 큐는 전달을 이렇게 보장합니다.

TEXT
at-most-once   : 최대 한 번(유실 가능, 중복 없음) — 손실 OK인 지표 등
at-least-once  : 최소 한 번(중복 가능, 유실 없음) — 가장 흔함
exactly-once   : 정확히 한 번(이상적이나 비싸고 제약 많음)

대부분 'at-least-once' → 같은 메시지가 2번 올 수 있다!
  "주문 생성" 이벤트 중복 → 멱등 처리 안 하면 결제 2번/알림 2번

대응:

  • 멱등 처리(idempotent): Idempotency Key·중복 체크로 "같은 메시지 두 번 처리해도 결과 1번"(요구사항 정의).
  • DLQ(Dead Letter Queue): N번 실패한 메시지를 격리 보관 → 정상 흐름을 안 막고 원인 분석·재처리.
  • 재시도(retry) + 백오프: 일시 장애는 점점 간격을 늘려 재시도.

PM·인프라 관점: "비동기로 빼자"는 결정엔 중복·실패·순서라는 새 문제가 따라옵니다. 멱등·DLQ·재시도 설계 없이 큐만 도입하면 중복 사고가 납니다.

동기 vs 비동기 판단 — 무엇을 어떻게 분리하나
동기로 둘까 비동기로 뺄까응답에 결과가 꼭 필요하면 동기(결제 승인), 나중에 처리해도 되면 비동기(알림·이메일·집계). '사용자가 결과를 기다려야 하나'가 기준
비핵심 작업(알림)이 본 요청을 막음동기 호출 체인에 외부 의존을 넣으면 그 장애가 전체를 마비. 알림 같은 비핵심은 큐로 분리 → 알림 API 죽어도 주문은 성공. '비핵심은 동기 경로에서 빼라'
메시지가 중복 전달됨at-least-once 큐는 중복이 정상 — 소비자를 멱등하게(같은 메시지 두 번 처리해도 결과 동일). 멱등키·업서트. '중복은 막는 게 아니라 견디는 것'
메시지 처리가 계속 실패무한 재시도 금지 — N회 실패하면 DLQ(Dead Letter Queue)로 격리 후 알림. 독성 메시지가 큐를 막지 않게. '실패는 격리하고 본류는 흐르게'
순서가 중요한가대부분의 큐는 순서 보장 안 함 — 순서 의존 설계를 피하거나 파티션 키로 보장(Kafka). '순서를 가정하지 마라, 필요하면 명시 설계'
재시도 정책일시 장애는 지수 백오프 + 지터로 재시도(thundering herd 방지). 영구 실패(잘못된 데이터)는 재시도 무의미 → DLQ. '재시도할 것과 버릴 것을 구분'

동기 체인 장애 전파 진단 — 직접 확인

1한 부분 지연이 전체를 멈추는지(동기 결합) 점검

"한 곳이 느린데 전체가 멈춘다"가 의심되면, 요청 경로의 동기 호출 체인과 비핵심 작업의 위치를 점검합니다.

TEXT
점검: 주문 API 한 번에 동기로 무엇이 실행되나?
  [결제] ← 사용자 응답에 필수(동기 OK)
  [재고차감] ← 보통 필수
  [알림발송] ← 비핵심인데 동기? → 알림 지연이 주문을 막음(끊어야 함)
  [정산기록] ← 비핵심인데 동기? → 비동기로 뺄 후보
  [추천갱신] ← 비핵심 → 비동기

진단 신호(관측성):
  - 주문 API p95 지연이 알림 서버 지연과 함께 치솟나? → 동기 결합 증거
  - 외부 호출(알림 API) 타임아웃이 주문 실패로 번지나? → 격리 필요

트레이스 분석 예(용어사전) — POST /orders3200ms:

구간소요
결제120ms
재고40ms
알림발송2900ms ← 외부 알림 API 지연이 주문 응답을 지배!
정산140ms

→ 알림·정산을 이벤트로 빼면 주문 응답 ~300ms로 단축 + 알림 장애 격리

echo '요청 처리 경로의 동기 호출 체인과 외부 의존 확인'
🔍실행 후 확인할 것
  • 분산 트레이스에서 한 요청의 단계별 소요를 본다(용어사전) — 비핵심 작업(알림·정산)이 전체 시간을 지배하면 동기 결합 증거. 비동기로 분리 후보
  • 외부 서비스(알림·결제사) 호출이 동기 체인에 있으면 그 외부 장애가 우리 핵심 기능을 멈춘다 → 비동기화 또는 서킷브레이커·타임아웃으로 격리
  • 비동기로 뺐다면 중복 도착에 멱등 처리가 됐는지 확인 — 같은 이벤트 2번에 결제/알림이 2번 나가면 at-least-once 함정에 빠진 것
  • DLQ가 쌓이고 있으면 특정 메시지가 반복 실패 중 → 그 메시지 형식·소비자 버그 조사. DLQ 적체는 "조용히 실패가 누적되는" 위험 신호

상황: 주문 처리에서 알림 발송을 동기로 호출하는데, 외부 알림(SMS/푸시) 게이트웨이가 느려지자 주문 API 응답이 함께 멈추고 타임아웃이 번져 주문 자체가 실패합니다. 정작 결제는 멀쩡한데 알림 때문에 매출이 막힙니다.

원인: 비핵심 작업을 핵심 흐름에 동기로 결합했습니다. 외부 의존(알림 API)의 장애가 호출 체인을 타고 핵심 기능(주문)으로 전파된 것입니다.

진단:

TEXT
□ 분산 트레이스에서 주문 지연의 대부분이 알림 단계인가? (그렇다면 확정)
□ 알림 호출에 타임아웃·서킷브레이커가 없나? (없으면 무한 대기)

해결: (1) 알림·정산·집계 같은 비핵심 작업을 이벤트로 분리 — 주문은 "주문생성됨" 이벤트만 큐에 발행하고 즉시 응답, 알림 소비자가 비동기로 처리. (2) 부득이 동기로 남길 외부 호출엔 타임아웃 + 서킷브레이커로 장애 격리. (3) 비동기 전환 시 멱등 처리·DLQ·재시도를 함께 설계해 중복·실패에 대비. "무엇이 사용자 응답에 진짜 필요한가"를 기준으로 동기/비동기를 가르는 것이 핵심입니다.

CQRS 패턴 — 읽기와 쓰기 경로 분리확대 위 그림처럼 CQRS는 명령(쓰기)과 조회(읽기)를 별도 경로로 처리해, 각각의 DB를 목적에 맞게 최적화할 수 있습니다.

심화 — 큐를 넣는 순간 생기는 두 번째 층의 문제들

💡개념

심화: DB 커밋과 이벤트 발행 사이 — 이중 쓰기 문제와 아웃박스 패턴

"주문을 DB에 저장하고, 주문생성 이벤트를 발행한다" — 이 두 줄 사이에는 멱등·DLQ 다음의 함정이 하나 더 있습니다. 둘은 서로 다른 시스템에 대한 쓰기라 하나의 트랜잭션으로 묶이지 않습니다.

  • 이중 쓰기(dual write) 문제: DB 커밋은 성공했는데 브로커 발행 직전에 프로세스가 죽으면 — 주문은 있는데 이벤트가 없습니다(소비자들은 영원히 모름). 순서를 바꿔 발행부터 하면 — 이벤트는 나갔는데 주문이 없는 '유령 이벤트'가 됩니다. 어느 순서든 장애 타이밍에 따라 불일치가 생깁니다.
  • 트랜잭셔널 아웃박스(Transactional Outbox): 이벤트를 브로커로 바로 보내지 않고, 같은 DB 트랜잭션 안에서 outbox 테이블에 저장합니다. 별도 릴레이 프로세스(폴링 또는 CDC)가 outbox를 읽어 브로커로 발행합니다. "주문 저장 + 이벤트 기록"이 원자적이 되므로 유실·유령이 사라집니다. 대신 릴레이는 재시도 기반이라 전달이 at-least-once가 됩니다 — 소비자 멱등이 여전히, 그리고 더욱 필수인 이유입니다.
  • 규모가 커지면 lag이 문제의 중심이 됩니다: 소비 지연(consumer lag)은 '유입량 > 처리량'이 쌓인 적분값이라, 문제가 시작되고 한참 뒤에야 증상이 보입니다. 그리고 파티션 수가 병렬 소비의 상한입니다 — 파티션이 8개면 소비자를 20개로 늘려도 8개만 일합니다. 파티션 키가 특정 값에 쏠리면(핫 파티션) 전체 처리량은 남아도는데 한 파티션만 몇 시간씩 밀리는 기현상이 납니다.
  • 리밸런싱은 공짜가 아닙니다: 소비자 그룹에 소비자가 들고 나면(배포·장애·스케일) 파티션 재배정 동안 그룹 전체의 소비가 잠시 멈춥니다. 소비자가 자주 재시작되는 환경이면 이 정지가 누적돼 lag의 숨은 원인이 됩니다.

상황: 이벤트당 처리가 무거운(외부 API 호출 + 집계) 소비자가 있습니다. 트래픽이 늘자 lag이 급증하는데, 소비자 프로세스는 CPU·메모리 모두 정상입니다. 로그를 보니 같은 메시지를 반복 처리하고 있고, 브로커 쪽에는 컨슈머 그룹 리밸런싱이 몇 분 간격으로 계속 찍힙니다.

원인: 한 번에 가져온 메시지 배치의 처리 시간이 브로커가 허용하는 폴링 간격 상한(max.poll.interval)을 넘었습니다. 브로커는 "이 소비자가 죽었다"고 판단해 그룹에서 제외하고 리밸런싱합니다. 처리 중이던 메시지는 커밋되지 못해 다른 소비자에게 재전달되고, 그 소비자도 같은 이유로 시간을 초과합니다 — 살아 있는 소비자들이 서로를 쫓아내는 리밸런싱 루프입니다. 처리량은 0에 수렴하고 중복 처리만 쌓입니다.

진단:

TEXT
□ 브로커/컨슈머 로그에 rebalance가 주기적으로 반복되나? (루프 확정 신호)
□ 배치 처리 시간(메시지 수 × 건당 처리시간)이 poll 간격 상한을 넘나?
□ 소비자 수를 늘려도 lag이 줄지 않나? (늘릴수록 리밸런싱만 잦아짐)

해결: (1) 한 번에 가져오는 메시지 수(max.poll.records)를 줄여 배치 처리 시간을 상한 아래로 낮춥니다. (2) 무거운 처리를 소비 루프에서 분리합니다 — 소비자는 가볍게 받아 내부 워커에 넘기고 빠르게 다음 poll로. (3) 중복 재처리의 피해는 멱등 처리가 막고 있어야 합니다 — 이 사고에서 멱등이 없었다면 외부 API 중복 호출 사고가 겹쳤을 것입니다. (4) 재발 방지로 lag과 함께 리밸런싱 횟수에 알람을 겁니다(용어사전) — lag은 결과고, 잦은 리밸런싱이 원인 쪽 신호입니다.

💼
실무 맥락
현업 패턴

인프라/SRE로서 비동기·이벤트 기반 도입은 큰 운영 책임을 동반합니다 — 메시지 브로커(Kafka 등) 클러스터 이중화·디스크/보존기간 산정·소비자 지연(consumer lag) 모니터링·DLQ 적체 알람을 설계해야 합니다(용어사전). "비동기로 빼자"는 한마디가 새로운 인프라(브로커·모니터링)와 새로운 실패 모드(중복·순서·DLQ)를 가져온다는 것을 의사결정에 올립니다. PM은 동기/비동기 경계를 "사용자 응답에 필수인가"로 판단해, 비핵심 작업의 지연·장애가 핵심 매출 흐름을 막지 않도록 요구사항(요구사항 정의)과 설계를 정렬합니다.

이것으로 Phase 5(아키텍처)를 마칩니다. 다음 Phase에서는 이 모든 것을 떠받치는 품질·관측·운영 문화를 다룹니다.

실전 랩으로 손에 익히기: 동기·비동기 실습 — 동기/비동기를 판단하고 메시지 큐·이벤트 구조를 설계합니다.

지식 확인

퀴즈 — 8문제

Q1

동기(synchronous) 호출과 비동기(asynchronous) 호출의 핵심 차이는?

Q2

메시지 큐(Kafka/RabbitMQ/SQS)가 생산자-소비자 사이에서 하는 핵심 역할은?

Q3

'DLQ(Dead Letter Queue)'의 용도는?

Q4

비동기·이벤트 기반에서 '멱등 처리(idempotent)'가 중요한 이유는?

Q5

'주문 생성' 이벤트를 발행하면 결제·알림·재고 서비스가 각자 구독해 처리하는 이벤트 기반 구조의 이점은?

Q6

비동기·이벤트 기반에서는 데이터가 '즉시'가 아니라 '결국' 일관돼진다(최종 일관성). 이를 다룰 때 주의할 점은?

Q7

[심화] 주문을 DB에 저장한 뒤 주문생성 이벤트를 브로커에 발행하는 두 줄 사이에 숨은 '이중 쓰기' 함정과 그 정석 해법으로 옳은 것은?

Q8

[심화] 컨슈머 서버는 CPU·메모리 모두 정상인데 lag이 계속 늘고, 같은 이벤트가 수십 번 재처리되며 그룹 리밸런싱이 몇 분마다 반복된다. 원인과 처방으로 옳은 것은?

0 / 8 답변

🧪 실습으로 확인하기

동기 vs 비동기 — 메시지 큐와 이벤트 기반 구조

고급

주문 흐름의 각 호출을 즉시 응답 필요·결합도·소요시간 기준으로 동기/비동기로 가르고, 비동기로 뺄 작업에 메시지 큐(재시도·DLQ·멱등성·순서)를 설계하며, 이벤트 기반 구조로 갈 지점과 그 대가(최종 일관성·복잡성)를 근거 있게 판단한다.

55📋 3단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요