API Gateway
Reverse Proxy VS API Gateway VS Load Balancer
Reverse Proxy, API Gateway, Load Balancer는 모두 “클라이언트와 서버 사이에 위치해 요청을 대신 받아 처리한다”는 공통점 때문에 자주 혼동되지만, 각각 만들어진 목적과 다루는 관심사의 층위가 다르다. 실제로는 하나의 제품(Nginx, Envoy, Kong, AWS API Gateway 등)이 여러 역할을 동시에 겸하는 경우도 많지만, 개념적으로는 아래와 같이 구분할 수 있다.
Reverse Proxy
- 정의: 클라이언트의 요청을 대신 받아서 내부망에 있는 서버로 전달하고, 그 응답을 다시 클라이언트에게 돌려주는 중개 서버이다.
- 역할과 목적
- 내부 서버의 실제 IP/구조를 클라이언트로부터 숨겨 보안을 강화한다.
- SSL Termination, 정적 파일 캐싱, 압축(Gzip) 등 서버 앞단에서 공통으로 처리할 수 있는 작업을 대신 수행해 백엔드 부담을 줄인다.
- 가장 기본적인 형태의 프록시로, “요청을 어디로 보낼지”보다 “요청을 안전하고 효율적으로 중개하는 것” 자체에 초점을 둔다.
Load Balancer
- 정의: Reverse Proxy의 개념을 확장해, 하나의 서버가 아니라 여러 대의 서버(서버 풀)에 트래픽을 분산시키는 역할을 전문으로 하는 컴포넌트이다.
- 역할과 목적
- Round Robin, Least Connections 등의 알고리즘으로 여러 서버에 트래픽을 고르게 분산한다.
- Health Check를 통해 장애가 발생한 서버를 트래픽 대상에서 자동으로 제외해 가용성을 높인다.
- 목적은 “여러 대의 동일한(혹은 유사한) 서버 간 부하 분산과 이중화”에 있으며, 요청 내용 자체를 깊이 해석하거나 비즈니스 로직에 관여하지 않는다.
API Gateway
- 정의: 여러 마이크로서비스로 구성된 백엔드 앞단에서, API 요청에 대한 진입점(Entry Point) 역할을 하며 인증/인가, 라우팅, 트래픽 제어 등 API 운영에 필요한 다양한 부가 기능을 처리하는 컴포넌트이다.
- 역할과 목적
- 단순 트래픽 분산을 넘어, 인증(Authentication), 인가(Authorization), Rate Limit, 파라미터 검증, 프로토콜 변환, 로깅/모니터링 등 API 자체의 운영 정책을 담당한다.
- 여러 마이크로서비스를 클라이언트에게 하나의 통합된 API처럼 보이게 해주는 창구(Facade) 역할을 한다.
- 목적은 “서비스 단위 트래픽 분산”이 아니라 “API 단위의 정책 집행과 서비스 오케스트레이션”에 있다.
차이 정리
| 구분 | Reverse Proxy | Load Balancer | API Gateway |
|---|---|---|---|
| 주요 목적 | 요청 중개, 내부 서버 은닉 | 여러 서버 간 부하 분산 | API 정책 집행, 서비스 오케스트레이션 |
| 관심 계층 | 네트워크/전송 계층 중심 | 네트워크/전송 계층 중심 (L4) 또는 애플리케이션 계층 (L7) | 애플리케이션 계층(L7), 비즈니스 로직에 가까움 |
| 대표 기능 | SSL Termination, 캐싱, 압축 | Health Check, 알고리즘 기반 분산 | 인증/인가, Rate Limit, 라우팅, 프로토콜 변환 |
| 트래픽 대상 | 단일 서버 또는 서버군 | 동일 서비스의 서버 풀 | 서로 다른 여러 마이크로서비스 |
| 비유 | 손님과 매장 사이의 안내 데스크 | 여러 계산대로 손님을 나눠 보내는 관리자 | 건물 로비의 보안 데스크 + 안내소(출입 통제, 목적지 안내) |
정리하면, Load Balancer는 Reverse Proxy의 한 종류로서 “부하 분산”에 특화된 것이고, API Gateway는 여기서 한 단계 더 나아가 트래픽 분산뿐 아니라 API 전반의 운영 정책(인증, 제어, 변환 등)까지 책임지는 상위 개념의 컴포넌트라고 볼 수 있다.
요청이 마이크로서비스까지 흘러가는 흐름
클라이언트 요청 하나가 실제로 마이크로서비스에 도달하기까지 이 세 컴포넌트를 순서대로 거친다고 생각하면 이해하기 쉽다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
┌────────┐
│ CLIENT │
└────────┘
│
▼
┌─────────────────────────────────┐
│ REVERSE PROXY │
│ (Entry Point / SSL Termination) │
└─────────────────────────────────┘
│
▼
┌───────────────────────────────────────────┐
│ LOAD BALANCER │
│ (Round Robin / Least Conn / Health Check) │
└───────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ API GATEWAY │
│ (Auth, Rate Limit, Routing, Protocol Conv.) │
└─────────────────────────────────────────────┘
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ SERVICE A │ │ SERVICE B │ │ SERVICE C │
└───────────┘ └───────────┘ └───────────┘
- Reverse Proxy 단계에서 TLS를 종료하고 내부 서버 구조를 감춘다.
- Load Balancer 단계에서 동일한 역할을 하는 서버 풀 중 하나로 요청을 분산한다(여기서는 API Gateway 자체가 여러 대로 이중화되어 있다고 가정).
- API Gateway 단계에서 인증/인가와 Rate Limit을 검사한 뒤, 요청 경로(URL, 헤더 등)를 보고 어떤 마이크로서비스로 라우팅할지 결정한다.
- 마지막으로 각 마이크로서비스(Service A/B/C)가 실제 비즈니스 로직을 처리하고 응답을 돌려준다.
API Gateway 역할
- Parameter Validation (파라미터 검증)
- 요청에 들어온 파라미터의 타입, 필수 여부, 형식(포맷)이 올바른지 API Gateway 단에서 먼저 검사한다.
- 잘못된 요청을 백엔드 서비스까지 보내지 않고 초입에서 걸러내므로, 각 마이크로서비스가 동일한 검증 로직을 중복 구현할 필요가 없어진다.
- Allow-list/Deny-list (접근 허용/차단 목록)
- 특정 IP, 클라이언트, API Key 단위로 접근을 허용하거나 차단하는 정책을 관리한다.
- 악성 트래픽이나 알려진 공격 출처를 백엔드에 도달하기 전에 원천 차단할 수 있어 보안 계층으로 활용된다.
- Authentication / Authorization (인증/인가)
- JWT, OAuth2, API Key 등을 이용해 요청을 보낸 주체가 누구인지(인증) 확인하고, 해당 리소스에 접근할 권한이 있는지(인가) 검사한다.
- 인증/인가 로직을 모든 마이크로서비스가 각자 구현하는 대신 Gateway 한 곳에서 통합 처리해 중복을 줄이고 정책을 일관되게 유지할 수 있다.
- Rate Limit (트래픽 제어)
- 클라이언트별/API별로 단위 시간당 허용 요청 수를 제한해 특정 사용자가 시스템 전체 리소스를 독점하지 못하게 막는다.
- 급격한 트래픽 폭주(의도적 공격이든 실수든)로부터 백엔드 서비스를 보호하는 방어선 역할을 한다.
- Routing (라우팅)
- 요청 경로(URL), HTTP 메서드, 헤더 등을 기준으로 어떤 마이크로서비스로 요청을 전달할지 결정한다.
- 클라이언트는 개별 서비스의 위치를 몰라도 되고, Gateway가 단일 진입점 역할을 하며 내부 라우팅 규칙을 관리한다.
- Service Discovery (서비스 디스커버리)
- 컨테이너 환경 등에서 서비스 인스턴스의 IP/포트가 동적으로 바뀌는 상황에서, 현재 살아있는 인스턴스 목록을 서비스 레지스트리(Eureka, Consul 등)로부터 조회해 라우팅에 반영한다.
- 인스턴스가 스케일 인/아웃되거나 재배포되어도 Gateway가 최신 상태를 추적하므로 안정적인 트래픽 전달이 가능하다.
- Protocol Conversion (프로토콜 변환)
- 클라이언트가 사용하는 프로토콜(REST/HTTP)과 내부 마이크로서비스가 사용하는 프로토콜(gRPC, WebSocket 등)이 다를 경우 이를 상호 변환해준다.
- 클라이언트와 백엔드가 서로 다른 기술 스택을 사용하더라도 Gateway가 중간에서 통역해주므로 자유롭게 프로토콜을 선택할 수 있다.
- Error Handling (에러 처리)
- 백엔드 서비스에서 발생한 에러를 클라이언트에게 그대로 노출하지 않고, 일관된 형식의 에러 응답으로 가공해서 반환한다.
- 내부 구현 세부사항(스택 트레이스, 내부 서버 정보 등)이 외부에 유출되는 것을 방지한다.
- Circuit Break (서킷 브레이커)
- 특정 백엔드 서비스가 응답 지연이나 장애를 반복할 경우, 해당 서비스로의 요청을 일시적으로 차단하고 빠르게 실패 처리(Fail Fast)한다.
- 장애가 발생한 서비스 하나 때문에 요청이 계속 쌓여 시스템 전체로 장애가 전파되는 연쇄 장애(Cascading Failure)를 막는다.
- Logging / Monitoring (로깅/모니터링)
- 모든 요청과 응답이 Gateway를 거치므로, 이 지점에서 트래픽 로그와 메트릭(응답 시간, 에러율 등)을 중앙에서 수집할 수 있다.
- 각 마이크로서비스마다 로깅을 따로 구현하지 않아도 전체 API 트래픽에 대한 가시성(Observability)을 확보할 수 있다.
- Cache (캐싱)
- 자주 요청되지만 자주 바뀌지 않는 응답을 Gateway 단에서 캐싱해두고, 동일한 요청이 다시 오면 백엔드까지 가지 않고 즉시 응답한다.
- 백엔드 서비스의 부하를 줄이고 클라이언트가 체감하는 응답 속도를 크게 개선할 수 있다.
API Gateway Rate Limit 설계
예시 상황
인기 아이돌 콘서트 티켓을 판매하는 예매 서비스가 있다고 가정하자. 사용자는 보통 공연 목록 조회(GET /events) → 좌석 조회(GET /seats) → 예매 확정(POST /bookings) 순서로 이동한다. 평소에는 초당 수십 건 수준의 트래픽이지만, 티켓 오픈 시각이 되면 동시에 수만 명이 몰려 다음과 같은 문제가 발생한다.
- 예매 확정 API(
POST /bookings)에 짧은 시간에 트래픽이 집중되어 DB 커넥션이 고갈되고 전체 서비스가 느려진다. - 매크로/봇을 이용한 사용자가 초당 수백 건씩 요청을 보내 정상 사용자보다 압도적으로 유리한 위치를 점한다.
- 좌석 조회 API(
GET /seats)처럼 자주 호출되지만 상대적으로 가벼운 API까지 같은 기준으로 제한하면 사용자 경험이 나빠진다. - 공연 목록 조회 API(
GET /events)는 로그인 여부와 상관없이 누구나 들어오는 완전 공개 API라, 랜딩 페이지처럼 가장 많은 트래픽이 몰리는 지점이면서도 가장 가벼운 조회이기 때문에 다른 API와 같은 기준으로 제한하면 안 된다.
이런 상황에서 API Gateway는 “누구에게, 어떤 API에, 얼마나 많은 요청을 허용할 것인가”를 결정하는 트래픽 통제 지점 역할을 해야 한다.
설계 목표
- 공정성: 특정 사용자(혹은 봇)가 트래픽을 독점해 다른 사용자의 예매 기회를 빼앗지 못하게 한다.
- 백엔드 보호: 예매 확정처럼 DB 쓰기가 발생하는 무거운 API를 시스템이 감당 가능한 수준으로만 통과시킨다.
- 차등 적용: 모든 API를 동일한 기준으로 제한하지 않고, API의 특성(읽기/쓰기, 중요도)에 맞게 제한 수준을 다르게 가져간다.
설계 단계
1) 제한 기준(Key) 정하기
- IP 주소만으로 제한하면 다수의 사용자가 같은 사내망/공유기(NAT)를 쓰는 경우 정상 사용자까지 함께 차단될 수 있고, 반대로 봇은 프록시를 돌려 IP를 바꿔가며 우회할 수 있다.
- 따라서 로그인된 사용자는 User ID(또는 API Key) 기준으로, 비로그인 사용자는 IP 기준으로 이중 적용하는 방식을 택한다.
2) 알고리즘 선택
아래에서 다룰 Fixed Window, Sliding Window, Token Bucket 세 가지 알고리즘 모두 별도의 백그라운드 타이머 없이, 요청이 들어오는 시점에 “저장해둔 마지막 상태 + 지금 시각”을 가지고 그 자리에서 계산한다(지연 계산, lazy evaluation). 사용자(키)가 수백만 개인 분산 환경에서 사용자마다 타이머를 돌리는 건 현실적이지 않기 때문이다.
Fixed Window
- 시간을 고정된 구간(예: 매분 0초~59초)으로 잘라 그 구간 안의 요청 수를 센다.
- 계산 시점: 요청이 오면
현재 시각이 몇 번째 구간에 속하는지계산해서 그 구간의 카운터를+1한다. Redis라면 구간 키에 TTL을 걸어두는 것만으로 리셋이 자동 처리되어 별도 스케줄러가 필요 없다. - 경계 버스트(Boundary Burst) 문제: 구간이 리셋되는 순간을 기준으로 요청이 몰리면 실제 순간 트래픽이 한도의 2배까지 통과할 수 있다.
- 예:
limit=100/min일 때00:00:59에 100개를 보내 한도를 다 채우고, 1초 뒤00:01:00에 카운터가 리셋되자마자 다시 100개를 보내면 → 2초 사이에 200개가 통과한다. - “구간 내 합계”만 볼 뿐 “구간을 걸치는 짧은 시간 동안의 실제 순간 트래픽”은 전혀 보지 못하는 구조적 한계다.
- 예:
Sliding Window Log (정확한 방식)
- 요청마다 타임스탬프를 저장해두고, 매 요청마다
현재 시각 - 윈도우 크기보다 오래된 타임스탬프를 지운 뒤 남은 개수를 센다. - 계산 시점: 요청이 올 때마다 오래된 로그를 지우는 것(가지치기)과 카운트, 두 가지가 함께 이루어진다. 별도의 청소 작업이 없다.
- 위 Fixed Window 예시에 그대로 대입하면:
00:01:00에 “지금 기준 최근 60초”를 보면00:00:59에 쌓인 100개가 여전히 그 구간 안에 있으므로 이미 꽉 찬 상태 → 새 요청은 거부된다. 구간이 통째로 리셋되는 순간이 없어 경계 버스트가 발생하지 않는다. - 단점: 사용자마다 모든 요청의 타임스탬프를 저장해야 해서 트래픽이 많을수록 메모리 사용량이 커진다.
같은 트래픽 패턴(경계를 걸쳐 100개씩 두 번 몰림)을 두 알고리즘에 나란히 대입해보면 차이가 명확해진다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
scenario: limit = 100 requests / 60s window
requests arrive in two bursts: t=59s (100 reqs) and t=60s (100 reqs)
Fixed Window
window t=59s: [00:00, 01:00) t=60s: [01:00, 02:00) <- NEW window, counter reset
count before burst t=59s: 0 t=60s: 0
count after burst t=59s: 100 t=60s: 100
result t=59s: PASS (100/100) t=60s: PASS (100/100)
=> 200 requests actually got through within ~1 second around the boundary
Sliding Window (looks back 60s from "now")
window t=59s: [-1, 59] t=60s: [0, 60]
count before burst t=59s: 0 t=60s: 100 <- the t=59 burst is still inside this window
count after burst t=59s: 100 t=60s: 200 (exceeds limit)
result t=59s: PASS (100/100) t=60s: BLOCKED
=> never exceeds 100 requests within any rolling 60-second window
Sliding Window Counter (근사 방식, 실무에서 더 흔함)
타임스탬프를 전부 저장하는 대신, Fixed Window처럼 구간별 카운터 2개(직전 구간, 현재 구간)만 두고 가중 평균으로 근사치를 계산한다.
1 2 3 4
estimated_count = current_window_count + previous_window_count × overlap_ratio overlap_ratio = (window_size - elapsed_time_in_current_window) / window_size- 계산 시점: 요청이 올 때마다 “지금이 현재 구간에서 얼마나 지났는지(elapsed)”를 계산해서
overlap_ratio를 구하고 그 자리에서 추정치를 낸다. 직전 구간 값을 현재 구간으로 옮기는 별도 작업은 없다. 예 (
limit=100/min, 직전 구간에 100개가 몰렸다고 가정):현재 구간 시작 후 경과 overlap_ratio 추정 카운트 0초 ( 00:01:00)(60-0)/60 = 100% 100 → 새 요청 거부 15초 ( 00:01:15)(60-15)/60 = 75% 75 → 25개 정도 더 허용 45초 ( 00:01:45)(60-45)/60 = 25% 25 → 훨씬 여유 생김 - Fixed Window처럼 “0 아니면 100”으로 뚝 끊기지 않고 시간이 지날수록 직전 구간의 영향력이 서서히 줄어들어 한도가 연속적으로 회복된다.
- 장점: 사용자당 정수 카운터 2개만 저장하면 되어 메모리 효율이 좋다. 실제 요청 분포가 균등하다는 가정 하의 근사치이지만 Fixed Window의 극단적인 2배 버스트보다는 훨씬 안전하다.
Token Bucket
- 사용자마다 “토큰이 담기는 양동이(버킷)”를 두고, 일정 속도(
refill rate)로 토큰이 채워지며, 요청마다 토큰을 1개씩 소비한다. 토큰이 없으면 거부한다. 버킷에는capacity개까지만 담을 수 있다. 계산 시점: “10초마다 토큰 1개씩 채워진다”고 하면 마치 10초마다 실행되는 타이머가 있는 것처럼 보이지만, 실제로는 타이머가 없다. 마지막 계산 시각(
last_refill_time)과 그때의 토큰 개수만 저장해두고, 다음 요청이 왔을 때 그 사이 흐른 시간만큼 쌓였을 토큰을 한 번에 계산해서 따라잡는다.1 2
elapsed = now - last_refill_time new_tokens = min(capacity, stored_tokens + elapsed × refill_rate)
예 (
capacity=5,refill=1개/10초):시각 상황 계산 결과 t=5s요청 발생, 저장된 토큰=2 elapsed=0 2개 중 1개 소비 → 1개 남음, last_refill_time=5s저장t=25s다음 요청이 20초 뒤 도착 20/10=2개 추가→min(5, 1+2)=33개 중 1개 소비 → 2개 남음, last_refill_time=25s로 갱신t=5s~t=25s사이에는 아무 계산도 일어나지 않다가, 요청이 들어온t=25s시점에야 “20초 지났으니 2개 쌓였겠다”를 한 번에 계산한다. 아무도 요청을 안 보내면 그 계산 자체가 아예 일어나지 않는다.- 평소 요청이 없던 사용자는 토큰을
capacity만큼 쌓아둘 수 있어 순간 버스트를 일부 허용하면서도, 리필 속도를 넘어서는 요청은 아무리 반복해도 절대 지속적으로 통과하지 못해 장기 평균 속도는 확실히 제한된다. - 참고로 Leaky Bucket은 반대로 요청을 큐에 쌓아두고 항상 일정한 속도로만 빼서 처리해 버스트 자체를 허용하지 않는다는 점에서 Token Bucket과 다르다.
정리
| 알고리즘 | 경계 버스트 | 메모리 비용 | 이 시나리오에 적합한 이유 |
|---|---|---|---|
| Fixed Window | 있음 (최대 2배) | 낮음 | 구현은 쉽지만 예매 오픈처럼 순간 폭주가 예상되는 상황엔 불리 |
| Sliding Window (Log/Counter) | 없음/완화됨 | 중간~높음 | 경계 버스트를 막으면서도 사용자 경험을 해치지 않아 조회성 API에 적합 |
| Token Bucket | 의도적으로 일부 허용 | 낮음 | 순간 버스트는 소량 허용하되 평균 처리량을 강하게 제한할 수 있어 쓰기 API에 적합 |
- 예매 확정 API(
POST /bookings)처럼 백엔드 보호가 최우선인 API는 Token Bucket을 사용해 순간 버스트는 소량 허용하되 평균 처리량을 DB가 감당 가능한 수준으로 강하게 제한한다. - 좌석 조회 API(
GET /seats)처럼 상대적으로 가벼운 조회성 API는 Sliding Window로 조금 더 여유 있게 허용한다.
3) 엔드포인트별 차등 정책 적용
1
2
3
POST /bookings -> key: user_id, algo: Token Bucket, capacity=5, refill=1 token/10s
GET /seats -> key: user_id/ip, algo: Sliding Window, limit=60 req/min
GET /events -> key: ip, algo: Sliding Window, limit=120 req/min
- 쓰기 API(
/bookings)는 사용자당 “10초에 1번, 최대 5번까지 순간 버스트 허용”처럼 엄격하게 설정해 매크로의 연속 요청을 무력화한다. - 읽기 API(
/seats,/events)는 사용자 경험을 해치지 않도록 상대적으로 넉넉한 한도를 둔다.
4) 분산 환경에서의 카운터 관리
- API Gateway가 여러 대로 이중화되어 있다면 각 인스턴스가 로컬 메모리에 카운터를 두는 방식은 정확한 제한이 되지 않는다(인스턴스마다 별도로 세기 때문에 실제 허용량이 몇 배로 늘어남).
- Redis 같은 중앙 저장소에
INCR+EXPIRE(또는 Lua 스크립트로 원자적 처리)를 이용해 모든 Gateway 인스턴스가 같은 카운터를 공유하도록 설계한다.
5) 초과 시 응답 처리
- 한도를 초과한 요청에는
429 Too Many Requests상태 코드와 함께, 몇 초 후에 재시도하면 되는지 알려주는Retry-After헤더를 반환한다. - 정상 요청에도
X-RateLimit-Limit,X-RateLimit-Remaining같은 헤더를 함께 내려줘서 클라이언트(프론트엔드)가 남은 한도를 미리 파악하고 스스로 요청 빈도를 조절할 수 있게 한다.
6) 추가 보완 장치
- Rate Limit만으로는 정교한 봇을 완전히 막기 어렵기 때문에, 티켓 오픈처럼 순간 폭주가 확실한 구간에는 대기열(Queue) 시스템을 앞단에 추가로 두어 초과 트래픽을 아예 뒷단으로 흘려보내지 않고 대기시키는 방식을 함께 고려한다.
- 비정상적으로 짧은 간격의 반복 요청 패턴은 Gateway의 로깅/모니터링 데이터를 기반으로 탐지해 별도의 봇 차단(Deny-list) 정책과 연계한다.