[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
---
|
||||
id: backend-api-gateway-bff
|
||||
title: API Gateway / BFF — 라우팅 / aggregation
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [backend, api-gateway, bff, vibe-coding]
|
||||
tech_stack: { language: "TS / Kong / Envoy", applicable_to: ["Backend"] }
|
||||
applied_in: []
|
||||
aliases: [API gateway, BFF, backend for frontend, Kong, Envoy, Apollo Federation]
|
||||
---
|
||||
|
||||
# API Gateway / BFF
|
||||
|
||||
> **Gateway = 횡단 관심사 (auth / rate / logging) 묶기**. **BFF = 프론트별 (web / iOS / android) 응답 최적화**. 마이크로서비스 + 다양한 클라.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- API Gateway: 단일 진입점 — Kong / Envoy / Cloudflare / API Gateway (AWS).
|
||||
- BFF: Backend for Frontend — 클라별 다른 backend.
|
||||
- Aggregation: 여러 service 호출 합쳐서 응답.
|
||||
- Transformation: legacy ↔ modern format.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### Gateway 책임
|
||||
```
|
||||
Client ──▶ Gateway ──▶ Service A
|
||||
└──▶ Service B
|
||||
└──▶ Service C
|
||||
|
||||
Gateway:
|
||||
- TLS termination
|
||||
- Auth (JWT 검증)
|
||||
- Rate limiting (per IP / per user)
|
||||
- Logging / tracing
|
||||
- CORS
|
||||
- Routing / load balance
|
||||
- Caching (GET)
|
||||
```
|
||||
|
||||
### Kong (DB-less)
|
||||
```yaml
|
||||
# kong.yml
|
||||
_format_version: "3.0"
|
||||
|
||||
services:
|
||||
- name: users
|
||||
url: http://users-svc:3000
|
||||
routes:
|
||||
- name: users-route
|
||||
paths: [/api/users]
|
||||
plugins:
|
||||
- name: jwt
|
||||
- name: rate-limiting
|
||||
config: { minute: 100, policy: local }
|
||||
```
|
||||
|
||||
### Cloudflare Workers (가벼운 gateway)
|
||||
```ts
|
||||
export default {
|
||||
async fetch(req: Request, env: Env): Promise<Response> {
|
||||
const url = new URL(req.url);
|
||||
|
||||
// Auth
|
||||
const token = req.headers.get('authorization')?.replace('Bearer ', '');
|
||||
const userId = await verifyJwt(token, env.JWT_SECRET);
|
||||
if (!userId && !isPublic(url.pathname)) return new Response('unauthorized', { status: 401 });
|
||||
|
||||
// Rate limit
|
||||
const ok = await env.RATE.limit({ key: userId ?? req.headers.get('cf-connecting-ip')! });
|
||||
if (!ok.success) return new Response('rate limited', { status: 429 });
|
||||
|
||||
// Routing
|
||||
if (url.pathname.startsWith('/api/users')) {
|
||||
return fetch(`http://users-svc${url.pathname}`, { headers: { 'x-user-id': userId } });
|
||||
}
|
||||
if (url.pathname.startsWith('/api/orders')) {
|
||||
return fetch(`http://orders-svc${url.pathname}`, { headers: { 'x-user-id': userId } });
|
||||
}
|
||||
return new Response('not found', { status: 404 });
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### BFF (Web-specific)
|
||||
```ts
|
||||
// bff-web/routes/dashboard.ts
|
||||
app.get('/api/dashboard', async (req, res) => {
|
||||
// 동시에 여러 service 호출
|
||||
const [user, orders, recommendations] = await Promise.all([
|
||||
fetch(`${USERS}/users/${req.userId}`).then(r => r.json()),
|
||||
fetch(`${ORDERS}/orders?userId=${req.userId}&limit=10`).then(r => r.json()),
|
||||
fetch(`${RECS}/for/${req.userId}`).then(r => r.json()),
|
||||
]);
|
||||
|
||||
// 프론트가 필요한 형태로 합치기
|
||||
res.json({
|
||||
user: { id: user.id, name: user.name, avatar: user.avatar },
|
||||
recentOrders: orders.map(o => ({ id: o.id, status: o.status, total: o.total })),
|
||||
recommendations: recommendations.slice(0, 5).map(r => ({ id: r.id, name: r.name })),
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
→ Web 만 사용. iOS BFF 가 다른 페이로드 반환.
|
||||
|
||||
### Pattern: Backend for X
|
||||
```
|
||||
bff-web/ → 큰 페이로드, 많은 데이터
|
||||
bff-mobile/ → 작은 페이로드, 압축
|
||||
bff-admin/ → 추가 관리 endpoint
|
||||
```
|
||||
|
||||
### Aggregation + Cache
|
||||
```ts
|
||||
const cache = new Map<string, { data: unknown; expires: number }>();
|
||||
|
||||
async function withCache<T>(key: string, ttl: number, fn: () => Promise<T>): Promise<T> {
|
||||
const c = cache.get(key);
|
||||
if (c && c.expires > Date.now()) return c.data as T;
|
||||
const data = await fn();
|
||||
cache.set(key, { data, expires: Date.now() + ttl });
|
||||
return data;
|
||||
}
|
||||
|
||||
const recs = await withCache(`recs:${userId}`, 60_000, () => fetch(...));
|
||||
```
|
||||
|
||||
### Header propagation (tracing)
|
||||
```ts
|
||||
const traceHeaders = ['traceparent', 'tracestate', 'x-request-id', 'x-user-id'];
|
||||
|
||||
async function forward(req: Request, target: string) {
|
||||
const h = new Headers();
|
||||
for (const k of traceHeaders) {
|
||||
const v = req.headers.get(k);
|
||||
if (v) h.set(k, v);
|
||||
}
|
||||
return fetch(target, { headers: h });
|
||||
}
|
||||
```
|
||||
|
||||
### Apollo Federation (GraphQL gateway)
|
||||
```ts
|
||||
// 여러 GraphQL service 를 하나의 schema 로
|
||||
import { ApolloGateway, IntrospectAndCompose } from '@apollo/gateway';
|
||||
import { ApolloServer } from '@apollo/server';
|
||||
|
||||
const gateway = new ApolloGateway({
|
||||
supergraphSdl: new IntrospectAndCompose({
|
||||
subgraphs: [
|
||||
{ name: 'users', url: 'http://users:4001' },
|
||||
{ name: 'orders', url: 'http://orders:4002' },
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const server = new ApolloServer({ gateway });
|
||||
```
|
||||
|
||||
### tRPC (TypeSafe RPC)
|
||||
```ts
|
||||
// router (server)
|
||||
const appRouter = router({
|
||||
user: userRouter,
|
||||
order: orderRouter,
|
||||
});
|
||||
export type AppRouter = typeof appRouter;
|
||||
|
||||
// client
|
||||
import type { AppRouter } from 'server/router';
|
||||
const trpc = createTRPCClient<AppRouter>({ url: '/api/trpc' });
|
||||
const u = await trpc.user.get.query({ id: '1' }); // typed end-to-end
|
||||
```
|
||||
|
||||
→ BFF 와 자연 결합.
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 상황 | 추천 |
|
||||
|---|---|
|
||||
| 단일 service | Gateway 불필요 |
|
||||
| 마이크로서비스 + 단일 클라 | Gateway 만 |
|
||||
| 다양한 클라 (web/iOS/AOS) | Gateway + BFF per-client |
|
||||
| Edge / 글로벌 | Cloudflare Workers / Vercel Edge |
|
||||
| GraphQL 통합 | Apollo Federation |
|
||||
| TypeScript 풀스택 | tRPC (BFF 비슷) |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Gateway 가 비즈니스 로직**: gateway = thin. 로직은 service.
|
||||
- **BFF 가 모든 클라 공유**: 의미 없음. per-client 또는 그냥 service.
|
||||
- **Aggregation 안 cache**: 매번 다중 호출. 5분 cache 만이라도.
|
||||
- **Header propagation 누락**: tracing 깨짐.
|
||||
- **Auth 가 service 마다**: gateway 한 번 + service 는 신뢰.
|
||||
- **Service 간 Sync 호출 chain**: latency 합산. 병렬 / async.
|
||||
- **Gateway = single point of failure 무 HA**: 다중 노드 + LB.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- Gateway = 횡단 (auth/rate/log) + routing + LB.
|
||||
- BFF = 클라별 응답 형태.
|
||||
- Tracing 헤더 항상 propagate.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[Backend_Rate_Limiting]]
|
||||
- [[Backend_Health_Check_Patterns]]
|
||||
- [[DevOps_Observability_Stack]]
|
||||
Reference in New Issue
Block a user