Files
2nd/10_Wiki/Topics/Coding/Backend_BFF_Pattern.md
T
2026-05-09 22:47:42 +09:00

9.2 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
backend-bff-pattern Backend for Frontend — Per-client API Coding draft B conceptual 2026-05-09 2026-05-09
backend
bff
vibe-coding
language applicable_to
TS
Backend
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)

// /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 예 (작은 페이로드)

// /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)

// 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 한 곳

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

// 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 통합

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

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)

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

// 단일 GraphQL endpoint per client
type Query {
  webDashboard(userId: ID!): WebDashboard
  iosDashboard(userId: ID!): IosDashboard
}

# Web  자기 query  보냄  정확 데이터.

→ Pothos / Yoga.

Aggregation patterns

// 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)

// Per user / per IP
const rate = await rateLimiter.check(userId);
if (!rate.allowed) return c.text('Rate limited', 429);

Failure isolation

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

// 매 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

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

// 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 항상.

🔗 관련 문서