백엔드 포 프론트엔드 패턴: UI가 실제로 원하는 API 만들기

백엔드 포 프론트엔드 패턴: 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 vs. 일반 백엔드

일반 백엔드는 비즈니스 로직과 데이터 무결성을 담당합니다. 도메인 규칙을 시행하고, 데이터베이스를 소유하며, 도메인이 이해하는 용어로 리소스를 노출합니다. 홈페이지가 프로필 페이지와 약간 다른 형태의 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에서 처리합니다. 프론트엔드 코드 여기저기에 분산되거나 여러 화면에 중복되지 않습니다.

마이크로서비스 vs. 모놀리스 환경에서의 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와 통신하여 외부 네트워크 홉 없이 거의 제로에 가까운 지연을 유지합니다. 항상 함께 배포되어 둘 사이의 버전 불일치를 제거합니다.

피해야 할 것

여러 프론트엔드 애플리케이션 간에 BFF를 공유하지 마세요. 그렇게 하는 순간 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.