[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,390 @@
|
||||
---
|
||||
id: backend-bff-pattern
|
||||
title: Backend for Frontend — Per-client API
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [backend, bff, vibe-coding]
|
||||
tech_stack: { language: "TS", applicable_to: ["Backend"] }
|
||||
applied_in: []
|
||||
aliases: [BFF, backend for frontend, edge BFF, aggregation API, gateway pattern]
|
||||
---
|
||||
|
||||
# BFF (Backend for Frontend)
|
||||
|
||||
> Frontend 별 backend layer. **Web BFF, iOS BFF, Android BFF**. Aggregation + transformation + 인증. Microservice + 다양 client 의 sweet spot.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- BFF: 한 frontend = 한 BFF.
|
||||
- Aggregation: 여러 service 호출 → 한 응답.
|
||||
- Tailoring: 그 client 가 필요한 데이터만.
|
||||
- Edge BFF: 사용자 가까이.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### Architecture
|
||||
```
|
||||
[Web] → [Web BFF] → Service A
|
||||
[iOS] → [iOS BFF] → Service B
|
||||
[Android] → [Android BFF] → Service C
|
||||
[Admin] → [Admin BFF]
|
||||
```
|
||||
|
||||
→ 각 BFF 가 그 client 의 needs 에 맞춰.
|
||||
|
||||
### Why per-client?
|
||||
```
|
||||
Web: 큰 페이로드 OK, 빠른 fetch, web-specific UI 데이터.
|
||||
iOS: 작은 (data plan), iOS-specific format (e.g. SF symbols).
|
||||
Android: 작은, 전력 절약.
|
||||
Admin: 풍부한 데이터, 권한 다름.
|
||||
|
||||
→ 한 API 가 모두 만족 X.
|
||||
```
|
||||
|
||||
### Web BFF 예 (Hono / Next API)
|
||||
```ts
|
||||
// /api/dashboard
|
||||
app.get('/api/dashboard', authRequired, async (c) => {
|
||||
const userId = c.get('userId');
|
||||
|
||||
// 여러 service 동시 호출
|
||||
const [user, recentOrders, recommendations, notifications] = await Promise.all([
|
||||
fetch(`${USERS_SVC}/users/${userId}`).then(r => r.json()),
|
||||
fetch(`${ORDERS_SVC}/orders?userId=${userId}&limit=5`).then(r => r.json()),
|
||||
fetch(`${RECS_SVC}/for/${userId}`).then(r => r.json()),
|
||||
fetch(`${NOTIF_SVC}/${userId}/unread`).then(r => r.json()),
|
||||
]);
|
||||
|
||||
// Web 의 needs 에 맞춰 합치기
|
||||
return c.json({
|
||||
user: { id: user.id, name: user.name, avatar: user.avatar },
|
||||
recentOrders: recentOrders.map((o: any) => ({
|
||||
id: o.id,
|
||||
status: o.status,
|
||||
total: o.total,
|
||||
itemCount: o.items.length,
|
||||
})),
|
||||
recommendations: recommendations.slice(0, 6),
|
||||
unreadCount: notifications.length,
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
→ Web 이 한 번의 fetch.
|
||||
|
||||
### iOS BFF 예 (작은 페이로드)
|
||||
```ts
|
||||
// /api/dashboard (iOS)
|
||||
app.get('/api/dashboard', authRequired, async (c) => {
|
||||
const userId = c.get('userId');
|
||||
|
||||
const [user, recentOrders] = await Promise.all([
|
||||
fetch(`${USERS_SVC}/users/${userId}`).then(r => r.json()),
|
||||
fetch(`${ORDERS_SVC}/orders?userId=${userId}&limit=3`).then(r => r.json()), // 작게
|
||||
]);
|
||||
|
||||
return c.json({
|
||||
user: { name: user.name, avatar: user.avatar }, // id 안 필요
|
||||
orders: recentOrders.map((o: any) => ({
|
||||
id: o.id,
|
||||
status: o.status,
|
||||
// total, itemCount 만 — 적은 byte
|
||||
})),
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Edge BFF (Cloudflare / Vercel)
|
||||
```ts
|
||||
// CF Worker / Vercel Edge
|
||||
export default {
|
||||
async fetch(req: Request, env: Env) {
|
||||
const userId = await getUserId(req, env);
|
||||
if (!userId) return new Response('Unauthorized', { status: 401 });
|
||||
|
||||
// Cache 적극
|
||||
const cacheKey = `dashboard:${userId}`;
|
||||
const cached = await env.CACHE.get(cacheKey, { type: 'json' });
|
||||
if (cached) return Response.json(cached);
|
||||
|
||||
const data = await aggregateData(userId, env);
|
||||
await env.CACHE.put(cacheKey, JSON.stringify(data), { expirationTtl: 30 });
|
||||
|
||||
return Response.json(data);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
→ 사용자 가까이 = 빠름.
|
||||
|
||||
### Authentication 한 곳
|
||||
```ts
|
||||
// BFF 가 JWT verify, 백엔드 service 호출 시 trusted
|
||||
async function callService(url: string, userId: string) {
|
||||
return fetch(url, {
|
||||
headers: {
|
||||
'X-User-ID': userId, // BFF 가 verify 한 user
|
||||
'X-Internal-Auth': INTERNAL_TOKEN, // service-to-service
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
→ BFF 가 user 인증 + service 호출.
|
||||
|
||||
### Caching strategy
|
||||
```ts
|
||||
// Per-user cache
|
||||
const userCache = `user:${userId}:dashboard`;
|
||||
|
||||
// Common cache
|
||||
const productsCache = `products:trending`;
|
||||
|
||||
// 다른 TTL
|
||||
- Personal data: 30s
|
||||
- Common (products): 5 min
|
||||
- Static (categories): 1 hour
|
||||
```
|
||||
|
||||
### Error 통합
|
||||
```ts
|
||||
async function safeCall<T>(fn: () => Promise<T>, fallback: T): Promise<T> {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e) {
|
||||
log.error({ err: e });
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
const data = {
|
||||
user: await safeCall(() => fetchUser(), null),
|
||||
orders: await safeCall(() => fetchOrders(), []),
|
||||
recommendations: await safeCall(() => fetchRecs(), []),
|
||||
};
|
||||
|
||||
// 일부 service 실패 = partial response
|
||||
```
|
||||
|
||||
→ Resilient — 한 service 다운 = 다른 데이터 표시.
|
||||
|
||||
### Header forwarding
|
||||
```ts
|
||||
const FORWARD_HEADERS = ['x-request-id', 'traceparent', 'tracestate', 'x-locale'];
|
||||
|
||||
async function callService(url: string, req: Request) {
|
||||
const headers = new Headers();
|
||||
for (const h of FORWARD_HEADERS) {
|
||||
const v = req.headers.get(h);
|
||||
if (v) headers.set(h, v);
|
||||
}
|
||||
return fetch(url, { headers });
|
||||
}
|
||||
```
|
||||
|
||||
→ Tracing 보존.
|
||||
|
||||
### Type-safe (tRPC / Hono RPC)
|
||||
```ts
|
||||
// BFF 가 tRPC server
|
||||
const bffRouter = router({
|
||||
dashboard: publicProcedure.query(async ({ ctx }) => {
|
||||
return aggregateDashboard(ctx.userId);
|
||||
}),
|
||||
});
|
||||
|
||||
// Client (Web)
|
||||
const client = createTRPCReact<BFFRouter>();
|
||||
const dashboard = client.dashboard.useQuery();
|
||||
```
|
||||
|
||||
→ Type-safe end-to-end.
|
||||
|
||||
### GraphQL BFF
|
||||
```ts
|
||||
// 단일 GraphQL endpoint per client
|
||||
type Query {
|
||||
webDashboard(userId: ID!): WebDashboard
|
||||
iosDashboard(userId: ID!): IosDashboard
|
||||
}
|
||||
|
||||
# Web 이 자기 query 만 보냄 → 정확 데이터.
|
||||
```
|
||||
|
||||
→ Pothos / Yoga.
|
||||
|
||||
### Aggregation patterns
|
||||
```ts
|
||||
// 1. Parallel
|
||||
const [a, b, c] = await Promise.all([...]);
|
||||
|
||||
// 2. Sequential (의존)
|
||||
const user = await fetchUser();
|
||||
const orders = await fetchOrders(user.id);
|
||||
|
||||
// 3. Conditional
|
||||
const user = await fetchUser();
|
||||
if (user.tier === 'pro') {
|
||||
data.proFeatures = await fetchProFeatures();
|
||||
}
|
||||
|
||||
// 4. Stream / pipe
|
||||
async function* streamData() {
|
||||
yield await fetchA();
|
||||
yield await fetchB();
|
||||
}
|
||||
```
|
||||
|
||||
### Rate limit (BFF level)
|
||||
```ts
|
||||
// Per user / per IP
|
||||
const rate = await rateLimiter.check(userId);
|
||||
if (!rate.allowed) return c.text('Rate limited', 429);
|
||||
```
|
||||
|
||||
### Failure isolation
|
||||
```ts
|
||||
// Circuit breaker per service
|
||||
const userBreaker = new CircuitBreaker(fetchUser, { timeout: 5000 });
|
||||
|
||||
if (userBreaker.isOpen()) {
|
||||
return Response.json({ user: cached, degraded: true });
|
||||
}
|
||||
```
|
||||
|
||||
→ Service 다운 = degraded mode.
|
||||
|
||||
### Observability
|
||||
```ts
|
||||
// 매 service call 추적
|
||||
import { trace } from '@opentelemetry/api';
|
||||
|
||||
const tracer = trace.getTracer('bff');
|
||||
|
||||
await tracer.startActiveSpan('fetch-user', async (span) => {
|
||||
span.setAttributes({ userId });
|
||||
try {
|
||||
return await fetchUser();
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Web push notification
|
||||
```ts
|
||||
// BFF 가 SSE / WebSocket 처리
|
||||
app.get('/api/events', async (c) => {
|
||||
return new Response(
|
||||
new ReadableStream({
|
||||
async start(controller) {
|
||||
const sub = pubsub.subscribe(c.get('userId'));
|
||||
for await (const event of sub) {
|
||||
controller.enqueue(`data: ${JSON.stringify(event)}\n\n`);
|
||||
}
|
||||
},
|
||||
}),
|
||||
{ headers: { 'Content-Type': 'text/event-stream' } }
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
### vs API Gateway
|
||||
```
|
||||
API Gateway:
|
||||
- Generic — 어떤 client 도 가능
|
||||
- 큰 organization (한 Gateway, 많은 client)
|
||||
- Auth / rate limit / routing
|
||||
|
||||
BFF:
|
||||
- Per-client — Web BFF, iOS BFF
|
||||
- 작은 organization (each team owns BFF)
|
||||
- 비즈니스 logic (aggregation)
|
||||
|
||||
→ Gateway = horizontal. BFF = vertical (client-specific).
|
||||
둘 다 같이 사용 가능.
|
||||
```
|
||||
|
||||
### Fan-out + cache
|
||||
```
|
||||
1 BFF call = 5 service calls.
|
||||
|
||||
Cache:
|
||||
- BFF response cache (per-user 30s)
|
||||
- Service response cache (Redis)
|
||||
- DB query cache (Redis)
|
||||
|
||||
→ 첫 call slow, 후속 fast.
|
||||
```
|
||||
|
||||
### Mobile-specific BFF
|
||||
```ts
|
||||
// iOS BFF
|
||||
- 작은 페이로드 (data plan)
|
||||
- iOS HIG-friendly format (SF symbol name 같은)
|
||||
- App version 별 다른 응답
|
||||
- Push token 등록 endpoint
|
||||
|
||||
// Android BFF
|
||||
- 작은 + 전력 절약
|
||||
- Material symbol name
|
||||
- App version 별
|
||||
```
|
||||
|
||||
### Versioning (per BFF)
|
||||
```
|
||||
/api/v1/dashboard
|
||||
/api/v2/dashboard
|
||||
|
||||
→ App version 별 BFF version pin.
|
||||
```
|
||||
|
||||
### Team ownership
|
||||
```
|
||||
Web 팀: Web BFF + Web frontend
|
||||
iOS 팀: iOS BFF + iOS app
|
||||
|
||||
→ Frontend 팀 가 BFF 소유. 빠른 iteration.
|
||||
```
|
||||
|
||||
### CDN integration
|
||||
```
|
||||
Static + edge BFF:
|
||||
- 정적 = CDN
|
||||
- 동적 = edge BFF
|
||||
- 사용자 = 가까운 region 자동
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 상황 | 추천 |
|
||||
|---|---|
|
||||
| 다양 client | BFF per client |
|
||||
| Single client | Direct API 충분 |
|
||||
| 마이크로서비스 + Web | Web BFF 가 aggregation |
|
||||
| Public API | Direct (다양 dev) |
|
||||
| Mobile + 작은 페이로드 | Mobile BFF 강력 |
|
||||
| Edge user 가까이 | Edge BFF |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **BFF 가 비즈니스 logic 모두**: service layer 의 책임.
|
||||
- **BFF 가 내부 API expose 그대로**: tailoring 의미 X.
|
||||
- **모든 client 한 BFF**: per-client 의 가치 잃음.
|
||||
- **Cache 무**: 매 fetch 가 N service.
|
||||
- **Auth 매 service 마다**: BFF 만.
|
||||
- **Header forward 무**: tracing 깨짐.
|
||||
- **Failure isolation 무**: 한 service down = BFF down.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- BFF = aggregation + tailoring + 인증.
|
||||
- Edge BFF (CF / Vercel) 가 가까운 user.
|
||||
- Type-safe = tRPC / Hono RPC.
|
||||
- Failure isolation + cache 항상.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[Backend_API_Gateway_BFF]]
|
||||
- [[Backend_Edge_Functions]]
|
||||
- [[Backend_Hono_Modern]]
|
||||
Reference in New Issue
Block a user