---
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]]
