infra
Platform

모듈 맵

[Infra Ops] 트래픽 제어와 이벤트 기반 연계 구조

0 / 52 완료

펼치기
0 / 52 완료0%

Infra-ops · 25 / 52

[Infra Ops] 트래픽 제어와 이벤트 기반 연계 구조

API Gateway 역할과 rate limiting, Webhook 수신 구조와 재시도 패턴까지

🚨INCIDENT ALERT
HIGH

결제 서비스사에서 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가 필요한가

API Gateway가 처리하는 공통 기능:

기능설명
인증/인가JWT 검증, API Key 확인 → 유효하지 않으면 401/403 반환
Rate Limiting클라이언트별 초당/분당 요청 수 제한 → 초과 시 429 반환
라우팅URL 경로 기반으로 적합한 백엔드 서비스로 프록시
로깅/추적모든 요청/응답 기록, 분산 트레이싱 헤더 주입
SSL TerminationHTTPS를 게이트웨이에서 종료, 내부는 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 설정:

Nginx
# /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}';
        }
    }
}

지수 백오프 재시도 로직 (클라이언트 측):

Python
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 예시):

JS
// 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);
});

Webhook 패턴 — Push vs Pull 비교

실습 — Rate Limiting과 Webhook 테스트

1Nginx Rate Limiting 설정 확인

현재 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' 메시지가 찍히는가
2간단한 Webhook 수신 서버 테스트

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 연계 실무를 다룹니다.

지식 확인

퀴즈 — 4문제

Q1

API Gateway를 서비스 앞에 두는 가장 핵심적인 이유는?

Q2

API 호출 시 429 Too Many Requests 에러를 받았을 때 올바른 재시도 전략은?

Q3

Webhook과 Polling의 차이로 가장 적절한 것은?

Q4

Webhook 수신 서버에 멱등성(idempotency)이 필요한 이유는?

0 / 4 답변

🧪 실습으로 확인하기

Nginx 설치 및 기동

초급

Linux 서버에 Nginx를 설치하고 systemd 서비스로 등록하여 80포트에서 응답하는 상태까지 만든다.

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

이것도 배워보세요

infra-ops고급 · 65
[Infra Ops] SAML/OAuth 기반 싱글사인온 구성과 장애 분석
인프라 서비스 운영 트랙 계속
linux입문 · 30
[Linux] 개발자가 왜 리눅스 서버와 커맨드라인을 반드시 배워야 하는가
Linux 트랙 시작점