API 조합은 프런트엔드 데이터 집계를 통합합니다
Emily Parker
Product Engineer · Leapcell

소개
끊임없이 진화하는 백엔드 개발 환경에서 풍부하고 동적이며 개인화된 사용자 경험을 제공하는 데 필요한 요구 사항은 계속해서 증대되고 있습니다. 이러한 경험을 추구하는 프런트엔드 애플리케이션은 종종 다양한 분산 백엔드 서비스에서 데이터를 필요로 합니다. 전통적으로 BFF(Backend for Frontend) 패턴은 다양한 클라이언트 유형이나 보기별로 맞춤화된 데이터를 집계 및 변환하는 널리 채택된 솔루션으로 사용되었습니다. BFF는 프런트엔드 문제를 백엔드 복잡성에서 분리하는 데 부인할 수 없는 이점을 제공하지만, 클라이언트 유형이나 기능 집합의 수가 증가함에 따라 유지 관리성, 확장성 및 로직 중복과 관련하여 자체적인 문제점을 야기할 수 있습니다. 이 글에서는 API 조합 패턴을 활용하여 보다 유연한 프런트엔드 데이터 집계를 달성하는 대안적인 패러다임을 탐구합니다. 이를 통해 기존 BFF 접근 방식에서 오는 흥미로운 발전을 제공합니다. 이 방법론이 개발자가 보다 적응성 있고 견고한 시스템을 구축할 수 있도록 어떻게 지원하는지 살펴볼 것입니다.
환경 이해
API 조합의 구체적인 내용에 들어가기 전에 중요한 개념들에 대한 공통된 이해를 정립해 봅시다.
- BFF(Backend for Frontend): 각 특정 프런트엔드 애플리케이션 또는 클라이언트 유형을 위한 전용 백엔드 서비스가 생성되는 디자인 패턴입니다. 주요 역할은 여러 하위 마이크로서비스에서 데이터를 집계하고, 프런트엔드의 요구에 맞게 변환하며, 인증 또는 데이터 형식 지정과 같은 클라이언트별 문제를 처리하는 것입니다. BFF는 백엔드 복잡성을 추상화하고 특정 UI에 대한 데이터 검색을 최적화합니다.
- 마이크로서비스: 애플리케이션을 느슨하게 결합되고, 독립적으로 배포 가능하며, 종종 독립적으로 유지 관리 가능한 서비스 컬렉션으로 구조화하는 아키텍처 스타일입니다. 각 서비스는 일반적으로 특정 비즈니스 역량을 수행합니다.
- API 게이트웨이: 모든 클라이언트 요청에 대한 단일 진입점으로 작동하는 서비스입니다. 요청 라우팅, 조합, 프로토콜 변환, 인증, 권한 부여, 캐싱, 속도 제한 등 기타 교차 관심사를 처리할 수 있습니다. API 게이트웨이는 일부 데이터 집계를 수행할 수 있지만, 일반적으로 클라이언트별 데이터 모양 지정보다는 일반적인 라우팅 및 정책 적용을 위해 설계되었습니다.
- API 조합: 서비스(프런트엔드 자체, API 게이트웨이 또는 전용 조합 서비스일 수 있음)가 여러 하위 API 호출의 결과를 동적으로 결합하여 단일하고 일관된 응답을 형성하는 패턴입니다. 고정된 BFF와 달리 API 조합은 종종 GraphQL, HATEOAS 또는 서버 측 조합 라이브러리를 활용하여 요청 컨텍스트 또는 소비 클라이언트의 요구에 기반한 데이터의 동적 조립을 강조합니다.
API 조합으로의 전환
기존 BFF에 대한 대안으로서 API 조합의 핵심 원칙은 고정된 클라이언트별 백엔드 서비스에서 벗어나 보다 동적이고 선언적이거나 구성 가능한 집계 메커니즘으로 이동하는 것입니다. 모든 새 클라이언트나 기능에 대해 새로운 마이크로서비스(BFF)를 작성하는 대신, 필요에 따라 필요한 데이터를 조합할 수 있는 시스템을 목표로 합니다.
API 조합의 원칙
- 선언적 데이터 가져오기: 클라이언트는 데이터를 어떻게 가져올지가 아니라 어떤 데이터가 필요한지를 지정합니다. GraphQL과 같은 도구는 여기서 뛰어난 성능을 발휘하며, 프런트엔드가 정확한 데이터 모양을 정의할 수 있도록 하여 과잉 가져오기 또는 부족한 가져오기를 방지합니다.
- 상태 비저장 집계 로직: 조합 로직은 이상적으로는 상태 비저장이고 재사용 가능해야 합니다. 이는 수많은 개별 BFF 인스턴스 내에서 상태를 관리하는 것과 비교할 때 복잡성을 줄이고 확장성을 향상시킵니다.
- 유연한 변환: 집계는 데이터를 함께 가져오지만, 변환은 올바른 형식으로 보장합니다. API 조합은 유연하고 종종 클라이언트 중심적인 변환 기능을 지원해야 합니다.
- 분리: 관심사의 명확한 분리를 유지합니다. 백엔드 서비스는 해당 도메인에 집중하고, 조합 계층은 응답 조립에 집중합니다.
구현 전략 및 코드 예시
API 조합을 구현하는 데는 여러 가지 방법이 있으며, 각각 장단점이 있습니다.
1. GraphQL 게이트웨이
GraphQL은 아마도 API 조합의 가장 두드러진 예일 것입니다. 단일 GraphQL 엔드포인트는 조합 계층 역할을 하여 클라이언트가 통합된 스키마를 통해 여러 하위 마이크로서비스에 쿼리할 수 있도록 합니다.
예시 시나리오: 전자 상거래 프런트엔드는 제품 세부 정보, 해당 리뷰, 해당 제품에 대한 사용자의 주문 기록을 표시해야 합니다. 이들은 각각 ProductService
, ReviewService
, OrderService
에서 올 수 있습니다.
전통적인 BFF 접근 방식:
A ProductBFF
서비스는 다음과 같은 작업을 수행합니다:
ProductService
를 호출하여 제품 세부 정보 가져오기.ReviewService
를 호출하여 제품 리뷰 가져오기.OrderService
를 호출하여(사용자 컨텍스트 포함) 제품 주문 기록 가져오기.- 데이터 결합 및 반환.
GraphQL을 사용한 API 조합:
먼저 마이크로서비스의 유형을 집계하는 GraphQL 스키마를 정의합니다.
# Product Service Schema (simplified) type Product { id: ID! name: String! description: String price: Float # ... other product fields } # Review Service Schema (simplified) type Review { id: ID! productId: ID! rating: Int! comment: String # ... other review fields } # Order Service Schema (simplified) type OrderItem { productId: ID! quantity: Int! # ... other order item fields } type Query { product(id: ID!): Product # ... other queries } # Extend Product with reviews and user orders extend type Product { reviews: [Review!] userOrders: [OrderItem!] }
그런 다음 해당 마이크로서비스에서 데이터를 가져오는 GraphQL 게이트웨이/서버에서 리졸버를 구현합니다.
// Example in Node.js with Apollo Server const { ApolloServer, gql } = require('apollo-server'); const axios = require('axios'); // For making HTTP requests to microservices const typeDefs = gql` # ... schema definition from above ... `; const resolvers = { Query: { product: async (_, { id }) => { const response = await axios.get(`http://product-service/products/${id}`); return response.data; }, }, Product: { reviews: async (product) => { const response = await axios.get(`http://review-service/reviews?productId=${product.id}`); return response.data; }, userOrders: async (product, _, { userId }) => { // userId from context/authentication const response = await axios.get(`http://order-service/orders?userId=${userId}&productId=${product.id}`); return response.data; }, }, }; const server = new ApolloServer({ typeDefs, resolvers, context: ({ req }) => { // Extract userId from authentication token, for example const userId = req.headers.authorization || ''; return { userId }; }, }); server.listen().then(({ url }) => { console.log(`🚀 Server ready at ${url}`); });
프런트엔드 쿼리:
프런트엔드는 단일 GraphQL 쿼리를 발행할 수 있습니다.
query ProductDetails($productId: ID!) { product(id: $productId) { id name description reviews { rating comment } userOrders { quantity } } }
이 접근 방식은 GraphQL 계층에 조합 로직을 중앙 집중화합니다. 프런트엔드는 요청하는 데이터만 얻어 네트워크 페이로드를 줄이고 클라이언트 측 데이터 처리를 단순화합니다.
2. 서버 측 조합 (프록시/게이트웨이와 스마트 오케스트레이션)
이 패턴은 여러 서비스 호출을 오케스트레이션하고 응답을 결합할 수 있는 지능형 프록시 또는 API 게이트웨이를 포함합니다. 단순한 프록시와 달리 이 게이트웨이는 데이터 모델을 이해하고 통합된 응답을 재구성할 수 있습니다. 이는 선언적 라우팅 및 응답 변환을 지원하는 프레임워크와 함께 자주 활용됩니다.
예시 시나리오: 위와 유사하지만, GraphQL 대신 REST 응답을 지능적으로 결합하는 게이트웨이를 사용합니다.
구현 (예: Apache camel, Spring Cloud Gateway와 사용자 정의 필터 활용, 개념적):
/api/v1/product-rich-info/{productId}
와 같은 요청을 수신하는 게이트웨이를 고려해 봅시다.
// Spring Cloud Gateway Predicate/Filter example (conceptual) @Configuration public class GatewayConfig { @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route("product_rich_info_route", r -> r.path("/api/v1/product-rich-info/{productId}") .filters(f -> f.filter(new ProductRichInfoCompositionFilter())) // Custom filter .uri("lb://product-service")) // Initial call to product service .build(); } // Custom GatewayFilter to compose data public class ProductRichInfoCompositionFilter implements GatewayFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // First, let the request proceed to the product-service return chain.filter(exchange).then(Mono.defer(() -> { ServerHttpResponse response = exchange.getResponse(); // Capture the product data from the initial response // (requires custom BodyCaptureFilter or similar to intercept response body) // For simplicity, let's assume we can get the product ID from the path String productId = exchange.getRequest().getPath().pathWithinApplication().value().split("/")[4]; // Make subsequent calls to review-service and order-service // (using WebClient in a non-blocking fashion) Mono<ProductData> productMono = /* ... extract product data from initial response ... */; Mono<List<ReviewData>> reviewsMono = WebClient.builder().build() .get().uri("http://review-service/reviews?productId=" + productId) .retrieve().bodyToFlux(ReviewData.class).collectList(); Mono<List<OrderItemData>> ordersMono = WebClient.builder().build() .get().uri("http://order-service/orders?userId=someUserId&productId=" + productId) // UserID from JWT/session .retrieve().bodyToFlux(OrderItemData.class).collectList(); // Combine results return Mono.zip(productMono, reviewsMono, ordersMono) .flatMap(tuple -> { ProductData product = tuple.getT1(); List<ReviewData> reviews = tuple.getT2(); List<OrderItemData> orders = tuple.getT3(); // Create a combined JSON response Map<String, Object> combinedResponse = new HashMap<>(); combinedResponse.put("product", product); combinedResponse.put("reviews", reviews); combinedResponse.put("userOrders", orders); // Write combined response back to the client byte[] bytes = new Gson().toJson(combinedResponse).getBytes(StandardCharsets.UTF_8); DataBuffer buffer = response.bufferFactory().wrap(bytes); response.getHeaders().setContentLength(bytes.length); return response.writeWith(Mono.just(buffer)); }); })); } } }
이 접근 방식은 단순한 프록시보다 설정이 더 복잡하지만, 엄격하지 않은 게이트웨이 내에 조합 로직을 중앙 집중화합니다. 전체 BFF 서비스를 요구하지 않고 동적 조립을 허용합니다.
적용 시나리오
API 조합 패턴은 특히 다음과 같은 경우에 적합합니다:
- 마이크로서비스 아키텍처: 데이터가 본질적으로 여러 전문 서비스에 분산되어 있는 경우.
- 신속한 UI 개발: 프런트엔드가 수많은 BFF의 백엔드 변경을 기다리지 않고 변경되는 데이터 요구 사항에 신속하게 적응할 수 있습니다.
- 옴니채널 경험: 웹, 모바일 및 기타 클라이언트에서 활용할 수 있는 단일 통합 데이터 액세스 계층을 제공하여 보다 일관된 데이터 모델을 만듭니다.
- 다양한 데이터 요구 사항을 가진 개발자를 위한 공개 API:
- 백엔드 중복 감소: 여러 BFF 서비스에 걸쳐 유사한 집계 로직을 작성하는 것을 피합니다.
결론
전통적인 BFF(Backend for Frontend) 패턴이 프런트엔드 데이터 집계를 위한 귀중한 메커니즘으로 사용되었지만, 복잡한 시스템의 유연성, 확장성 및 유지 관리성 측면에서의 한계가 드러날 수 있습니다. GraphQL과 같은 기술이나 정교한 API 게이트웨이를 통해 API 조합 패턴을 채택함으로써 개발자는 데이터 집계에 대해 보다 동적이고 덜 규정적인 접근 방식을 달성할 수 있습니다. 이러한 변화는 프런트엔드 팀에 데이터 검색에 대한 더 큰 제어를 제공하고, 백엔드 개발 오버 헤드를 줄이며, 궁극적으로 보다 적응성 있고 견고하며 성능이 뛰어난 애플리케이션 아키텍처를 만듭니다. API 조합은 데이터 전달을 간소화하여 프런트엔드 집계를 진정으로 유연하고 효율적으로 만듭니다.