Backend for Frontend 패턴: 당신의 UI가 정말 원하는 API 만들기

Backend for Frontend 패턴: 당신의 UI가 정말 원하는 API 만들기

프런트엔드 개발자라면 누구나 한 번쯤 기술적으로는 맞지만 자신이 만드는 것에는 전혀 맞지 않는 API 응답을 받아본 적이 있을 겁니다. 홈페이지에 사용자 이름, 아바타, 최근 주문 세 건이 필요하다고 해보죠. API를 네 번 따로 호출하고, 클라이언트에서 데이터를 이어 붙이고, 네트워크가 충분히 빨라서 로딩 순서를 아무도 못 느끼길 바랍니다. 필요한 건 6개 필드인데 엔드포인트는 47개 필드를 돌려주고, 정작 필요한 필드 하나는 usr_acct_disp_nm 같은 이름을 갖고 있습니다.

이건 아키텍처 문제입니다. 백엔드는 범용 사용이나 전혀 다른 클라이언트를 위해 설계되었습니다. Backend for Frontend(BFF) 패턴은 바로 이 문제를 해결하려고 존재합니다.

BFF란 무엇인가?

이 용어는 SoundCloud에서 태어나 Sam Newman이 마이크로서비스 연구에서 정리했습니다. 핵심 아이디어는 단순합니다. 클라이언트마다 전용 서버 측 계층을 만드는 것입니다. 모든 클라이언트 표면(웹, 모바일, TV, 서드파티 통합)을 하나로 서비스하려는 범용 API는 어느 것도 제대로 서비스하지 못합니다.

BFF는 다음을 수행하는 서버입니다.

클라이언트 표면당 BFF 하나라는 원칙은 양보할 수 없습니다. 모바일 앱과 웹 앱은 데이터 요구, 성능 예산, 상호작용 패턴이 다릅니다. 둘 사이에 BFF를 공유하면 아무도 위해 최적화하지 않는 범용 API를 다시 만드는 셈입니다.

BFF와 일반 백엔드

일반 백엔드는 비즈니스 로직과 데이터 무결성을 담당합니다. 도메인 규칙을 적용하고, 데이터베이스를 소유하고, 도메인 이 이해하는 용어로 리소스를 노출합니다. 홈페이지가 프로필 페이지보다 약간 다른 형태의 User를 필요로 한다는 사실에는 신경 쓰지 않습니다.

BFF는 프레젠테이션 로직을 담당합니다. 다운스트림 백엔드가 제공하는 것을 가져와 특정 프런트엔드가 필요한 형태로 변환합니다. 실제로는 다음과 같습니다.

이 구분은 중요합니다. 백엔드 엔지니어는 엔티티와 불변 조건을 기준으로 생각합니다. 프런트엔드 엔지니어의 기준은 컴포넌트와 화면이 필요로 하는 데이터입니다. BFF는 이 두 번째 언어를 유창하게 구사합니다.

아키텍처: BFF 없음 vs. BFF 있음

Architecture diagram: without BFF vs. with BFF

실제 차이가 어떤지 보겠습니다. BFF가 없으면 대시보드 페이지는 이런 식으로 동작할 수 있습니다.

// 프론트엔드가 세 개의 별도 호출로 화면을 조합
const [user, orders, notifications] = await Promise.all([
  fetch("/api/users/me"),
  fetch("/api/orders?userId=me&limit=3"),
  fetch("/api/notifications?userId=me&unread=true"),
]);
const unreadCount = notifications.filter((n) => !n.read).length;
const displayName = `${user.firstName} ${user.lastName}`;

BFF가 있으면 프런트엔드는 한 번의 호출로 정확히 필요한 것을 얻습니다.

// BFF가 이 화면에 맞게 설계된 엔드포인트 제공
const { user, recentOrders, unreadCount } = await fetch("/bff/dashboard");

집계와 변환 로직은 BFF 안에 있습니다. 프런트엔드 곳곳에 흩어지거나 여러 화면에 중복되지 않습니다.

마이크로서비스와 모놀리스 맥락에서의 BFF

마이크로서비스: 전형적인 사용 사례

BFF 패턴은 마이크로서비스 세계에서 태어났고, 그럴 만한 이유가 있습니다. 백엔드가 열두 개 서비스(사용자, 주문, 재고, 알림, 결제)로 나뉘어 있으면 프런트엔드는 그 모두를 알아야 합니다. 작은 UI 변경 하나에도 응답 형태를 조금 바꾸기 위해 백엔드 팀 세 곳과 조율해야 할 수 있습니다.

BFF는 프런트엔드의 단일 계약 역할을 하며 이 문제를 해결합니다. BFF는 필요한 다운스트림 서비스와 통신합니다. 사용자 서비스가 API를 바꾸면 BFF가 그 변경을 흡수합니다. 프런트엔드는 신경 쓸 필요가 없습니다.

이것은 프런트엔드/백엔드 경계에 적용한 안티 커럽션 계층 패턴입니다. 프런트엔드를 백엔드 진화의 잦은 변화로부터 격리합니다.

모놀리스: 다른 형태

BFF는 모놀리스에서도 똑같이 유용합니다. 동기는 달라질 뿐입니다. 여러 서비스를 조율하는 것이 아닙니다. 당신이 마주하는 것은 특정 UI 요구를 위해 설계되지 않은 범용 API입니다.

이 맥락에서 BFF는 보통 모놀리스 앞의 얇은 어댑터 계층입니다. 같은 코드베이스(모듈, Next.js 라우트 핸들러 모음, 모노레포 안의 전용 Express 앱)에 있거나 별도 서비스로 존재할 수 있습니다.

모놀리스 경우의 핵심 규칙은 BFF를 얇게 유지하는 것입니다. 모놀리스의 비즈니스 로직을 복제하기 시작하는 BFF는 안티 패턴입니다. 번역하고 집계해야 합니다. 비즈니스 규칙은 백엔드에 속합니다.

BFF는 누가 작성하나?

여기서 팀이 가장 흔히 실수를 합니다.

BFF는 프런트엔드 팀이 소유해야 합니다. 이것이 바로 패턴의 전부입니다.

백엔드 엔지니어가 BFF를 소유하면 그것은 서서히 범용 API로 흘러갑니다. 리뷰 주기가 느려집니다. 프런트엔드 팀은 결국 풀 리퀘스트와 JIRA 티켓을 통해 응답 형태를 협상하게 됩니다. 형태는 백엔드 팀이 승인해야만 나갑니다. 문제 하나를 같은 문제의 더 느린 버전으로 바꾼 셈입니다.

프런트엔드 엔지니어가 BFF를 소유하면 그들이 계약을 소유합니다. 필요할 때 데이터를 다시 다듬습니다. 요청을 제출하지 않고 새 엔드포인트를 추가합니다. 프런트엔드 속도로 출시합니다.

이를 위해 프런트엔드 엔지니어가 서버 측 코드를 조금 작성해야 합니다. 많지 않습니다. BFF 코드는 대부분 글루 작업입니다. 이 서비스를 호출하고, 그 응답을 변환하고, 깨끗한 형태를 돌려줍니다. HTTP, 인증 흐름, 비동기 I/O에 익숙해야 합니다. 복잡한 React 애플리케이션을 작성할 수 있는 엔지니어라면 대부분 어렵지 않게 감당합니다.

소유권 경계는 BFF 그 자체입니다. BFF는 다운스트림 서비스를 호출하지만 비즈니스 로직은 절대 소유하지 않습니다. 데이터베이스 쓰기, 도메인 규칙, 불변 조건은 백엔드에 남습니다. BFF는 프레젠테이션 계층에 확실히 머물러 있습니다. 그 구분을 적극적으로 유지해야 합니다.

언어 선택

중요합니다. 특히 프런트엔드 엔지니어가 주요 소유자일 때 그렇습니다.

Node.js 런타임과 함께 쓰는 TypeScript가 기본 권장입니다. 이유는 실용적입니다.

Go는 조직에 이미 Go 전문성이 있고 BFF를 풀스택이나 백엔드 엔지니어가 유지할 것으로 예상될 때 합리적인 대안입니다. Go는 동시 팬아웃을 잘 처리합니다. BFF가 단일 응답을 만들기 위해 다운스트림 요청을 여러 개 동시에 보낼 때 이는 중요합니다.

언어 선택은 소유권 모델을 따라야 합니다. 프런트엔드 엔지니어가 BFF를 소유한다면 이미 아는 언어를 써야 합니다. 다른 선택은 마찰을 만들고, 마찰은 결국 소유권을 백엔드 팀으로 밀어냅니다.

보안: BFF가 공짜로 주는 것

보안은 BFF를 지지하는 가장 강력한 논거 중 하나인데, 끊임없이 과소평가됩니다.

토큰 저장 문제

단일 페이지 애플리케이션은 오래된 딜레마를 안고 있습니다. 액세스 토큰을 어디에 저장할까요. localStorage는 편리하지만 페이지의 어떤 JavaScript에도 노출됩니다. npm 패키지의 공급망 공격 시대에는 이것이 실질적인 위험입니다. 인메모리는 더 안전하지만 새로고침하면 사라집니다. HTTP-only 쿠키는 작동하지만 서버 측 처리가 필요합니다.

BFF는 이 문제를 완전히 해결합니다. 브라우저는 원시 액세스 토큰을 절대 보유하지 않습니다.

BFF가 토큰 교환을 수행합니다. 신원 공급자로부터 인증 코드를 받아 액세스 토큰과 리프레시 토큰으로 교환하고, 토큰을 서버 측에 저장하고, 브라우저에 HTTP-only 세션 쿠키를 전달합니다. 이후 프런트엔드의 모든 요청은 그 쿠키를 담고 옵니다. BFF는 세션을 찾아 다운스트림 요청에 실제 토큰을 붙여 전달합니다. 토큰은 JavaScript 영역에 들어가지 않습니다.

// BFF가 OAuth 콜백을 처리 — 브라우저는 액세스 토큰을 볼 수 없음
app.get("/auth/callback", async (req, res) => {
  const { code } = req.query;
  const tokens = await identityProvider.exchangeCode(code);
 
  // 토큰은 서버 측에 저장
  await sessionStore.save(req.sessionId, tokens);
 
  res.setCookie("session", req.sessionId, {
    httpOnly: true,
    secure: true,
    sameSite: "strict",
  });
 
  res.redirect("/dashboard");
});

BFF에 넣지 말아야 할 것

BFF는 도메인 규칙이 쌓이는 장소가 되어서는 안 됩니다. 설계된 용도에 머물게 하세요. 읽기 집계, 응답 형태, 인증 위임이 그것입니다.

구체적으로:

BFF에 자체 데이터베이스가 있고 비즈니스 규칙을 소유하기 시작했다면 BFF를 만드는 것을 멈추고 두 번째 백엔드를 만들고 있는 것입니다. 그것은 전혀 다른 문제입니다.

배포

프런트엔드와 함께 위치

Next.js를 사용한다면 이미 가벼운 BFF를 위한 인프라가 있습니다. 그것이 Route Handlers(App Router) 또는 API Routes(Pages Router)입니다. 프런트엔드와 BFF는 같은 프로젝트에 살며 하나의 단위로 함께 배포됩니다.

이것은 소규모 및 중규모 애플리케이션의 올바른 기본값입니다. 대가로 프런트엔드와 독립적으로 BFF를 확장할 수 없습니다. 대부분 팀에게 그 제약은 무관합니다. 여기서 시작하세요.

모노레포의 별도 서비스

더 큰 애플리케이션에서 흔한 패턴은 모노레포에서 프런트엔드 옆에 사는 전용 BFF 서비스입니다. 둘 다 TypeScript 타입을 공유하지만 독립적으로 배포됩니다. 프런트엔드는 HTTP로 BFF를 호출합니다. BFF는 백엔드 서비스로 팬아웃합니다.

이 구성은 운영상 더 복잡하지만 독립적 확장과 더 깨끗한 분리를 제공합니다. BFF가 운영 부담을 정당화할 만큼 충분히 복잡해졌을 때 의미가 있습니다. 여러 집계 패턴, 캐시 계층, 커넥션 풀이 있다는 뜻입니다.

Kubernetes 사이드카

Kubernetes 환경에서 BFF는 프런트엔드 서버와 같은 파드의 사이드카 컨테이너로 배포될 수 있습니다. 프런트엔드는 localhost로 BFF와 통신하여 외부 네트워크 홉 없이 지연을 거의 0으로 유지합니다. 항상 함께 배포되므로 둘 사이의 버전 불일치가 사라집니다.

피해야 할 것

여러 프런트엔드 애플리케이션 간에 BFF를 공유하지 마세요. 그 순간 API 게이트웨이를 만드는 것입니다. 한 단계 위에서 해결하려던 일반화 문제를 다시 도입한 것입니다.

실제 사례

SoundCloud는 이 패턴이 공식적으로 이름 붙은 곳입니다. 그 엔지니어링 팀은 위에서 설명한 바로 그 조율 문제에 직면했습니다. 모바일과 웹 클라이언트는 서로 다른 데이터 형태가 필요했고, 공유 범용 API는 프런트엔드와 백엔드 팀 사이의 끊임없는 협상을 의미했습니다. 클라이언트별 BFF로 나누면서 출시 속도가 회복되었습니다.

Netflix는 지원하는 각 기기에 대해 BFF 계층을 운영합니다. 수가 많습니다. TV, 휴대폰, 태블릿, 게임 콘솔, 웹 브라우저는 성능 예산과 렌더링 제약이 크게 다릅니다. 하나의 API로 모두를 잘 서비스할 수 없습니다. Netflix의 BFF 계층이 각 표면에 대한 번역을 처리합니다.

Spotify도 비슷한 모델을 사용합니다. 웹 플레이어, 데스크톱 클라이언트, 모바일 앱은 각각 데이터 요구와 성능 목표가 다릅니다. 그들의 BFF 계층이 내부 서비스로의 팬아웃을 처리하고 각 클라이언트에 맞춘 응답을 돌려줍니다.

세 경우 모두 BFF는 엔지니어링 팀이 어떻게 조직되고 얼마나 빨리 출시해야 하는지를 반영하는 구조적 결정입니다.

BFF를 사용하지 말아야 할 때

BFF 패턴은 네트워크 홉, 운영할 서비스, 관리할 배포를 추가합니다. 그 부담은 제자리를 증명해야 합니다.

다음 경우 BFF를 건너뛰세요.

가장 비싼 BFF는 이유가 생기기 전에 만든 것입니다. 뼈대가 인지 부담을 더하고, 흡수할 실제 조율 문제가 없다면 유지할 추가 코드가 됩니다.

결론

BFF 패턴은 아키텍처 패턴으로 위장한 팀 구조 도구입니다.

UI를 만드는 팀이 그를 먹여 살리는 API 계약을 소유해야 한다고 말합니다. 언어 선택(TypeScript)은 소유권에서 따라옵니다. 보안 모델(서버 측에 보관된 토큰)은 BFF가 신뢰할 수 있는 백엔드 계층이라는 사실에서 따라옵니다. 배포는 프런트엔드와 BFF가 함께 진화한다는 사실에서 따라옵니다.

조율 문제가 있을 때 사용하세요. 프런트엔드 엔지니어가 백엔드 팀이 응답을 다시 다듬기를 기다리고 있을 때, 또는 요구가 크게 다른 여러 클라이언트 표면을 서비스할 때입니다. 아직 없는 문제를 해결하려고 사용하지 마세요.

필요할 때, 프런트엔드 팀이 자신의 코드와 그 코드가 소비하는 데이터 사이의 계약을 완전히 통제하게 해주는 것보다 더 좋은 것은 없습니다.

© Melvin Laplanche - All rights reserved.