오늘 발표할 주제는 비동기 메시징 시스템을 언제 쓰면 좋은가입니다.
서비스가 작을 때는 주문 기능이 결제 기능을 바로 호출하고, 재고 기능을 바로 호출해도 큰 문제가 없습니다.
하지만 기능이 많아지고 요청이 늘어나면 이런 문제가 생깁니다.
- 요청 하나가 너무 오래 걸립니다.
- 알림 같은 부가 기능 장애가 주문 같은 핵심 기능까지 망가뜨립니다.
- 갑자기 요청이 몰리면 서버가 버티기 어렵습니다.
- 주문, 결제, 재고, 알림, 통계 기능이 서로 너무 강하게 묶입니다.
이럴 때 검토하는 방법이 비동기 메시징입니다.
비동기 메시징은 쉽게 말하면
“지금 바로 처리할 일”과 “잠시 뒤에 처리해도 되는 일”을 나누는 방식입니다.
Kafka, RabbitMQ, AWS SQS 같은 기술이 대표적이지만, 오늘은 특정 제품 사용법보다 도입 기준에 집중하겠습니다.
먼저 동기 처리와 비동기 처리의 차이를 주문 예시로 보겠습니다.
동기 처리는 요청한 사람이 결과를 받을 때까지 기다리는 방식입니다.
주문 API 안에서 다음 작업을 모두 처리합니다.
- 주문 생성
- 결제 처리
- 재고 차감
- 알림 발송
- 응답 반환
구조가 단순하고 성공, 실패를 바로 알 수 있다는 장점이 있습니다.
하지만 결제가 느리거나 알림 서버가 장애가 나면 주문 응답도 같이 느려지거나 실패할 수 있습니다.
비동기 처리는 모든 일을 그 자리에서 끝내지 않습니다.
먼저 주문 생성처럼 꼭 필요한 일만 처리하고, 나머지는
**“주문이 생성되었다”**는 메시지로 남깁니다.
그다음 결제, 재고, 알림, 통계 기능이 각자 그 메시지를 보고 자기 일을 처리합니다.
흐름은 이렇게 바뀝니다.
- 주문 생성
- “주문이 생성됨” 메시지 발행
- 사용자에게 먼저 응답 반환
- 결제, 재고, 알림, 통계는 뒤에서 처리
핵심은
사용자에게 즉시 필요한 작업과 나중에 처리해도 되는 작업을 분리하는 것입니다.
비동기 메시징은 아무 때나 쓰는 기술은 아닙니다.
다음 상황에 해당할 때 도입을 검토할 수 있습니다.
예를 들면 이메일 발송, 푸시 알림, 로그 저장, 통계 집계, 검색 인덱스 갱신 같은 작업입니다.
회원가입에서 회원 정보 저장은 즉시 끝나야 합니다.
하지만 가입 축하 이메일이나 마케팅 로그 저장은 몇 초 늦어져도 큰 문제가 없습니다.
이런 부가 작업까지 모두 요청 안에서 처리하면 사용자 응답 시간이 길어집니다.
선착순 이벤트, 예약 오픈, 주문 폭주처럼 요청이 한 번에 몰리는 경우가 있습니다.
이때 모든 작업을 즉시 처리하려고 하면 DB나 외부 API에 부하가 몰립니다.
메시지 큐는 쉽게 말해 작업 대기줄입니다.
요청을 일단 대기줄에 쌓아두고, 처리 가능한 만큼 꺼내서 처리할 수 있습니다.
주문 서비스가 결제, 재고, 알림 서비스를 직접 호출하면 주문 서비스는 상대 기능의 주소, 응답 형식, 장애 상황을 모두 신경 써야 합니다.
비동기 메시징을 사용하면 주문 서비스는
**“주문이 생성되었다”**는 메시지만 남깁니다.
필요한 기능들이 그 메시지를 보고 각자 처리합니다.
즉,
**“내가 직접 너를 호출할게”**에서
**“일어난 일을 알려둘 테니 필요한 쪽이 처리해”**로 바뀝니다.
전문 용어로는 기능 사이의 결합도를 낮춘다고 말합니다.
주문은 정상적으로 생성되었는데 알림 서버 장애 때문에 주문 전체가 실패한다면 이상합니다.
이런 경우 알림 발송을 비동기로 분리하면 주문은 성공시키고, 알림은 나중에 다시 시도할 수 있습니다.
비동기 메시징은 장점만 있는 것은 아닙니다.
도입하면 새로 신경 써야 할 문제가 생깁니다.
비동기 처리에서는 모든 작업이 동시에 끝나지 않습니다.
예를 들어 주문은 생성되었지만 재고 차감은 아직 처리 중일 수 있습니다.
또 알림 발송은 실패해서 재시도 중일 수 있습니다.
이처럼 지금 당장은 상태가 조금 어긋나도 시간이 지나면 맞춰지는 방식을
최종적 일관성이라고 합니다.
은행 이체처럼 “지금 바로 정확히 맞아야 하는 일”은 단순히 메시지 큐로 분리하기 어렵습니다.
메시징 시스템에서는 같은 메시지가 두 번 전달될 수 있습니다.
예를 들어 쿠폰 지급 메시지가 두 번 처리되면 쿠폰이 두 장 지급될 수 있습니다.
그래서 이미 처리한 메시지인지 확인하거나, 메시지 ID를 저장하는 방식이 필요합니다.
같은 메시지가 여러 번 와도 결과가 달라지지 않게 만드는 것을
멱등성이라고 합니다.
비동기 메시지는 처리 담당자 쪽에서 조용히 실패할 수 있습니다.
그래서 실패한 메시지 수, 재시도 횟수, 처리 지연 시간 등을 확인할 수 있어야 합니다.
계속 실패한 메시지는 따로 보관해야 합니다.
이를 보통 DLQ, 즉 실패 메시지 보관함이라고 부릅니다.
DLQ를 확인하지 않으면 나중에 “왜 알림이 안 갔지?”, “왜 통계가 빠졌지?” 같은 문제로 발견될 수 있습니다.
간단한 주문 시스템으로 정리해보겠습니다.
주문 API 안에서 모든 작업을 처리합니다.
- 주문 생성
- 결제 요청
- 재고 차감
- 알림 발송
- 통계 반영
이 구조에서는 알림이나 통계가 느려져도 주문 응답이 느려질 수 있습니다.
주문 서비스는 주문 생성 후 OrderCreated 메시지만 발행합니다.
그 뒤에는 각 기능이 메시지를 보고 처리합니다.
- 결제 서비스: 결제 처리
- 재고 서비스: 재고 차감
- 알림 서비스: 주문 알림 발송
- 통계 서비스: 주문 통계 집계
이렇게 하면 주문 서비스는 핵심 책임에 집중할 수 있습니다.
다만 실패 처리, 재시도, 중복 메시지 방지는 반드시 함께 설계해야 합니다.
이제 이 흐름을 Spring Boot 콘솔 예제로 확인해보겠습니다.
예제 프로젝트는 async-messaging 폴더에 있습니다.
cd async-messaging
mvn spring-boot:run이 예제는 Kafka나 RabbitMQ를 설치하지 않습니다.
Spring 안에 있는 이벤트 기능으로 “메시지를 남기고 다른 쪽에서 처리한다”는 흐름만 간단히 보여줍니다.
콘솔에서 빨간색 [집중] 로그를 중심으로 보면 됩니다.
특히 다음 장면을 보시면 됩니다.
- 동기 방식은 모든 작업이 끝난 뒤에야 응답한다
- 비동기 방식은
OrderCreated메시지만 남기고 먼저 응답한다 - 결제, 재고, 알림, 통계 담당자가 별도 스레드에서 처리한다
- 같은 메시지가 두 번 와도 이미 처리한 담당자는 건너뛴다
- 알림 실패는 재시도하고, 계속 실패하면 DLQ에 저장한다
이 예제는 운영용 메시징 시스템은 아닙니다.
하지만 비동기 메시징의 핵심 흐름을 이해하기에는 충분합니다.
비동기 메시징은 무조건 성능을 올려주는 기술이 아닙니다.
핵심은
일을 나누고, 장애가 퍼지는 것을 줄이고, 실패한 작업을 다시 처리할 수 있게 만드는 것입니다.
도입 전에는 다음 질문을 확인해야 합니다.
이 작업은 반드시 지금 처리되어야 하는가?
실패했을 때 다시 시도해도 되는가?
같은 메시지가 두 번 와도 안전한가?
잠깐 상태가 맞지 않아도 괜찮은가?
실패한 메시지를 찾고 다시 처리할 수 있는가?
이 질문에 답할 수 있을 때 비동기 메시징은 좋은 선택이 될 수 있습니다.
감사합니다.