결제 서비스사에서 Webhook으로 결제 완료 이벤트를 보내준다고 합니다. 그런데 가끔 주문이 두 개 생성된다는 버그 신고가 들어옵니다. 로그를 보니 동일한 결제 완료 이벤트가 두 번 수신됐습니다. 한편 API Gateway 앞단에서는 특정 IP가 갑자기 초당 100개 요청을 보내 서버가 느려지는 일도 생겼습니다. 두 문제 모두 API Gateway와 Webhook 설계를 이해하면 해결됩니다.
이 모듈에서는 API Gateway의 역할과 Nginx Rate Limiting 설정, Webhook 수신 요구사항과 멱등성 패턴, 지수 백오프 재시도 전략을 다룹니다.
- 1API Gateway가 서비스 앞에 위치하는 이유와 처리하는 공통 기능을 설명할 수 있다
- 2Nginx limit_req_zone으로 Rate Limiting을 설정하고 동작을 확인할 수 있다
- 3Webhook 수신 서버의 필수 요구사항(응답 시간, 멱등성, HTTPS)을 설명할 수 있다
- 4429 응답 시 지수 백오프 재시도 전략을 설계하고 구현할 수 있다
- 5Webhook 중복 수신을 멱등키(idempotency key)로 처리하는 방법을 적용할 수 있다
API Gateway의 역할
왜 API Gateway가 필요한가
마이크로서비스 환경에서 각 서비스가 인증, Rate Limiting, 로깅, CORS를 개별적으로 구현하면 코드 중복이 발생하고 정책 일관성이 깨집니다. API Gateway는 모든 외부 요청의 단일 진입점(Single Entry Point)으로, 이런 공통 관심사를 한 곳에서 처리합니다.

API Gateway가 처리하는 공통 기능:
| 기능 | 설명 |
|---|---|
| 인증/인가 | JWT 검증, API Key 확인 → 유효하지 않으면 401/403 반환 |
| Rate Limiting | 클라이언트별 초당/분당 요청 수 제한 → 초과 시 429 반환 |
| 라우팅 | URL 경로 기반으로 적합한 백엔드 서비스로 프록시 |
| 로깅/추적 | 모든 요청/응답 기록, 분산 트레이싱 헤더 주입 |
| SSL Termination | HTTPS를 게이트웨이에서 종료, 내부는 HTTP |
| 요청 변환 | 헤더 추가/제거, 요청 본문 변환 |
서비스 구조:
클라이언트
│
▼
[API Gateway] ← 인증, Rate Limiting, 로깅
│
├─► /api/users/* → User Service (내부)
├─► /api/orders/* → Order Service (내부)
└─► /api/products/* → Product Service (내부)
주요 API Gateway 제품 비교:
| 제품 | 특징 | 적합 상황 |
|---|---|---|
| Kong | 플러그인 기반, DB 또는 DB-less 모드 | 엔터프라이즈, 다양한 플러그인 필요 |
| AWS API Gateway | 완전관리형, Lambda 연동 용이 | AWS 환경, 서버리스 아키텍처 |
| Nginx | 가볍고 유연, 직접 설정 필요 | 단순 라우팅/Rate Limiting, 소규모 |
| Traefik | 컨테이너 친화적, 자동 서비스 디스커버리 | Kubernetes/Docker 환경 |
Rate Limiting — 남용 방지와 서버 보호
Nginx limit_req_zone으로 Rate Limiting 구현
Rate Limiting 없이 서비스를 운영하면 악의적인 요청이나 버그가 있는 클라이언트가 서버 자원을 독점합니다. Nginx의 limit_req_zone은 클라이언트별로 요청 속도를 제한하는 방식으로, 별도 Gateway 없이 Nginx만으로도 기본적인 보호가 가능합니다.
Nginx Rate Limiting 설정:
# /etc/nginx/nginx.conf 또는 /etc/nginx/conf.d/rate-limit.conf
http {
# ── Rate Limit Zone 정의 ──────────────────────────
# $binary_remote_addr: 클라이언트 IP (바이너리 형식, 메모리 절약)
# zone=api_limit:10m: 'api_limit'이란 이름, 10MB 공유 메모리 (약 16만 IP 저장 가능)
# rate=10r/s: 초당 10개 요청 허용
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
# 로그인 같은 민감한 엔드포인트는 더 엄격하게
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;
server {
listen 80;
# ── API 엔드포인트에 Rate Limiting 적용 ──────
location /api/ {
# burst=20: 순간적으로 20개까지 큐에 대기 허용
# nodelay: 큐 대기 없이 즉시 처리 (burst 초과분은 429)
limit_req zone=api_limit burst=20 nodelay;
limit_req_status 429; # 초과 시 반환할 상태 코드
proxy_pass http://backend;
}
# 로그인은 더 엄격하게
location /api/login {
limit_req zone=login_limit burst=3 nodelay;
limit_req_status 429;
proxy_pass http://backend;
}
# Rate Limit 초과 응답 커스터마이징
error_page 429 @rate_limit_exceeded;
location @rate_limit_exceeded {
default_type application/json;
return 429 '{"error":"too_many_requests","message":"요청 한도를 초과했습니다. 잠시 후 다시 시도하세요.","retry_after":1}';
}
}
}
지수 백오프 재시도 로직 (클라이언트 측):
import time
import requests
def call_api_with_retry(url, max_retries=5):
for attempt in range(max_retries):
try:
response = requests.get(url, timeout=10)
if response.status_code == 429:
# Retry-After 헤더가 있으면 그 시간만큼 대기
retry_after = int(response.headers.get('Retry-After', 0))
wait_time = retry_after if retry_after > 0 else (2 ** attempt)
print(f"Rate limited. Waiting {wait_time}s (attempt {attempt + 1})")
time.sleep(wait_time)
continue
return response
except requests.exceptions.Timeout:
wait_time = 2 ** attempt # 1, 2, 4, 8, 16초
print(f"Timeout. Waiting {wait_time}s (attempt {attempt + 1})")
time.sleep(wait_time)
raise Exception(f"API 호출 실패: {max_retries}회 재시도 초과")
Webhook 수신 구조
Webhook의 동작 방식과 수신 서버 요구사항
결제 서비스사에서 "Webhook이 응답이 없어 재전송하고 있다"는 연락이 왔습니다. 동시에 주문이 두 개씩 생성된다는 버그 신고도 들어옵니다. Webhook 수신 서버가 처리하는 데 3초씩 걸렸고, 그 사이 발신측이 재시도를 했습니다. "2초 내 응답"과 "멱등성"을 모르면 Webhook을 제대로 운영할 수 없습니다.
Webhook은 이벤트 주도 방식입니다. 우리가 외부 서비스에 "이 URL로 이벤트 보내줘"라고 등록하면, 이벤트(결제 완료, 배송 시작, SMS 발송 결과 등) 발생 시 외부 서비스가 우리 서버로 HTTP POST를 보냅니다.
Webhook 흐름:
결제 서비스 (발신측) 우리 서버 (수신측)
│ │
결제 완료 이벤트 발생 │
│ │
├── POST /webhook/payment ─►│
│ {event: "payment.done", │
│ order_id: "ORD-001", │
│ amount: 50000} │
│ │ 2초 내 200 OK 응답 (필수)
│◄── HTTP 200 OK ───────────┤
│ │ (이후 비동기로 실제 처리)
Webhook 수신 서버의 핵심 요구사항:
| 요구사항 | 이유 | 미충족 시 |
|---|---|---|
| 2초 내 응답 | 발신측 timeout 방지 | timeout → 실패로 판단 → 재시도 → 중복 이벤트 |
| 멱등성 | 재시도 시 중복 처리 방지 | 결제 두 번 처리, 이메일 두 번 발송 등 |
| HTTPS | 이벤트 데이터 암호화 | 대부분의 외부 서비스가 HTTPS만 허용 |
| 서명 검증 | 위조 요청 차단 | 공격자가 결제 완료 이벤트 위조 가능 |
멱등성 구현 패턴 (Node.js/Express 예시):
// webhook 수신 핸들러
const processedEvents = new Set(); // 실제로는 Redis나 DB 사용
app.post('/webhook/payment', async (req, res) => {
// 1. 즉시 200 OK 응답 (2초 제한 준수)
res.status(200).json({ received: true });
// 2. 이후 비동기로 실제 처리
const eventId = req.headers['x-event-id'] || req.body.event_id;
// 3. 중복 체크 (멱등성)
if (processedEvents.has(eventId)) {
console.log(`중복 이벤트 무시: ${eventId}`);
return;
}
// 4. 서명 검증 (발신측에서 제공하는 시크릿 사용)
const signature = req.headers['x-webhook-signature'];
const expectedSig = hmac('sha256', process.env.WEBHOOK_SECRET, JSON.stringify(req.body));
if (signature !== expectedSig) {
console.error('서명 불일치 — 위조 요청 차단');
return;
}
// 5. 처리 완료 기록
processedEvents.add(eventId);
// 6. 실제 비즈니스 로직 (비동기)
await processPaymentComplete(req.body);
});

실습 — Rate Limiting과 Webhook 테스트
현재 Nginx 설정에서 Rate Limiting 관련 지시어를 확인합니다.
# Nginx 전체 설정에서 Rate Limiting 설정 확인
nginx -T | grep limit_req
# limit_req_zone 정의와 limit_req 적용 위치 확인
nginx -T | grep -E "limit_req_zone|limit_req "
# Rate Limiting 동작 테스트 (빠르게 11개 요청 → 일부 429)
for i in $(seq 1 15); do
curl -s -o /dev/null -w "Request $i: %{http_code}\n" http://localhost/api/test
done
# 처음 10개는 200, 이후는 429 또는 503 (설정에 따라 다름)
nginx -T | grep limit_req- nginx -T 출력에 limit_req_zone 정의가 있는가
- rate=10r/s 또는 비슷한 제한 값이 설정됐는가
- 반복 요청 테스트에서 일부 요청이 429를 반환하는가
- Nginx error.log에 'limiting requests' 메시지가 찍히는가
nc(netcat)로 임시 수신 서버를 열고 Webhook 요청을 받아봅니다.
# 터미널 1: nc로 8888 포트 수신 대기
nc -l -p 8888
# 터미널 2: 별도 창에서 Webhook 발송 테스트
curl -X POST http://localhost:8888/webhook/test \
-H "Content-Type: application/json" \
-H "X-Event-ID: evt-001" \
-d '{"event":"payment.done","order_id":"ORD-001","amount":50000}'
# 터미널 1에서 수신된 HTTP 요청 전체 내용 확인:
# POST /webhook/test HTTP/1.1
# Host: localhost:8888
# Content-Type: application/json
# X-Event-ID: evt-001
# ...
# {"event":"payment.done","order_id":"ORD-001","amount":50000}
nc -l -p 8888- nc 터미널에 HTTP 요청 헤더가 표시됐는가
- X-Event-ID 헤더가 전달됐는가
- 요청 본문(JSON)이 정확하게 수신됐는가
- 실제 Webhook 수신 서버라면 2초 내 200 OK를 반환해야 함을 확인했는가
트러블슈팅
원인: 백엔드 서비스 프로세스는 살아있지만 응답이 지연되거나(응답 timeout 초과), keepalive 연결이 끊어진 상태에서 Gateway가 재연결하기 전에 요청이 들어온 경우입니다. 또는 백엔드가 Gateway의 IP에서 오는 연결을 방화벽으로 차단한 경우입니다.
# Gateway의 업스트림 에러 로그 확인
grep "upstream" /var/log/nginx/error.log | tail -20
# 출력 예: [error] upstream timed out (110: Connection timed out)
# [error] connect() failed (111: Connection refused)
# 백엔드 서버 직접 접속 테스트 (Gateway 우회)
curl -v http://192.168.10.11:8080/actuator/health
# → 200이면 백엔드는 정상, Gateway→백엔드 구간 문제
# Gateway 서버에서 백엔드로 TCP 연결 확인
nc -zv 192.168.10.11 8080
# Nginx proxy timeout 설정 확인 및 조정
nginx -T | grep -E "proxy_(connect|send|read)_timeout"
# 너무 짧으면 증가
# proxy_connect_timeout 10s;
# proxy_read_timeout 60s;
해결: proxy_read_timeout을 백엔드 응답 시간에 맞게 조정하고, upstream의 keepalive 설정을 추가합니다. 방화벽 문제라면 Gateway IP에서 백엔드 포트로의 inbound 규칙을 확인합니다.
원인: Webhook 수신 서버의 응답이 2초를 넘기는 경우 발신측이 timeout으로 판단하고 재전송합니다. 수신 서버가 응답을 보내기 전에 이미 처리를 시작했다면, 재전송 이벤트가 오면 동일 주문이 두 번 처리됩니다.
# 수신 서버 응답 시간 확인
curl -w "\nTime: %{time_total}s\n" -X POST http://localhost/webhook/payment \
-H "Content-Type: application/json" \
-d '{"event":"payment.done","order_id":"ORD-TEST"}'
# 2초 이상이면 발신측이 timeout으로 처리
# 애플리케이션 로그에서 중복 이벤트 확인
grep "ORD-001" /var/log/app/webhook.log
# 동일 order_id가 두 번 이상 나타나면 중복 처리 중
# 해결 방안 확인:
# 1. 응답을 먼저 보내고 처리는 비동기로 → 즉시 200 OK
# 2. 이벤트 ID를 DB나 Redis에 기록하여 중복 체크
# 예: Redis SETNX로 멱등키 설정 (이미 있으면 중복)
# redis-cli SET webhook:evt-001 1 NX EX 86400
# → (nil)이면 이미 처리된 이벤트
해결: Webhook 핸들러를 "즉시 200 응답 → 비동기 처리" 패턴으로 변경하고, 이벤트 ID를 처리 완료 후 DB 또는 Redis에 저장하여 중복 수신 시 건너뛰는 멱등성 로직을 추가합니다.
실제 업무에서 이 지식이 쓰이는 상황:
결제 연동이나 배송 추적, SMS 발송 결과 수신 등 외부 서비스 연동은 대부분 Webhook으로 이루어집니다. 신입 개발자나 주니어 인프라 담당자가 가장 자주 실수하는 부분이 "2초 내 응답"과 "멱등성" 두 가지입니다.
Webhook 수신 서버 구성 체크리스트:
# 1. 응답 시간 확인 (2초 이내여야 함)
curl -w "응답시간: %{time_total}s\n" -X POST http://localhost/webhook/test \
-H "Content-Type: application/json" -d '{}'
# 2. HTTPS 설정 확인 (대부분 외부 서비스가 요구)
curl -vI https://our-server.com/webhook/payment 2>&1 | grep -E "SSL|TLS|Verify"
# 3. Webhook URL을 외부에서 접근 가능한지 확인
curl -m 5 https://our-server.com/webhook/payment
# Connection refused → 방화벽 또는 포트 미오픈
# 4. Rate Limiting 현황 확인
nginx -T | grep limit_req
API Gateway 장애 시 빠른 확인 순서:
# Gateway 상태
systemctl status nginx kong
# 최근 에러 로그
tail -50 /var/log/nginx/error.log | grep -E "upstream|timeout|502|504"
# 백엔드 직접 접속 테스트
curl http://백엔드서버:포트/health
API Gateway와 Webhook을 올바르게 구성하면 서비스 안정성이 크게 높아집니다. 다음 모듈에서는 NICE 본인인증, SMS 게이트웨이, 전자서명 같은 외부 기관 API 연계 실무를 다룹니다.