BFF API
BFF: Backend-For-Frontend
프론트엔드의 요구에 최적화된 백엔드의 레이어
멀티플랫폼을 지원하는 서비스에서 한 백엔드를 사용한다면, 한 백엔드가 여러 프론트엔드의 API 호출을 대응함
백엔드 api는 여러 플랫폼의 요구사항을 모두 충족시키기 위해 모든 데이터를 반환하도록 구현하게 됨
각 플랫폼의 프론트엔드는 요청한 것 외에 다른 데이터까지 함께 받게 되는 셈이라 데이터 처리 로직이 복잡해짐
MSA(MicroService Architecture) - 하나의 어플리케이션을 독립적인 여러 개의 마이크로 서비스로 구성
여러 마이크로 서비스를 호출해서 데이터를 조합해야 하는 경우 클라이언트가 이를 직접 수행하면 api 호출 횟수 증가함
BFF를 사용하면 여러 마이크로서비스에서 필요한 데이터를 조합해서 클라이언트가 필요한 형태로 가공할 수 있음
(멀티플랫폼은 배포 및 실행 환경, MSA는 소프트웨어 아키텍처 방식을 의미함. 서로 다른 개념임)
(MSA를 사용해도 단일 플랫폼을 만들 수 있고, 멀티플랫폼이어도 MSA를 사용하지 않을 수 있음)
또, 결재 로직 같은 경우에는 여러 복합적인 정보가 필요해 API를 여러 번 호출해야 함
필요한 모든 API 호출을 프론트엔드에서 반복하다 보면 로직이 복잡해짐
BFF, 프론트엔드 생산성을 높이고자 데이터를 통합하는 처리 담당
1. BFF에서 API 응답을 조합해 한 번의 요청으로 필요한 데이터를 받을 수 있게 설계할 수 있음
2. BFF에서 API 응답을 가공해서 필요한 데이터만 프론트엔드로 전달할 수 있음.
3. 멀티플랫폼 서비스의 경우, 각 플랫폼 서비스마다 하나의 BFF를 가짐 (앱, 웹, 데스크톱 등)
시나리오 - 여러 API를 호출하는 경우
1. REST API(기본적으로 사용하는 예시)
응답 값을 받아 가공한 후 상태 관리에 저장한다
화면에 필요하지 않은 response_time, create_dt 같은 값이 내려오고,
브라우저 쪽에서는 필요한 만큼 API 요청을 반복해야 한다
2. GraphQL로 BFF 구현
POST로 요청이 발생하고, 브라우저의 캐시 대신 라이브러리에서 제공을 해준다
REST API는 필요 없는 데이터를 포함(오버패칭)하거나, 부족한 데이터를 여러 번 호출(언더패칭)해야 함
GraphQL은 클라이언트가 원하는 필드만 선택해 요청할 수 있고, 하나의 쿼리로 여러 데이터를 가져올 수 있음
API의 데이터 구조를 미리 정의하는 Schema를 사용해, 타입 검사로 API 응답을 예측 가능하게 만듦
API 응답 값이 먼저 BFF에 도착하고, BFF에서 화면에 필요한 데이터만 가공해서 프론트엔드로 전달함
[1] Apollo Server 구현
const { ApolloServer, gql } = require("apollo-server");
const axios = require("axios");
// GraphQL 스키마 정의
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
}
type Post {
id: ID!
title: String!
content: String!
author: User
}
type Comment {
id: ID!
text: String!
post: Post
author: User
}
type Query {
user(id: ID!): User
post(id: ID!): Post
comments(postId: ID!): [Comment]
}
`;
// REST API에서 데이터를 가져오는 Resolver
const resolvers = {
Query: {
user: async (_, { id }) => {
const response = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`);
return response.data;
},
post: async (_, { id }) => {
const response = await axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`);
const userResponse = await axios.get(`https://jsonplaceholder.typicode.com/users/${response.data.userId}`);
return {
...response.data,
author: userResponse.data,
};
},
comments: async (_, { postId }) => {
const response = await axios.get(`https://jsonplaceholder.typicode.com/comments?postId=${postId}`);
return response.data;
},
},
};
// Apollo Server 생성
const server = new ApolloServer({ typeDefs, resolvers });
// 서버 실행
server.listen().then(({ url }) => {
console.log(`🚀 GraphQL 서버 실행 중: ${url}`);
});
블로그 글 API를 불러오는 시나리오를 가정해보자
우선, API 수신 후 필요한 정보만 가공하기 위해 Schema를 만든다
resolver에서 여러 개의 API 요청을 한 쿼리문으로 한 번에 처리한다
사용자 정보, 게시글 정보, 댓글 정보를 한 번에 가져온다
예시에서 Apollo Server가 등장했다!
Apollo Server는 GraphQL API를 쉽게 구축할 수 있도록 도와주는 오픈소스 Node.js용 GraphQL 서버이다
GraphQL Schema 정의, Resolver 작성, Data Fetching 구현이 간단해지도록 도와준다
Express, Fastify와 통합할수도 있고, 독립적인 서버로 실행할 수도 있다
위 예시 자체가 Apollo Server를 구축의 결과다
Apollo Server는 NestJS, Fastify, Koa와 통합해 실행할 수도 있다
1. NestJS
NestJS는 GraphQL 모듈을 내장하고 있음
@nestjs/graphql 패키지로 Apollo Server를 통합할 수 있음
import { Module } from "@nestjs/common";
import { GraphQLModule } from "@nestjs/graphql";
import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo";
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: true, // GraphQL SDL 자동 생성
}),
],
})
export class AppModule {}
2. Fastify
Express보다 빠른 성능을 제공하며, apollo-server-fastify를 이용해 GraphQL을 추가할 수 있음
const Fastify = require("fastify");
const { ApolloServer, gql } = require("@apollo/server");
const fastifyApollo = require("@as-integrations/fastify");
const app = Fastify();
const typeDefs = gql`
type Query {
hello: String
}
`;
const resolvers = {
Query: {
hello: () => "Hello from Fastify!",
},
};
const apolloServer = new ApolloServer({ typeDefs, resolvers });
(async () => {
await apolloServer.start();
app.register(fastifyApollo.default, { apolloServer });
app.listen({ port: 3000 }, () => {
console.log("🚀 Fastify + Apollo Server 실행 중: http://localhost:3000/graphql");
});
})();
3. Koa에서 Apollo Server 사용
Koa는 미들웨어 기반의 백엔드 프레임워크, apollo-server-koa를 사용해 GraphQL을 추가할 수 있음
const Koa = require("koa");
const { ApolloServer, gql } = require("@apollo/server");
const { koaMiddleware } = require("@as-integrations/koa");
const app = new Koa();
const typeDefs = gql`
type Query {
hello: String
}
`;
const resolvers = {
Query: {
hello: () => "Hello from Koa!",
},
};
const apolloServer = new ApolloServer({ typeDefs, resolvers });
(async () => {
await apolloServer.start();
app.use(koaMiddleware(apolloServer));
app.listen(3000, () => {
console.log("🚀 Koa + Apollo Server 실행 중: http://localhost:3000/graphql");
});
})();
[2] Apollo Client 구현
import { ApolloClient, InMemoryCache, gql } from "@apollo/client";
const client = new ApolloClient({
uri: "http://localhost:4000",
cache: new InMemoryCache(),
});
client
.query({
query: gql`
query {
user(id: "1") {
name
email
}
post(id: "1") {
title
content
author {
name
email
}
}
comments(postId: "1") {
text
author {
name
}
}
}
`,
})
.then((response) => console.log(response.data));
프론트엔드에서 위 구현된 BFF에 GraphQL Query를 요청하려면 위와 같이 작성하면 된다
ApolloClient를 사용하는 방식이다
[3] 응답
{
"data": {
"user": {
"name": "Leanne Graham",
"email": "leanne@example.com"
},
"post": {
"title": "GraphQL API Example",
"content": "This is a sample post content.",
"author": {
"name": "Leanne Graham",
"email": "leanne@example.com"
}
},
"comments": [
{
"text": "Great post!",
"author": {
"name": "Bret"
}
},
{
"text": "Very helpful, thanks!",
"author": {
"name": "Antonette"
}
}
]
}
}
요청한 내용에 대한 가공된 응답이 회신된다
Apollo Client를 사용할 때 주의할 점
관련 정보를 찾던 중 카카오페이지에서 Apollo Client의 문제점에 대해 작성한 글을 봤다
Apollo Client의 캐싱 방식은 Nomalized Cache를 사용해 id를 기준으로 데이터를 참조한다
중복 데이터 재호출을 막아 성능을 향상시킬 수 있지만, 캐싱 문제가 발생할 수 있다
카카오엔터 테크 블로그의 예시에서는 카카오페이지에서 특정 카테고리에 해당하는 데이터를 불러오는 예시를 설명했다
(자세한 예시는 원본 링크에서 확인! https://fe-developers.kakaoent.com/2022/220310-kakaopage-bff/ )
Layout 1번 아래에는 여러 category filter가 존재하고, 특정 카테고리에 해당하는 데이터만 불러올 수 있다
(아마 웹툰 중 SF, 로맨스 등 특정 장르만 불러오는 필터링 같은 게 아닐까?)
layout({categoryFilter: 1}) 호출
Layout:1 - Section:1 - groups[Group:1, Group:3]
layout({categoryFilter: 2}) 호출
Layout:1 - Section:1 - groups[Group: 2]
Section id가 1로 같기 때문에 이전의 호출인 1,3을 2로 덮어씌운다
layout({catergoryFilter: 2}) 재호출
아까 호출한 categoryFilter: 1 응답이 캐시되어 있으니 반환해야지! 라고 판단한다
그러나 categoryFilter: 1 응답을 캐싱한 Section :1의 정보는 이미 categoryFilter: 2 응답이 덮어씌운 상태임..
따라서 group 1/3번이 아니라 group 2번이 return된다
해결법
1. Section의 id를 categoryFilter에 따라 다르게 만든다
2. Apollo Client의 keyFileds 설정 - 특정 필드를 기준으로 캐싱을 식별할 수 있음
3. Urql(라이브러리): 캐시를 document 방식으로 저장 - ID 기반이 아니라 쿼리 자체를 캐싱함
3. WebFlux와 코루틴을 활용한 BFF 구현
(출처: 카카오페이 테크. WebFlux와 코루틴으로 BFF 구현하기 https://tech.kakaopay.com/post/bff_webflux_coroutine)
카카오페이 기술블로그에서는 GraphQL이 아닌 WebFlux+코루틴 조합으로 BFF를 구현했다
GraphQL을 쓰지 않은 이유는 카카오페이에서 구현하고자 하는 기능은 MSA이지만, 멀티플랫폼이 아니었기 때문이다
카카오맵, 가게 정보, 혜택 등이 각각 다른 마이크로 서비스로 구현되어 있어서 이를 하나로 합쳐주는 BFF 로직이 필요한데, 이 기능을 모바일 화면에만 적용하면 된다는 뜻. 여러 API를 거쳐 데이터를 가져올 때 속도를 높이기 위해 non-blocking&비동기 패턴을 적용하므로, Spring의 WebFlux로 구현했다고 함.
GraphQL은 모바일, 데스크톱 등 UI 를 여러 개 지원할 때, GraphQL이 백엔드에서 데이터를 불러서 필요한 내용만 걸러 프론트엔드에 전달하는 것인데, 지금 상황은 모바일만 제공하면 돼서 GraphQL까지 도입할 필요가 없었다고 함.
*아래 예시는 테크 블로그의 예시를 간소화한 버전이므로, 정확한 예제는 링크를 참고 부탁드립니다
일반 BFF 구현
여러 API를 순차적으로 호출함. 동기적 방식
사용자 정보 API 호출(200ms) - 결제 내역 API 호출(300ms) - 추천 상품 API 호출(400ms) = 900ms
fun getBffData(): BffResponse {
val user = userApi.getUser() // 200ms
val payments = paymentApi.getPayments() // 300ms
val recommendations = recommendApi.getRecommendations() // 400ms
return BffResponse(user, payments, recommendations)
}
WebFlux
Spring에서 리액티브 프로그래밍 기반으로 구현된 비동기 처리 방식
여러 개의 API 호출을 동시에 처리할 수 있어 응답 속도가 빨라짐
기존의 Spring MVC와 달리, 요청이 들어오면 blocking 없이 바로 다음 작업을 처리함(non-blocking)
코루틴
비동기 코드를 통해 동기 코드처럼 간결하게 작성할 수 있는 기능
기존 리액티브 스트림(WebFlux의 Mono, Flux)보다 가독성이 좋고 유지보수가 쉬움
async {}로 사용자 정보/결제 내역/추천 상품 API 3개를 동시에 호출함
awiat()를 호출할 때까지 결과를 기다리지 않고 다른 작업을 수행함
병렬처리 하니까 최대 400ms 걸림
suspend fun getBffData(): BffResponse = coroutineScope {
val userDeferred = async { userApi.getUser() }
val paymentsDeferred = async { paymentApi.getPayments() }
val recommendationsDeferred = async { recommendApi.getRecommendations() }
val user = userDeferred.await()
val payments = paymentsDeferred.await()
val recommendations = recommendationsDeferred.await()
BffResponse(user, payments, recommendations)
}
출처
[1] 카카오엔터 FE 기술 블로그. 카카오페이지는 BFF를 어떻게 적용했을까?. https://fe-developers.kakaoent.com/2022/220310-kakaopage-bff/
[2] gpt에게 BFF API와 GraphQL에 대해 묻다
[3] 카카오페이 기술 블로그. WebFlux와 코루틴으로 BFF 구현하기 https://tech.kakaopay.com/post/bff_webflux_coroutine/
'개발자 강화 > 프론트엔드' 카테고리의 다른 글
[개발, 매일메일] @tanstack/react-query v4/v5 차이...(suspense, error-boundary 처리) (0) | 2025.02.14 |
---|---|
[매일메일] 민감한 데이터는 어디에 저장해야 할까? (FE.250211) (0) | 2025.02.11 |
🌟[공부, 매일메일] Suspense란? (1) | 2025.02.10 |
[매일메일] React의 Concurrent Mode(동시성 모드) (FE.250207) (0) | 2025.02.08 |
[매일메일] React의 메모이제이션? (FE.241223/250206) (0) | 2025.02.06 |