[Daily morning study] BFF (Backend for Frontend) ํŒจํ„ด

#daily morning study

Image


BFF ํŒจํ„ด์ด๋ž€

BFF(Backend for Frontend)๋Š” ๊ฐ ํด๋ผ์ด์–ธํŠธ ์œ ํ˜•์— ๋งž๋Š” ์ „์šฉ ๋ฐฑ์—”๋“œ ๋ ˆ์ด์–ด๋ฅผ ๋ณ„๋„๋กœ ๋‘๋Š” ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด์ด๋‹ค. ์›น, ๋ชจ๋ฐ”์ผ ์•ฑ, IoT ๋””๋ฐ”์ด์Šค ๋“ฑ ํด๋ผ์ด์–ธํŠธ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ ์š”๊ตฌ์‚ฌํ•ญ์ด ๋‹ค๋ฅผ ๋•Œ ๋‹จ์ผ API๋กœ ๋ชจ๋‘๋ฅผ ๋งŒ์กฑ์‹œํ‚ค๊ธฐ ์–ด๋ ต๋‹ค๋Š” ๋ฌธ์ œ์—์„œ ์ถœ๋ฐœํ–ˆ๋‹ค.

Sam Newman์ด 2015๋…„ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๋งฅ๋ฝ์—์„œ ์ฒ˜์Œ ์ œ์•ˆํ•œ ํŒจํ„ด์ด๋‹ค.


๋“ฑ์žฅ ๋ฐฐ๊ฒฝ

๊ธฐ์กด ๋‹จ์ผ ๋ฐฑ์—”๋“œ API๋Š” ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๊ณต์œ ํ•œ๋‹ค.

๋ชจ๋ฐ”์ผ ์•ฑ โ”€โ”€โ”
์›น ์•ฑ     โ”€โ”€โ”ผโ”€โ”€โ†’ ๋‹จ์ผ API โ”€โ”€โ†’ ์—ฌ๋Ÿฌ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๋“ค
๋ฐ์Šคํฌํƒ‘  โ”€โ”€โ”˜

์ด ๊ตฌ์กฐ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋“ค:

  • Over-fetching: ๋ชจ๋ฐ”์ผ์€ ์ด๋ฆ„๊ณผ ์•„๋ฐ”ํƒ€๋งŒ ํ•„์š”ํ•œ๋ฐ ์ „์ฒด ์‚ฌ์šฉ์ž ๊ฐ์ฒด๋ฅผ ๋ฐ›๋Š”๋‹ค
  • Under-fetching: ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ๊ทธ๋ฆฌ๋ ค๋ฉด ์—ฌ๋Ÿฌ ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ˆœ์ฐจ ํ˜ธ์ถœํ•ด์•ผ ํ•œ๋‹ค
  • ํด๋ผ์ด์–ธํŠธ ๊ฐ„ ์ถฉ๋Œ: ๋ชจ๋ฐ”์ผ์„ ์œ„ํ•œ ๋ณ€๊ฒฝ์ด ์›น ํด๋ผ์ด์–ธํŠธ๋ฅผ ๋ง๊ฐ€๋œจ๋ฆด ์ˆ˜ ์žˆ๋‹ค
  • ํŒ€ ์˜์กด์„ฑ: ํ”„๋ก ํŠธ์—”๋“œ ํŒ€์ด ๋ฐฑ์—”๋“œ ํŒ€์˜ ์ผ์ •์— ์ข…์†๋œ๋‹ค

BFF ๊ตฌ์กฐ

๊ฐ ํด๋ผ์ด์–ธํŠธ ์ฑ„๋„๋งˆ๋‹ค ๋ณ„๋„์˜ BFF๋ฅผ ๋‘”๋‹ค.

๋ชจ๋ฐ”์ผ ์•ฑ  โ”€โ”€โ†’ Mobile BFF โ”€โ”€โ”
์›น ์•ฑ      โ”€โ”€โ†’ Web BFF    โ”€โ”€โ”ผโ”€โ”€โ†’ ๋‚ด๋ถ€ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๋“ค
ํŒŒํŠธ๋„ˆ API โ”€โ”€โ†’ Partner BFF โ”€โ”˜

BFF์˜ ์ฃผ์š” ์—ญํ• :

  1. ์—ฌ๋Ÿฌ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ํ˜ธ์ถœ์„ aggregationํ•œ๋‹ค
  2. ํด๋ผ์ด์–ธํŠธ ํ˜•ํƒœ์— ๋งž๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€ํ™˜ํ•œ๋‹ค
  3. ๋ถˆํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ํ•„ํ„ฐ๋งํ•œ๋‹ค

์ฝ”๋“œ ์˜ˆ์‹œ

๋ชจ๋ฐ”์ผ BFF โ€” ๋„คํŠธ์›Œํฌ ์ตœ์†Œํ™”, ์ตœ์†Œ ๋ฐ์ดํ„ฐ

// ๋ชจ๋ฐ”์ผ: ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ + ์ตœ๊ทผ ์ฃผ๋ฌธ 3๊ฐœ๋งŒ ํ•„์š”
app.get('/mobile/user-summary', async (req, res) => {
  const [user, orders] = await Promise.all([
    userService.getUser(req.userId),
    orderService.getRecentOrders(req.userId, { limit: 3 })
  ]);

  res.json({
    name: user.name,
    avatarUrl: user.avatarUrl,
    recentOrders: orders.map(o => ({
      id: o.id,
      status: o.status,
      total: o.totalAmount
    }))
  });
});

์›น BFF โ€” ๋” ํ’๋ถ€ํ•œ ๋ฐ์ดํ„ฐ

// ์›น: ๋Œ€์‹œ๋ณด๋“œ์— ์ „์ฒด ์ฃผ๋ฌธ ์ด๋ ฅ + ์ถ”์ฒœ ์ƒํ’ˆ + ์•Œ๋ฆผ ํ•„์š”
app.get('/web/dashboard', async (req, res) => {
  const [user, orders, recommendations, notifications] = await Promise.all([
    userService.getUser(req.userId),
    orderService.getAllOrders(req.userId),
    recommendationService.get(req.userId),
    notificationService.getUnread(req.userId)
  ]);

  res.json({
    user: {
      name: user.name,
      email: user.email,
      preferences: user.preferences,
      memberSince: user.createdAt
    },
    orders: orders.map(mapOrderDetail),
    recommendations,
    notifications
  });
});

ํ•œ ์—”๋“œํฌ์ธํŠธ ํ˜ธ์ถœ๋กœ ๋Œ€์‹œ๋ณด๋“œ ๋ Œ๋”๋ง์— ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ๋‘ ๋ฐ›๋Š”๋‹ค. ํด๋ผ์ด์–ธํŠธ๋Š” N๋ฒˆ ํ˜ธ์ถœํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค.


API Gateway์™€์˜ ์ฐจ์ด

BFF์™€ API Gateway๋Š” ๋น„์Šทํ•ด ๋ณด์ด์ง€๋งŒ ์—ญํ• ์ด ๋‹ค๋ฅด๋‹ค.

ํ•ญ๋ชฉAPI GatewayBFF
๋ชฉ์ ๊ณตํ†ต ํšก๋‹จ ๊ด€์‹ฌ์‚ฌ ์ฒ˜๋ฆฌํด๋ผ์ด์–ธํŠธ๋ณ„ ๋ฐ์ดํ„ฐ ์ตœ์ ํ™”
๋กœ์ง๊ฑฐ์˜ ์—†์Œ(๋ผ์šฐํŒ…, ์ธ์ฆ)๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ํฌํ•จ ๊ฐ€๋Šฅ
๋ฒ”์œ„๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ ๊ณต์œ ํด๋ผ์ด์–ธํŠธ ์œ ํ˜•๋ณ„ ๋ถ„๋ฆฌ
์ฃผ์ฒดํ”Œ๋žซํผ ํŒ€ํ”„๋ก ํŠธ์—”๋“œ ํŒ€

์‹ค์ œ๋กœ ๋‘˜์„ ํ•จ๊ป˜ ์“ฐ๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ๋‹ค.

ํด๋ผ์ด์–ธํŠธ โ”€โ”€โ†’ API Gateway (์ธ์ฆ/์†๋„์ œํ•œ/TLS) โ”€โ”€โ†’ BFF (aggregation/๋ณ€ํ™˜)

์žฅ๋‹จ์ 

์žฅ์ 

  • ํด๋ผ์ด์–ธํŠธ ์ตœ์ ํ™”: ๊ฐ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋”ฑ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ๋ฐ›๋Š”๋‹ค
  • ํŒ€ ์ž์œจ์„ฑ: ํ”„๋ก ํŠธ์—”๋“œ ํŒ€์ด BFF๋ฅผ ์ง์ ‘ ์†Œ์œ ํ•˜๊ณ  ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค
  • ๋…๋ฆฝ ๋ฐฐํฌ: BFF ๋ณ€๊ฒฝ์ด ๋‹ค๋ฅธ ํด๋ผ์ด์–ธํŠธ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋Š”๋‹ค
  • ์žฅ์•  ๊ฒฉ๋ฆฌ: ์›น BFF ์žฅ์• ๊ฐ€ ๋ชจ๋ฐ”์ผ BFF์— ์ „ํŒŒ๋˜์ง€ ์•Š๋Š”๋‹ค

๋‹จ์ 

  • ์ฝ”๋“œ ์ค‘๋ณต: ์—ฌ๋Ÿฌ BFF์—์„œ ๋น„์Šทํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ์ค‘๋ณต๋  ์ˆ˜ ์žˆ๋‹ค
  • ์šด์˜ ๋ณต์žก๋„: ๊ด€๋ฆฌํ•ด์•ผ ํ•˜๋Š” ์„œ๋น„์Šค ์ˆ˜๊ฐ€ ๋Š˜์–ด๋‚œ๋‹ค
  • ํŒ€ ๊ฒฝ๊ณ„ ๋ถˆ๋ช…ํ™•: โ€œ์–ด๋А ํŒ€์ด BFF๋ฅผ ์†Œ์œ ํ•˜๋Š”๊ฐ€โ€๋ฅผ ๋ช…ํ™•ํžˆ ํ•ด์•ผ ํ•œ๋‹ค

GraphQL๊ณผ์˜ ๊ด€๊ณ„

GraphQL์€ BFF์˜ ๋Œ€์•ˆ์œผ๋กœ ์ž์ฃผ ์–ธ๊ธ‰๋œ๋‹ค. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ํ•„์š”ํ•œ ํ•„๋“œ๋ฅผ ์ฟผ๋ฆฌ์— ์ง์ ‘ ๋ช…์‹œํ•˜๊ธฐ ๋•Œ๋ฌธ์— over-fetching, under-fetching ๋ฌธ์ œ๋ฅผ ํ•ด์†Œํ•  ์ˆ˜ ์žˆ๋‹ค.

# ๋ชจ๋ฐ”์ผ: ํ•„์š”ํ•œ ํ•„๋“œ๋งŒ ์š”์ฒญ
query MobileProfile {
  user(id: "123") {
    name
    avatarUrl
    recentOrders(limit: 3) {
      id
      status
      total
    }
  }
}

๋‹จ, GraphQL์ด ํ•ญ์ƒ BFF๋ฅผ ๋Œ€์ฒดํ•˜์ง„ ์•Š๋Š”๋‹ค:

  • ๊ธฐ์กด REST ์ธํ”„๋ผ๋ฅผ ์œ ์ง€ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ
  • ํด๋ผ์ด์–ธํŠธ๋ณ„ ๋ณต์žกํ•œ aggregation ๋กœ์ง์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ
  • GraphQL์˜ ํ•™์Šต ๊ณก์„ ์ด ๋ถ€๋‹ด์ธ ๊ฒฝ์šฐ

GraphQL BFF ์กฐํ•ฉ๋„ ์กด์žฌํ•œ๋‹ค โ€” GraphQL ์Šคํ‚ค๋งˆ๋ฅผ ํด๋ผ์ด์–ธํŠธ๋ณ„๋กœ ๋‚˜๋ˆ„๋Š” ๋ฐฉ์‹์ด๋‹ค.


์–ธ์ œ BFF๋ฅผ ์„ ํƒํ•˜๋‚˜

BFF๊ฐ€ ์ ํ•ฉํ•œ ์ƒํ™ฉ:

  • ํด๋ผ์ด์–ธํŠธ ์œ ํ˜•์ด ๋‹ค์–‘ํ•˜๊ณ  ๊ฐ๊ฐ ๋‹ค๋ฅธ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ์„ ์š”๊ตฌํ•  ๋•Œ
  • ํ”„๋ก ํŠธ์—”๋“œ ํŒ€์ด ๋ฐฑ์—”๋“œ ์˜์กด์„ฑ ์—†์ด ๋น ๋ฅด๊ฒŒ ๋ฐ˜๋ณตํ•˜๊ณ  ์‹ถ์„ ๋•Œ
  • ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ํ™˜๊ฒฝ์—์„œ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์—ฌ๋Ÿฌ ์„œ๋น„์Šค๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋Š” ๋ณต์žก๋„๋ฅผ ์ค„์ด๊ณ  ์‹ถ์„ ๋•Œ

BFF๊ฐ€ ๊ณผํ•  ์ˆ˜ ์žˆ๋Š” ์ƒํ™ฉ:

  • ํด๋ผ์ด์–ธํŠธ๊ฐ€ ํ•œ ์ข…๋ฅ˜๋ฟ์ผ ๋•Œ
  • ์„œ๋น„์Šค ์ดˆ๊ธฐ ๋‹จ๊ณ„๋กœ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ๋ถ€๋‹ด๋  ๋•Œ
  • ํŒ€์ด ์ž‘์•„์„œ BFF๋ฅผ ๋ณ„๋„๋กœ ์šด์˜ํ•˜๊ธฐ ์–ด๋ ค์šธ ๋•Œ