B2B 고객사를 대상으로 대용량 알림·청구 데이터를 접수받아 처리하는 API 게이트웨이에서, 기존에는 요청 한 건을 처음부터 끝까지 동기(Synchronous)로 처리했습니다.
text
[기존 100% 동기 처리 파이프라인]API 수신 → DB 저장 (요청 원문 + 페이로드 전체) → 내부 비즈니스 로직 수행 → MQ 통신 (외부 처리 엔진 호출, 응답 대기) → 결과 DB 저장 → API 결과 응답
이 구조에서는 위 6단계가 하나의 요청 스레드 위에서 순차적으로 진행되므로, 한 구간이라도 지연되면 그 시간이 그대로 최종 응답 레이턴시에 누적됩니다.
관찰된 병목 현상
구간
증상
원인
DB 저장
대용량 페이로드 Insert 시 커넥션 점유 시간 급증, 커넥션 풀 고갈
수 MB 단위 BLOB/CLOB을 요청 경로에서 동기 Insert
MQ 통신
공용 큐 채널에 일반 트래픽과 긴급 트래픽이 섞여 대기열 폭증
우선순위 구분 없는 단일 채널, 소비자 처리량 한계
장애 전파
특정 구간(주로 MQ) 지연이 전체 API 먹통으로 확산
Timeout 미설정으로 요청 스레드 무한 대기 → 스레드 풀 고갈
트래픽 특성
접수(Command): 최대 1,000 TPS 수준의 버스트성 대량 인입
결과 조회(Query): 약 30 TPS, 고객사가 배치성으로 폴링
즉 쓰기와 읽기의 부하 편차가 30배 이상이며, 동일 파이프라인·동일 자원으로 두 트래픽을 감당하고 있었음
핵심 해결 과제
응답 레이턴시 안정화: 데이터 처리 병목이 API 응답 시간에 직접 전이되지 않도록 접수와 실제 처리를 분리한다.
자원 격리: DB·MQ에 부하가 몰려도 API 스레드 풀이 고갈되지 않도록 방어선을 만든다.
부분 장애 허용: 특정 컴포넌트 장애 시 전체 다운이 아니라 부분 지연으로 degrade 되도록 설계한다.
2. 전체 API 처리 Flow Diagram
비동기 처리 아키텍처 흐름
기존 병목을 해결하기 위해, 비동기 기반 아키텍처로 개편했습니다.
B2B 고객의 특성상 즉각적인 최종 결과 응답이 필수가 아니므로, 접수 시점에는 접수 확인(Ack)만 즉시 반환하고 실제 처리 결과는 별도 조회 API로 확인하도록 설계했습니다. (CQRS의 명령/조회 분리 관점 적용)
설계 포인트
접수 응답 경로 최소화: 요청 경로에서 하는 일은 "필수키 저장 + 페이로드 staging + 처리요청 발행 + Ack 반환" 4가지로 고정. 무거운 작업은 전부 Worker 경로로 밀어냄.
접수 ID 발급: 202 Accepted와 함께 접수 ID(트래킹 키)를 반환하여, 고객사가 이후 조회 API로 상태를 추적.
읽기/쓰기 자원 분리 지향: 조회 트래픽은 별도 API·별도 커넥션 풀(필요 시 리드 레플리카)로 분리하여 접수 부하와 간섭하지 않도록 함.
3. 병목 구간별 해결 전략 및 다이어그램
주요 해결 방안은 ① DB 분할 저장, ② MQ 우선순위 채널 분리, ③ Timeout 정책 및 방어 로직 3가지입니다.
3.1 DB 병목 현상 해결 (데이터 분할 저장)
대용량 데이터의 DB Insert로 인한 커넥션 점유·병목을 방지하기 위해, 데이터의 크기·중요도에 따라 저장 시점과 경로를 이원화했습니다.
해결 방법
필수·경량 데이터는 동기 즉시 저장: PK·슈퍼키 역할을 하는 가벼운 필수 데이터(접수 ID, 고객사 ID, 상태값, 라우팅 키)는 요청 경로에서 즉시 저장하여 데이터 무결성과 후속 조회·라우팅 기능을 보장합니다.
대용량 페이로드는 파일 시스템에 선(先)적재: 페이로드가 큰 데이터는 서버 내부 파일 시스템의 Staging 영역에 먼저 기록하고, 위치·해시만 인덱스 테이블에 남깁니다. 요청 경로에서 대용량 Insert가 사라지므로 커넥션 점유 시간이 짧아집니다.
DB 여유 상태 기반 비동기 반영: 커넥션 사용률·부하 지표가 임계치 아래일 때만, 배치/스케줄러가 페이로드를 Insert/Update(결과 Sync)합니다. 부하가 높으면 백오프 후 재시도하여 DB에 대한 백프레셔(Backpressure)를 구현합니다.
⚠️ 트레이드오프: 파일 시스템 선적재는 "DB에 아직 없는 구간"을 만들므로, ① 파일 유실 대비(주기적 정합성 점검·재적재), ② 조회 API가 "처리중/적재중" 상태를 명확히 반환하도록 상태 모델을 설계해야 합니다.
3.2 MQ 통신 병목 해결 (우선순위 기반 채널 분리)
공용 Queue 채널 하나로 모든 트래픽을 흘려보내면, MQ 서버 병목은 DB보다 더 심각하게(모든 처리의 관문이므로) 나타납니다. 이를 우선순위 발급 + 채널 분리로 완화했습니다.
해결 방법
Priority(우선순위) 발급 기능 추가: API의 성격(실시간성 여부)과 고객사 SLA를 기준으로 요청마다 우선순위를 부여합니다.
mqAgent·Dispatcher 로직 수정: 즉시 처리가 필요한 건은 Fast Channel로, 일반 대량 처리 건은 기존 Normal Channel로 라우팅하여 트래픽을 물리적으로 분산합니다. Fast Channel은 전용 소비자 풀을 두어 일반 트래픽 적체의 영향을 받지 않습니다.
과부하 시 등급 강등: 전체 큐 적체가 임계치를 넘으면 Normal 채널 소비율을 낮추고 Fast 채널 처리량을 우선 보장하여, 중요 트래픽의 지연을 방어합니다.
3.3 Timeout 정책 및 방어 로직 (장애 확산 방지)
시스템 내부 특정 병목이 전체 API 통신 먹통으로 이어지지 않도록 하는 안전장치입니다.
해결 방법
구간별 Timeout 명시: 각 통신 구간(특히 MQ 통신, 비동기 처리 호출)에 Timeout을 명확히 설정합니다.
무한 대기 차단: 병목 발생 시 즉시 Timeout 에러 응답을 반환하여 API 스레드가 즉시 반납되도록 합니다. 스레드 풀 고갈 → 전체 API 마비로 가는 연쇄를 끊습니다.
선제 알림: 별도 Response 경로로 병목 구간·발생 건수를 모니터링에 전송하고, 서버 담당자에게 알림이 고지되도록 하여 장애 대응 리드타임을 단축합니다.
반복 임계 시 자동 조치: 동일 구간 Timeout이 임계치 이상 반복되면 해당 채널을 일시 강등/차단하여 장애 전파를 막습니다. (아래 Circuit Breaker 변형)
4. 표준 패턴 관점의 정리 (Result & Design Rationale)
해결책은 단순한 편법이 아니라, 고부하 분산 시스템에서 널리 쓰이는 표준 패턴을 환경에 맞게 구현한 것입니다.
4.1 CQRS (명령과 조회의 분리) 관점 적용
API 접수(Command)와 결과 조회(Query)를 분리된 경로·분리된 자원으로 처리합니다.
이를 통해 1,000 TPS(쓰기) / 30 TPS(읽기) 라는 30배 이상의 트래픽 편차를 한 파이프라인에서 안정적으로 소화하는 비동기 파이프라인을 구축했습니다.
쓰기 경로는 "빠른 Ack"에, 읽기 경로는 "일관된 상태 응답"에 각각 최적화됩니다.
4.2 Graceful Degradation ( 의도된 성능 저하)
DB·MQ에 부하가 몰려도 시스템 전체가 다운되지 않습니다.
파일 시스템 선적재(백프레셔) + 채널 우선순위 강등 + Timeout을 통해, 장애가 "전면 중단"이 아니라 "부분 지연"으로만 나타나도록 설계하여 장애 허용력(Fault Tolerance)을 높였습니다.
4.3 Circuit Breaker 패턴의 변형 적용
Timeout 정책으로 무한 대기를 끊고, 동일 구간 실패가 반복되면 채널을 강등/차단하며, 서버 담당자에게 선제 알림을 고지합니다.
완전한 상태 기계(Closed/Open/Half-Open) 구현은 아니지만, 장애 구간을 격리하여 전파를 막는다는 서킷 브레이커의 핵심 효과를 얻어 MSA 환경의 연쇄 장애(Cascading Failure)를 방어합니다.
댓글
GitHub 계정으로 의견이나 질문을 남길 수 있습니다.