BFF 패턴
역사와 등장 배경
웹사이트 하나를 만들 때는 그 화면에 맞는 서버 API를 함께 만들기 쉽다. 여기에 모바일 앱과 외부 연동이 붙으면 같은 데이터를 쓰더라도 필요한 응답이 달라진다. 상품 상세를 넓게 보여주는 웹, 이동 중 짧게 확인하는 앱, 데이터를 다른 서비스로 가져가는 외부 개발자가 API 하나의 변경 순서를 함께 기다리게 된다.
SoundCloud도 웹·모바일 앱·외부 연동이 공용 API를 함께 쓰던 구조에서 이 문제를 겪었다. 회사가 2021년에 공개한 회고는 BFF 도입을 2013년으로 설명한다. 서비스가 나뉜 뒤에도 각 화면이 여러 데이터를 가져와 합쳐야 했고, 화면별 API를 따로 두면서 담당 팀이 필요한 조합을 직접 만들기 시작했다.1
Phil Calçado는 2015년 9월 18일 이 과정을 정리했다. 처음에는 웹과 모바일의 API를 나누는 데서 출발했지만, 점차 클라이언트가 조립하던 사용자 프로필을 서버에서 완성해 돌려주게 됐다. 이 글은 BFF라는 이름을 당시 웹 기술 책임자 Nick Fisher가 붙였다고 기록한다.2
같은 해 11월 18일 Sam Newman은 REA와 SoundCloud의 사례를 바탕으로 패턴을 설명했다. 특정 사용자 경험을 위한 서버를 두고, 그 화면을 만드는 팀이 API도 함께 다룬다는 구성이었다. 웹과 모바일의 분리는 화면 차이뿐 아니라 팀이 API 변경을 결정하고 배포하는 방식과도 이어졌다.3
2015년 12월 Thoughtworks가 소개한 SoundCloud 사례에는 이 변화의 실무적 이유가 더 구체적으로 나온다. 앱에 새 엔드포인트가 필요할 때마다 다른 팀의 공용 API 작업으로 설득하고 우선순위를 기다리는 과정이 길어졌다. BFF를 맡은 프런트엔드 팀은 필요한 데이터와 응답 형식을 스스로 조정했다.4
개요
BFF(Backend for Frontend)는 특정 프런트엔드가 쓰기 좋은 API를 제공하는 서버 계층이다. 브라우저나 앱의 요청을 받아 필요한 서비스를 호출하고, 화면에서 바로 쓸 수 있는 응답으로 정리한다. 같은 상품 서비스와 재고 서비스를 쓰더라도 웹 BFF는 상세 화면용 데이터를, 모바일 BFF는 목록 카드용 데이터를 돌려줄 수 있다.5
BFF가 맞추는 대상은 화면의 작업이다. 내부 서비스가 상품과 재고를 따로 관리하더라도 사용자는 상품 화면 하나를 연다. 이 간격을 BFF에서 메우면 화면 코드는 내부 서비스의 분할 방식보다 자신이 표시할 데이터에 집중할 수 있다. 화면이 바뀔 때 함께 바뀌는 응답 계약을 어느 팀이 책임질지도 분명해진다.
쉬운 예제: 상품 이름과 재고 표시
모바일 앱에 상품 카드 하나를 띄운다고 해보자. 카드에는 상품 이름과 재고 유무가 필요하지만 상품 API는 이름과 가격을, 재고 API는 수량을 준다. 이 예제를 쓰는 이유는 두 조회 결과를 화면용 응답 하나로 바꾸는 과정만으로 BFF의 역할을 볼 수 있기 때문이다.
Catalog: GET /products/p-100
Inventory: GET /inventory/p-100Catalog response:
{ "id": "p-100", "name": "Travel mug", "price": 12000 }
Inventory response:
{ "quantity": 7 }앱이 두 API를 직접 호출하면 두 결과를 합치고 수량을 재고 표시로 바꾸는 일도 앱에서 한다. BFF를 두면 앱은 아래 요청 한 번으로 카드 데이터를 받는다. BFF 안에서는 여전히 두 서비스를 조회하며, 앱이 기다릴 응답과 화면 쪽 조립 코드가 줄어든다.
GET /mobile/products/p-100{ "id": "p-100", "name": "Travel mug", "inStock": true }이 예제의 모바일 카드는 가격을 표시하지 않으므로 응답에서도 뺐다. 웹 상세 화면에 가격과 정확한 재고 수량이 필요하면 웹 BFF에서 그 형식으로 제공한다. 어느 필드를 포함할지는 각 화면의 요구로 정한다.
UML로 보는 구성
컴포넌트 다이어그램은 어떤 부분이 다른 부분을 사용하는지 보여준다. 아래 사각형 오른쪽 위의 작은 표시는 UML 컴포넌트 표기이고, 점선 화살표는 사용하는 쪽에서 의존하는 쪽으로 향한다. Web UI와 Mobile UI는 각자의 BFF를 사용하고, 두 BFF는 공통 업무 서비스를 이용한다.6
이 그림에서 상품 서비스는 상품 정보를, 재고 서비스는 재고 조회와 예약을 맡는다. Web BFF와 Mobile BFF는 각각 화면에 필요한 데이터를 고르고 조립한다. 모바일 화면의 응답 필드가 바뀌면 Mobile BFF의 계약을 함께 바꾸는 식으로 책임을 따라갈 수 있다.
UML로 보는 요청 순서
시퀀스 다이어그램은 상품 카드를 여는 요청 한 건을 시간 흐름에 따라 보여준다. 세로 점선은 각 참여자의 생명선이며, 위에서 아래로 메시지를 읽는다. par 영역에서는 상품 조회와 재고 조회를 병렬로 진행한다.
BFF는 두 조회 결과를 받은 뒤 수량을 inStock으로 바꾸고 앱에 반환한다. 앱에서 BFF로 가는 요청은 한 번이고, BFF와 내부 서비스 사이에는 두 번의 조회가 있다. 병렬 조회는 두 서비스를 기다리는 시간을 겹치게 하지만, 응답 시간은 실제 네트워크와 느린 조회의 영향을 받는다.7
같은 흐름을 코드로 읽기
아래 JavaScript를 .mjs 파일로 저장하면 Node.js로 실행할 수 있다. 통신은 두 비동기 함수로 대신했다. 각 함수는 성공이면 ok: true와 value, 실패면 ok: false와 error를 반환한다고 약속하며, mobileProduct는 두 결과를 받아 화면용 객체를 만든다.
async function mobileProduct(id, services) {
const [product, inventory] = await Promise.all([
services.getProduct(id),
services.getInventory(id),
]);
if (!product.ok) return product;
if (!inventory.ok) return inventory;
return {
ok: true,
value: {
id: product.value.id,
name: product.value.name,
inStock: inventory.value.quantity > 0,
},
};
}
const services = {
getProduct: async (id) => ({
ok: true, value: { id, name: "Travel mug", price: 12000 },
}),
getInventory: async () => ({ ok: true, value: { quantity: 7 } }),
};
console.log(await mobileProduct("p-100", services));{ "ok": true, "value": { "id": "p-100", "name": "Travel mug", "inStock": true } }재고 조회가 실패하면 함수는 그 실패를 반환한다. 실패를 수량 0으로 바꾸면 앱은 조회 장애를 품절로 표시하게 되므로 이 예제에서는 그 둘을 구별한다. 실제 API에서는 이 결과를 화면이 이해하는 오류 응답으로 변환한다.
어떤 규칙을 어디에 둘까
부분 | 이 예제에서 맡는 일 |
|---|---|
프런트엔드 | 상품 카드를 그리고 사용자 입력을 받는다. |
BFF | 상품·재고 결과를 모아 화면용 응답을 만든다. |
업무 서비스 | 상품 가격, 재고 수량과 예약 가능 여부를 결정한다. |
API Gateway | 여러 API의 공통 진입점에서 라우팅과 공통 접근 정책을 적용한다. |
- 부분
프런트엔드
- 이 예제에서 맡는 일
상품 카드를 그리고 사용자 입력을 받는다.
- 부분
BFF
- 이 예제에서 맡는 일
상품·재고 결과를 모아 화면용 응답을 만든다.
- 부분
업무 서비스
- 이 예제에서 맡는 일
상품 가격, 재고 수량과 예약 가능 여부를 결정한다.
- 부분
API Gateway
- 이 예제에서 맡는 일
여러 API의 공통 진입점에서 라우팅과 공통 접근 정책을 적용한다.
API Gateway는 여러 API에 공통으로 적용할 진입 규칙을 맡고, BFF는 특정 화면에 맞춘 응답을 맡는다. 배치에 따라 Gateway 뒤에 BFF를 두거나 한 서비스가 두 역할을 함께 담당할 수 있다. GraphQL 역시 API를 표현하는 방식이므로, 화면 전용 리졸버를 가진 BFF의 구현에 사용할 수 있다.5
상품 카드의 inStock은 조회 시점의 표시 값이다. 구매할 때 실제로 수량을 예약할 수 있는지는 재고 서비스가 판단한다. 웹과 앱이 같은 구매 규칙을 따라야 한다면 그 규칙의 원본을 업무 서비스에 두고 각 BFF가 사용한다.
운영하면서 생기는 변화
처음에는 응답 몇 개를 조립하던 BFF에도 기능이 쌓인다. SoundCloud의 회고는 작은 CRUD 서비스들을 연결하는 업무 로직까지 BFF에 모이면 여러 BFF 사이에서 규칙이 복제되고 서로 달라지는 문제를 지적한다. 공통된 업무 개념이 드러나면 그 개념을 담당하는 서비스로 옮기고, BFF에는 화면별 조합을 남길 수 있다.1
여러 서비스의 응답을 기다리는 엔드포인트에는 기다릴 시간과 실패 시 응답을 정한다. 상품 정보가 필수이고 추천 목록은 선택이라면 추천 실패를 별도 상태로 내려 화면에서 생략할 수 있다. 캐시도 응답에 담긴 정보의 갱신 주기와 사용자 범위에 맞춰야 하며, 호출 추적에는 같은 요청 식별자를 이어 전달한다.7
도입을 판단하는 기준
웹과 앱이 서로 다른 응답을 원하거나 화면 하나를 위해 여러 서비스를 반복해서 조회한다면, 그 조합을 BFF로 옮겼을 때 화면 코드와 팀 간 조율이 얼마나 줄어드는지 볼 만하다. 반대로 화면 하나가 이미 적절한 API 하나로 끝난다면 별도 BFF 서버를 배포하고 운영할 이유는 약하다.
BFF의 경계는 사용자 경험과 팀의 변경 방식에 맞춰 잡는다. 비슷한 iOS·Android 앱을 한 팀이 관리하면 하나를 함께 쓰는 선택도 가능하고, 기능과 출시 주기가 갈라지면 각각의 BFF를 둘 수 있다.3
다이어그램의 표기법은 UML 문서와 함께 읽을 수 있다.
각주
- Jorge Creixell. Service Architecture at SoundCloud — Part 1: Backends for Frontends. SoundCloud Backstage, 2021-07-29. Backends for Frontends; The Bad. ↩
- Phil Calçado. The Back-end for Front-end Pattern (BFF). 2015-09-18. The challenges with dogfooding; The Back-end for Front-end Pattern (BFF). ↩
- Sam Newman. Pattern: Backends For Frontends. 2015-11-18. Introducing The Backend For Frontend; How Many BFFs?; And Autonomy. ↩
- Lukasz Plotnicki. BFF @ SoundCloud. Thoughtworks, 2015-12-09. Motivation; Solution; Evolution of a BFF. ↩
- Microsoft. Backends for Frontends pattern. Azure Architecture Center. Context and problem; Solution; Problems and considerations. ↩
- Object Management Group. Unified Modeling Language 2.5.1. December 2017. Components; Interactions. ↩
- Microsoft. Gateway Aggregation pattern. Azure Architecture Center. Solution; Problems and considerations. ↩