grep

Engineering

화면 단위 복잡성을 흡수하다: 여기어때 BFF의 기록

정혜인Lina(리나) / 항공연동개발팀여기어때

2026년 3월 10일

원문에서 보기 ↗

안녕하세요. 여기어때컴퍼니 항공연동개발팀(전 BFF개발팀) 리나입니다.

이번 글은 올해 2월까지 BFF개발팀으로서 근무하며 경험했던 내용을 바탕으로, “왜 BFF가 필요했는지” 그리고 “BFF팀이 어떤 역할을 수행했는지”를 정리한 기록입니다.

여기어때는 주문, 검색, 쿠폰, 리뷰, 항공, 결제 등 각 도메인 팀이 독립적으로 운영되는 MSA 구조를 기반으로 서비스되고 있습니다.

도메인 단위로 분리된 구조는 확장성과 자율성을 보장하지만, 화면 단위로 보면 또 다른 문제가 발생합니다.

“한 화면을 구성하기 위해 왜 이렇게 많은 API를 호출해야 할까?”

이 질문에서 BFF 도입이 시작되었습니다.

1. 잘 나뉜 도메인, 그러나 무거워진 화면

현재 여기어때의 백엔드는 거대한 시스템을 도메인 단위로 나눈 MSA(Microservices Architecture) 구조로 이루어져 있습니다.

2025년 12월 기준으로 B2C Gateway(GW)에 등록되어 사용 중인 서비스는 약 50여개에 달합니다. 주문결제, 유저혜택, 숙소, 검색, 전시, 패키지, 항공 등 수많은 도메인 팀이 각자의 Bounded Context 내에서 전문성 있게 API를 제공하고 있습니다.

하지만 데이터를 소비해야 하는 클라이언트(iOS, Android, Web) 입장에서는 어떨까요?

앞서 말씀드린 MSA 환경에서는 도메인이 세분화된 만큼 프론트엔드의 고충이 커지게 됩니다.

예를 들어, 새롭게 기획된 화면에서 아래 정보가 동시에 필요하다고 가정해보겠습니다.

이 경우 클라이언트는 5개 팀의 API를 모두 호출하여 조합하는 무거운 복잡한 연산을 수행해야합니다.

이러한 방식에는 다음과 같은 문제가 발생합니다.

즉, 도메인은 잘 나뉘어 있었지만

화면 단위 복잡성은 클라이언트에 남아 있었습니다.

2. BFF란 무엇인가

BFF(Backend For Frontend)는 단어 뜻 그대로 ‘프론트엔드를 위한 백엔드’를 의미합니다.

도메인 API가 기능 단위라면, BFF API는 화면 단위입니다.

BFF는 단순 라우팅 레이어가 아닌, 특정 클라이언트의 화면(UI)이 요구하는 데이터 구조에 맞춰 여러 도메인 API를 조합하고 가공하는 UI-Driven 서버 계층입니다.

BFF의 핵심 역할은 다음과 같습니다.

즉, 클라이언트가 하던 데이터 조합 책임을 서버로 이전하는 것.

이것이 BFF 도입의 본질이었습니다.

3. 구조의 변화

일반적인 API 구조

클라이언트 → Gateway → 각 도메인 API

기존의 여기어때 B2B/B2C Gateway는 단순한 트래픽 라우팅 역할에 가까웠습니다.

메쉬업, 데이터 조합, 비즈니스 분기 처리는 오롯이 클라이언트의 책임으로 남아있었고, 앱이 점점 무거워지는 원인이 되었습니다.

BFF 도입 구조

클라이언트 → BFF → (내부망) → 각 도메인 API

이제 클라이언트는 화면을 그리기 위해 단 하나의 BFF API만 호출합니다.

BFF는 내부 Private Zone 망에서 여러 도메인 API를 빠르게 호출하고, 클라이언트가 화면을 그리기 편한 View Model 형태로 가공해 내려줍니다.

클라이언트는 복잡한 메쉬업 로직을 알 필요가 없어졌습니다.

4. 실제 적용 사례

현재 파트너센터 와 광고센터에는 BFF가 중간 레이어로써 역할을 하고 있습니다. 아래 예시는 광고센터의 주문 내역 상세 페이지입니다.

화면을 온전히 구성하려면 다음과 같이 총 7~8개의 API를 호출해야만 주문 내역 정보를 온전히 표기할 수 있습니다.

public OrderDetailContext loadOrderDetailContext(long orderId, List<Long> affiliateIds) {
        
        // 주문 정보 조회
        OrderDetailResponse orderDetailExt = serviceAdvertisementComponent.getOrderDetail(orderId);

        // 주문과 엮인 결제 정보 조회
        PaymentDetailResponseExt paymentDetailExt = serviceAdvertisementComponent.getPaymentDetail(paymentId);

        // 주문과 엮인 제휴점 정보 조회
        AffiliateDetailResponseExt affiliateDetailResponseExt = serviceAdvertisementComponent.getAffiliateDetail(affiliateId);

        // 잔여금 내역 조회
        WalletResponseExt walletResponseExt = serviceAdvertisementComponent.getAffiliateWalletDetail(affiliateId);

        // 신용카드 내역 조회
        List<DepositEntryDtoExt> creditCardDeposits =
                getDepositsByPaymentMethod(affiliateId, PaymentMethodTypeExt.CARD_BIZ);
        // 가상계좌 내역 조회
        List<DepositEntryDtoExt> virtualAccountDeposits =
                getDepositsByPaymentMethod(affiliateId, PaymentMethodTypeExt.VBANK);

        if (plpOrderLineOpt.isPresent()) {
            ... 
            // 추가 주문 정보 조회
            List<OrderLinePageResponseExt> orderLines = serviceAdvertisementComponent.getOrderLines(request); 
            ...
        }
        ...
    }

여기서 복잡성은 단순히 여러 API를 호출하는 것에 그치지 않습니다. 주문 상세 화면은 상태 조합에 따라 비즈니스 로직이 달라지는 구조이기 때문입니다.

예를 들어 CTA 버튼 노출 여부는 결제 상태, 계약 동의 상태, 결제 수단, 중지 여부 등 여러 조건을 종합적으로 판단해 결정됩니다. 이러한 분기 로직을 클라이언트가 직접 처리할 경우 각 플랫폼마다 동일한 조건식을 반복 구현해야 하므로 복잡도가 급격히 증가합니다.

또한 결제 수단이 자동이체인 경우에는 일반 결제와 달리 출금 요청 정보를 추가로 조회해야 하므로, 응답 모델 자체가 달라집니다. 이에 따라 자동이체 전용 응답 생성 로직을 별도로 분리하여 처리하고, 그 외의 경우에는 표준 응답 모델을 사용하도록 설계했습니다.

즉, 결제 유형과 상태 조합에 따른 UI 분기 로직을 서버 단에서 흡수함으로써, 클라이언트는 단일 API 응답만으로 화면을 안정적으로 구성할 수 있도록 구조를 정리했습니다.

결과적으로 클라이언트는 복잡한 조합 로직을 알 필요 없이, BFF에서 제공하는 ‘주문 상세 조회 API’라는 단건의 API만 호출하면 화면 구성에 필요한 모든 정보가 정제된 형태로 내려옵니다.

결과적으로,

이라는 효과를 얻을 수 있었습니다.

BFF는 단순 중계 계층이 아니라,

화면 단위 복잡성을 흡수하는 조합 및 분기 레이어로 동작했습니다.

5. BFF 구조에서 발생할 수 있는 문제점 및 실제 사례

하지만 모든 아키텍처가 그렇듯 BFF 역시 만병통치약은 아닙니다. 중간에 계층이 하나 추가되면서 저희가 겪었던 문제와 주의사항도 함께 공유해 드립니다.

⚠️ 장애 전파 (Cascading Failures)

BFF는 기본적으로 여러 하위 도메인 API를 의존합니다. 만약 6개 중 1개의 API 응답이 계속 지연된다면, BFF 서버의 스레드는 해당 응답을 기다리느라(Block) 점차 고갈됩니다. 결국 해당 API와 무관한 다른 정상적인 요청들까지 처리하지 못하게 되는 ‘장애 전파’ 현상이 발생하게 됩니다.

저희는 Spring Retry 기반의 지수 백오프(Exponential Backoff) 재시도 전략 을 적용해 일시적 네트워크 오류나 순간적인 외부 API 불안정에 대해 방어했습니다. 재시도 후에도 실패할 경우에는 빠른 실패(Fast Fail) 또는 대체 응답(Fallback) 으로 전환해 화면 전체가 멈추는 최악의 상황을 피하도록 설계했습니다.

추가로, 특정 외부 API가 실패율/지연 시간이 임계치를 넘는 상태가 일정 시간 이상 지속 된다면, 재시도만으로는 리소스 소모가 커질 수 있습니다. 이런 경우에는 실패 임계치를 기준으로 호출 자체를 차단하는 서킷 브레이커(Circuit Breaker) 도입도 고려하였습니다.

@Retryable(
        retryFor = {RetryableException.class},
        noRetryFor = {NonRetryableException.class},
        maxAttempts = 4,
        backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 10000),
        listeners = {"retryListener"}
)

⚠️ 메모리 및 힙(Heap) 과부하

여러 마이크로서비스에서 대량의 데이터를 가져와 애플리케이션 메모리 위에서 조합(Join)하고 정렬하는 과정은 서버의 메모리를 크게 소모합니다. 하위 API에서 생각보다 큰 볼륨의 리스트를 반환하거나 병렬 스트림(Parallel Stream)을 남용할 경우, 잦은 GC(Garbage Collection)가 발생하거나 심하면 OOM(Out Of Memory)으로 서버가 다운될 위험이 있습니다.

저희는 이를 예방하기 위해 도메인 팀과 협의하여 하위 API 호출 시 반드시 페이징 처리를 강제하고, 반복 호출이 많은 데이터에는 Caffeine cache를 도입하여 불필요한 외부 호출과 객체 생성을 줄임으로써 메모리 부담을 줄여나갔습니다.

BFF는 화면 단위 편의성을 제공하는 레이어이지만 동시에 데이터가 집중되는 지점이기도 합니다. 따라서 기능 구현뿐 아니라 메모리 사용 패턴을 고려한 설계 역시 중요한 운영 과제로 다루었습니다.

@Cacheable(
        cacheNames = "priceCompetitionFilterCache",
        keyGenerator = "dashboardFilterKeyGen",
        sync = true
)
public CommonResponse<DashboardFilterResponse> getFilters(
@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public List<CaffeineCache> caffeineConfig() {
        return Arrays.stream(CacheType.values())
            .map(cache -> new CaffeineCache(cache.getCacheName(), Caffeine.newBuilder()
                    .recordStats()
                    .expireAfterWrite(cache.getExpireAfterWrite(), TimeUnit.SECONDS)
                    .maximumSize(cache.getMaximumSize())
                    .build()
                )
            )
            .toList();
    }

    @Bean
    public CacheManager cacheManager(List<CaffeineCache> caffeineCaches) {
        final SimpleCacheManager cacheManager = new SimpleCacheManager();
        cacheManager.setCaches(caffeineCaches);
        return cacheManager;
    }
}

6. 마치며

MSA는 도메인을 세분화합니다.

하지만 사용자는 도메인을 경험하지 않습니다. 사용자가 마주하는 것은 결국 하나의 화면입니다.

BFF는 그 화면을 중심에 두고 설계된 레이어였습니다.

도메인 간의 복잡성을 흡수하고, 상태 조합과 분기 로직을 정리해 화면이 온전히 동작하도록 만드는 역할을 담당했습니다.

지금도 여기어때의 많은 화면 뒤에서는 BFF가 여러 도메인의 데이터를 조합하고 재구성하는 중간 레이어로 동작하고 있습니다.

이 글이 BFF의 역할과 필요성을 이해하는데 도움이 되었기를 바랍니다.

긴 글 읽어주셔서 감사합니다.