본문으로 바로가기

05

동기와 비동기

사용자를 기다리게 할 일과, 나중에 해도 되는 일을 나눈다

먼저 보면 좋은 강의: 지연 시간과 처리량, API와 JSON

단계별로 보기

스페이스바로 재생/정지, 좌우 화살표로 단계 이동, R로 처음으로 돌아갑니다.

대기 중1 / 4 단계 — 전부 끝날 때까지 기다린다

주문 API가 요청을 받아 영수증·메일 처리로 넘깁니다. 이 처리에 200밀리초가 걸립니다. 그동안 사용자는 화면 앞에서 기다립니다. 주문 저장은 이미 20밀리초 만에 끝났는데도, 메일이 나갈 때까지 "주문 완료"를 보여주지 않습니다.

이 강의를 마치면

  • 요청 하나 안에서 무엇이 꼭 지금 끝나야 하고 무엇이 나중에 해도 되는지 구분한다.
  • 큐와 워커가 사용자 대기 시간을 어떻게 줄이는지 설명한다.
  • 비동기가 일을 없애주지 않으며 밀린 일은 다른 곳에 쌓인다는 것을 관찰한다.
  • 비동기로 옮기면 안 되는 작업의 기준을 말할 수 있다.

주문 버튼을 누르면 벌어지는 일

쇼핑몰에서 주문 버튼을 누르면 서버는 이런 일들을 합니다.

  1. 주문 내용을 데이터베이스에 저장한다 — 0.02초
  2. 영수증 PDF를 만든다 — 0.8초
  3. 확인 메일을 보낸다 — 1.2초
  4. 창고 시스템에 알린다 — 0.5초

이걸 순서대로 다 하고 나서 "주문 완료" 화면을 띄우면, 사용자는 2.5초를 봅니다. 버튼을 누르고 화면이 멈춘 채 2.5초. 사용자는 버튼을 한 번 더 누릅니다.

질문 하나

사용자가 이 네 가지를 전부 기다려야 할 이유가 있습니까?

없습니다. 사용자가 알고 싶은 건 "내 주문이 접수됐나?" 하나뿐입니다. 메일이 지금 나갔는지 3초 뒤에 나갔는지는 관심 밖입니다. 애초에 메일은 화면에 보이지도 않습니다.

카페 계산대 비유

카페에서 주문하고 계산할 때, 직원이 커피가 다 나올 때까지 계산대를 붙잡고 있지 않습니다.

동기 방식: 주문 → 커피 만들기(3분) → 영수증 주기 → 다음 손님
비동기 방식: 주문 → 영수증과 진동벨 주기 → 다음 손님
            (커피는 뒤에서 바리스타가 따로 만든다)

두 번째 방식에서 계산대 직원은 20초에 한 명씩 처리합니다. 커피는 여전히 3분이 걸리지만 그건 뒤에서 별도로 진행됩니다. 손님은 진동벨을 들고 자리에 가서 앉습니다.

여기서 세 가지 부품이 등장합니다.

카페시스템
주문서를 꽂아두는 집게큐(대기열)
바리스타워커
진동벨작업 ID / 알림

시스템으로 옮기면

전:  사용자 → API → (저장 + PDF + 메일 + 창고알림) → 응답     … 2.5초
후:  사용자 → API → 저장 → 큐에 "할 일" 넣기 → 응답            … 0.05초
                           ↓
                      워커가 꺼내서 PDF·메일·창고 처리

사용자가 보는 시간이 50배 줄었습니다. 그것도 서버를 더 빠르게 만들지 않고, 기다릴 필요 없는 일을 기다리지 않기로 결정한 것만으로요.

여기서 반드시 알아야 할 것

이 지점에서 대부분이 착각합니다. 비동기는 일을 없애주지 않습니다.

메일은 여전히 1.2초가 걸립니다. 초당 100건의 주문이 들어오면 초당 100통의 메일 작업이 생깁니다. 워커 한 명이 1.2초에 한 통씩 처리하면 초당 0.8통입니다. 나머지는 큐에 쌓입니다.

들어오는 일  100건/초
처리하는 일    0.8건/초
─────────────────────
쌓이는 일     99.2건/초

10분이면 6만 건이 밀립니다. 이때 사용자 화면은 어떨까요? 완벽하게 정상입니다. 0.05초 만에 "주문 완료"가 뜹니다. 응답 시간 그래프도, 오류율 그래프도 아름답습니다.

그런데 고객센터에는 전화가 옵니다. "주문했는데 메일이 안 와요." 세 시간 전에 한 주문의 메일이 지금 나가고 있기 때문입니다.

비동기의 진짜 위험은 문제가 보이지 않는 곳으로 옮겨간다는 것입니다. 그래서 큐를 쓰기 시작하면 반드시 큐에 몇 건이 밀려 있는지를 봐야 합니다. 이 숫자를 안 보면 장애를 고객이 먼저 알게 됩니다.

해결책은 단순합니다. 워커를 늘립니다. 큐 뒤의 일꾼은 사용자를 기다리게 하지 않으므로 마음 편히 늘릴 수 있습니다. 이것이 큐를 두는 두 번째 이유입니다 — 일의 양과 일꾼의 수를 분리할 수 있게 됩니다.

무엇을 뒤로 미루면 안 되는가

기준은 하나입니다. 사용자가 다음 행동을 결정하는 데 필요한 정보인가?

작업미뤄도 되나이유
재고 확인품절이면 주문 자체가 안 된다
결제 승인실패하면 주문이 성립하지 않는다
주문 저장저장 못 했는데 "완료"라고 하면 거짓말이다
영수증 PDF나중에 받아도 된다
확인 메일몇 초 늦어도 아무도 모른다
추천 모델 갱신내일 해도 된다

"완료했습니다"라고 말한 뒤에 그 일이 실패하면 사용자에게 거짓말을 한 것이 됩니다. 그래서 비동기로 옮긴 작업은 실패해도 반드시 다시 시도되어야 하고, 그 재시도가 같은 메일을 두 번 보내지 않도록 보장해야 합니다. 그 이야기는 뒤의 강의에서 다룹니다.

이 강의가 단순화한 것

교육용 모형은 언제나 무언가를 생략합니다. 무엇을 생략했는지 아는 것도 학습의 일부입니다.

전제한 것

  • 큐는 메시지를 받으면 즉시 '접수했다'고 응답하고, 실제 처리는 워커가 따로 진행한다.
  • 이 강의의 큐는 가득 차지 않을 만큼 크다고 본다. 실제 큐에는 한계가 있고, 넘치면 작업이 유실된다.
  • 워커가 작업에 실패했을 때의 재시도와 중복 처리 문제는 다루지 않는다.
  • 사용자에게 결과를 다시 알려주는 경로(알림·폴링)는 그래프에 그리지 않았다.

다루지 않은 것

  • 메시지 전달 보장(at-least-once / exactly-once)과 멱등성
  • 데드 레터 큐와 실패한 작업의 처리
  • 큐의 순서 보장과 파티셔닝
  • 작업 결과를 사용자에게 되돌리는 방법(웹훅, 폴링, 푸시)

더 읽을거리: Enterprise Integration Patterns — Message Channel

이제 직접 만들어 보세요

읽어서 아는 것과 만들어서 아는 것은 다릅니다. 컴포넌트를 배치하고 트래픽을 흘려서 실제로 동작하는지 확인해 보세요.

실습 과제 열기

이 강의에 물어보기

하루 5번

이 강의 본문만 근거로 답합니다. 강의에 없는 내용은 지어내지 않습니다.