API Gateway와 BFF 도입 기준 및 보안 설계

· 약 10분 읽기 · 조회 2 #아키텍처#API Gateway#BFF#보안#Spring

API Gateway와 BFF(Backend for Frontend)는 둘 다 클라이언트와 서비스 사이에 서는 계층이라 자주 섞여 쓰이지만, 맡는 일과 막아야 할 위협이 다르다. 역할을 섞으면 게이트웨이가 화면 로직으로 비대해지거나, 반대로 "게이트웨이가 막아 주겠지" 하며 서비스가 인가를 빼먹는다. 이 글은 두 계층의 차이, 도입해야 할 때와 하지 말아야 할 때, 그리고 보안 검사를 게이트웨이·BFF·서비스 중 어디에 둘지 정리한 것이다.

호텔 로비 안내 데스크에서 투숙객이 확인을 받고 다른 손님들은 각자 엘리베이터로 향하는 장면

한눈에 보기

계층 하는 일 하지 않는 일
API Gateway TLS 종료, 토큰 검증, 속도·크기 제한, 라우팅, 요청 ID 부여 화면별 응답 조립, 업무 규칙 판단
BFF 한 종류의 클라이언트에 맞춘 API 조합, 웹 세션과 토큰 교환 다른 클라이언트의 요구 처리, 데이터 소유
서비스 업무 처리, 기능 권한과 객체 단위 인가 앞단 검사를 믿고 인가 생략

웹 브라우저와 모바일 앱이 각자의 BFF를 거쳐 API Gateway로 들어가고, 게이트웨이가 토큰 검증과 속도 제한을 한 뒤 서비스로 보내며, 서비스는 객체 단위 인가를 다시 하고, 게이트웨이를 건너뛴 직접 호출은 막히는 구조

요청은 클라이언트에서 BFF, 게이트웨이, 서비스 순으로 지나간다. 앞 계층일수록 "누구인가"를, 뒤 계층일수록 "이 데이터를 만져도 되는가"를 판단한다.

API Gateway와 BFF는 무엇이 다른가

API Gateway는 모든 외부 요청이 지나가는 단일 입구다. 관심사는 클라이언트 종류와 무관한 공통 처리다. 인증 토큰이 유효한지, 호출량이 한도를 넘지 않는지, 요청을 어느 서비스로 보낼지 정한다.

BFF는 Sam Newman이 정리한 패턴으로, 클라이언트 경험마다 전용 백엔드를 하나씩 둔다. 웹 화면 하나를 그리려면 주문·회원·알림 서비스를 세 번 불러야 할 때, 웹 BFF가 이를 한 번의 응답으로 묶어 준다. 모바일 BFF는 같은 데이터를 작은 화면과 느린 망에 맞게 줄여서 준다. 핵심은 "그 BFF를 쓰는 클라이언트 팀이 BFF도 소유한다"는 점이다. 화면이 바뀔 때 다른 팀의 일정을 기다리지 않아도 된다.

정리하면 게이트웨이는 가로 방향 공통 관심사, BFF는 세로 방향 클라이언트 전용 관심사다. 게이트웨이에 "모바일이면 필드를 줄여라" 같은 분기가 들어가기 시작하면 BFF가 필요하다는 신호다.

도입 기준: 언제 필요하고 언제 과한가

두 계층 모두 홉이 하나 늘고, 배포·모니터링 대상이 늘어난다. 이 비용을 감수할 이유가 있을 때만 들인다.

신호 게이트웨이 BFF
외부에 노출되는 서비스가 3개 이상 도입 검토 -
인증·속도 제한을 서비스마다 따로 구현 중 도입 검토 -
웹과 모바일의 화면 요구가 크게 다름 - 클라이언트별 도입
화면 하나에 서비스 호출이 여러 번 - 도입 검토
웹 앱이 브라우저에 토큰을 보관 중 - 웹 BFF 도입 검토
클라이언트 1종, 서비스 1개(단일 애플리케이션) 불필요 불필요

하지 않아도 되는 경우도 분명하다. 공공 SI에서 흔한 단일 WAR 구조는 앞단의 웹 서버나 리버스 프록시가 TLS와 접근 제어를 이미 맡고 있다. 이때 게이트웨이를 추가하면 같은 검사를 두 번 하고 장애 지점만 하나 더 생긴다. 클라이언트가 웹 하나뿐이면 BFF는 그냥 그 애플리케이션의 컨트롤러 계층이다. 별도 서버로 떼어낼 이유가 없다.

게이트웨이 하나냐, 클라이언트별 BFF냐

선택지는 보통 세 가지다.

  • 게이트웨이만: 클라이언트 요구가 비슷하고 API가 범용일 때. 가장 단순하다.
  • 게이트웨이 + 클라이언트별 BFF: 그림의 구성. 공통 검사는 게이트웨이에 한 번, 화면 맞춤은 BFF에서 한다.
  • BFF가 게이트웨이 역할까지: 클라이언트가 한두 종이고 팀이 작을 때. BFF마다 토큰 검증과 속도 제한을 같은 라이브러리로 넣는다.

"범용 BFF 하나"는 피한다. 웹과 모바일과 외부 기관이 모두 같은 BFF를 쓰면 그것은 이름만 BFF인 두 번째 게이트웨이가 되고, 각 클라이언트의 요구가 한 코드에서 충돌한다. BFF끼리 공통 로직이 생기면 BFF를 합치지 말고 라이브러리나 하위 서비스로 뺀다.

보안 1: 게이트웨이는 입구 검사만 한다

게이트웨이가 맡기 좋은 검사는 요청 내용을 깊이 몰라도 판단할 수 있는 것들이다.

  • TLS 종료와 재암호화: 종료 지점과 그 뒤 구간 설계는 TLS 종료 지점과 Reverse Proxy 신뢰 경계 설계에서 다뤘다.
  • 토큰 검증: 서명, 만료, 발급자(iss), 대상(aud)을 확인한다. 검증 방식 자체는 Spring Security JWT 인증과 인가 아키텍처를 따른다.
  • 속도 제한과 요청 크기 제한: 사용자나 클라이언트 단위로 초당 호출 수를 제한하고, 본문 크기 상한을 둔다. 로그인·인증번호 발송 같은 엔드포인트는 더 낮게 잡는다.
  • 외부에서 온 내부용 헤더 제거: 클라이언트가 보낸 X-User-Id, X-Roles 같은 헤더는 무조건 지운다. 지우지 않으면 누구나 헤더 한 줄로 다른 사용자가 된다.
  • 요청 ID 부여: 입구에서 X-Request-Id를 만들거나 받은 값을 검증해 모든 하위 호출에 전달한다. 장애 분석과 감사 로그를 한 요청 단위로 묶을 수 있다.

Spring Cloud Gateway로 구성하면 대략 다음과 같다.

spring:
  cloud:
    gateway:
      default-filters:
        - RemoveRequestHeader=X-User-Id
        - RemoveRequestHeader=X-Roles
      routes:
        - id: order
          uri: http://order-service:8080
          predicates:
            - Path=/api/orders/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 20
                redis-rate-limiter.burstCapacity: 40
            - name: RequestSize
              args:
                maxSize: 5000000

RequestRateLimiter는 누구 단위로 셀지 정하는 KeyResolver 빈과 Redis가 필요하다. 설정 접두어는 Spring Cloud 버전에 따라 바뀌었으므로 쓰는 버전의 공식 문서로 확인한다.

보안 2: 웹 BFF는 토큰을 브라우저에 두지 않는다

브라우저 앱이 Access 토큰을 자바스크립트 메모리나 localStorage에 들고 있으면, XSS 한 번에 토큰이 밖으로 나간다. 웹 BFF를 두면 이 문제를 구조로 없앨 수 있다. IETF OAuth 작업 그룹의 "OAuth 2.0 for Browser-Based Applications" 초안도 이 방식을 BFF 패턴으로 설명한다.

  1. 브라우저는 BFF와만 통신하고, 인증 상태는 HttpOnly; Secure; SameSite 세션 쿠키 하나로 유지한다.
  2. BFF가 OAuth 클라이언트가 되어 로그인 흐름을 처리하고, Access·Refresh 토큰을 서버 쪽 세션에 보관한다.
  3. 하위 API를 부를 때 BFF가 세션의 Access 토큰을 꺼내 Authorization: Bearer 헤더로 붙인다. Spring Cloud Gateway를 BFF로 쓰면 TokenRelay 필터가 이 일을 한다.

이 구성에서 브라우저는 토큰을 한 번도 보지 못한다. 대신 쿠키로 인증하므로 CSRF 방어는 BFF의 몫이 된다. 상태를 바꾸는 요청에는 CSRF 토큰을 요구하고 SameSite를 Lax 이상으로 둔다. BFF의 세션 저장소는 이제 토큰 보관소이므로 Redis 같은 외부 저장소를 쓴다면 접근 통제와 암호화를 함께 건다.

모바일 앱은 쿠키 기반 세션이 맞지 않으므로 PKCE를 쓴 OAuth 흐름으로 직접 토큰을 받고, 모바일 BFF는 그 토큰을 검증·전달한다.

보안 3: 인가는 서비스가 다시 한다

게이트웨이가 "로그인한 사용자"라고 확인해 줘도, 그 사용자가 주문 1234번을 볼 수 있는지는 주문 서비스만 안다. 객체 단위 인가를 앞단에 맡기면 BOLA가 그대로 열린다. 방어 방법은 Spring Security에서 IDOR·BOLA 취약점 방어하기에 정리했다.

서비스가 사용자 신원을 받는 방법은 두 가지다.

  • 서비스도 토큰을 직접 검증: 각 서비스를 리소스 서버로 두고 전달받은 JWT를 다시 검증한다. 게이트웨이를 우회해 들어온 요청도 막힌다. 기본값으로 권한다.
  • 게이트웨이가 넣은 헤더를 신뢰: 성능 때문에 헤더 방식을 쓴다면 그 헤더는 게이트웨이만 쓸 수 있어야 한다. 서비스는 게이트웨이 외의 출발지를 네트워크 정책이나 mTLS로 거부한다.

두 번째 방식에서 흔한 구멍은 내부망 직접 호출이다. 서비스 포트가 내부망에 열려 있으면, 내부에 들어온 공격자나 다른 시스템이 게이트웨이를 건너뛰고 헤더를 위조해 부를 수 있다. 내부라서 안전하다는 가정은 Zero Trust 관점의 관리자 시스템 접근 통제 설계에서 본 것처럼 성립하지 않는다.

검사 위치 빠지면 생기는 일
TLS, 속도·크기 제한 게이트웨이 서비스마다 다른 기준, 대량 호출에 서비스가 직접 노출
토큰 서명·만료 검증 게이트웨이 + 서비스 위조 토큰 통과, 우회 경로에서 무검증
내부용 헤더 제거 게이트웨이 헤더 한 줄로 사용자 위장
세션 ↔ 토큰 교환, CSRF 웹 BFF 브라우저 토큰 탈취, 쿠키 기반 위조 요청
기능 권한, 객체 단위 인가 서비스 다른 사용자 데이터 조회·수정

자주 하는 실수

  • 게이트웨이에 업무 로직을 넣는다. 응답 가공과 분기가 쌓이면 모든 팀이 게이트웨이 배포를 기다리는 병목이 된다.
  • 들어온 X-User-Id를 그대로 넘긴다. 외부 헤더를 지우지 않은 게이트웨이는 신원 위조 통로다.
  • 게이트웨이 인증을 서비스 인가로 착각한다. 로그인 여부와 데이터 접근 권한은 다른 질문이다.
  • 서비스 포트를 내부망에 열어 둔다. 게이트웨이를 우회하면 헤더 기반 신원은 아무 의미가 없다.
  • 모든 클라이언트가 하나의 BFF를 쓴다. 클라이언트별 요구가 한 코드에서 부딪혀 BFF를 둔 이유가 사라진다.

게이트웨이와 BFF가 늘어난 홉만큼 하위 장애가 전파되기 쉬우므로 타임아웃과 차단 설정은 서비스 장애 시 Graceful Degradation과 Circuit Breaker 설계를 참고해 함께 둔다.

점검표

  • 게이트웨이·BFF를 두는 이유가 도입 기준 표의 신호 중 하나 이상으로 설명된다
  • 게이트웨이에 클라이언트 종류에 따른 응답 가공 분기가 없다
  • BFF마다 소유 팀과 대상 클라이언트가 하나로 정해져 있다
  • 게이트웨이가 외부에서 온 X-User-Id, X-Roles 헤더를 지운다
  • 로그인·인증번호 발송 엔드포인트에 별도 속도 제한이 있다
  • 요청 크기 상한이 설정되어 있다
  • 브라우저 저장소(localStorage, 자바스크립트 변수)에 Access 토큰이 없다
  • 웹 BFF의 상태 변경 요청에 CSRF 방어가 켜져 있다
  • 서비스가 토큰을 직접 검증하거나, 게이트웨이 외 출발지를 네트워크 수준에서 거부한다
  • 객체 단위 인가를 서비스에서 두 사용자 테스트로 확인했다

게이트웨이는 누가 들어오는지를, BFF는 클라이언트가 무엇을 받는지를, 서비스는 그 사람이 이 데이터를 만져도 되는지를 판단한다. 이 세 질문을 한 계층에 몰지 않는 것이 도입 기준이자 보안 설계의 출발점이다.

댓글 0개

아직 댓글이 없습니다. 첫 댓글을 남겨보세요.

댓글 쓰기

← 목록으로 돌아가기