Skip to content
Published on

인가 모델의 진화 — RBAC, ABAC, ReBAC 그리고 OpenFGA/Zanzibar

Authors

들어가며 — 인증의 다음 전장, 인가

지난 몇 년간 업계는 인증(authentication) 문제를 상당 부분 해결했습니다. OIDC/SAML 기반 SSO는 상식이 되었고, passkey가 비밀번호를 대체하고 있습니다. 그런데 "누구인지 확인"한 다음의 질문 — "이 사용자가 이 리소스에 이 작업을 해도 되는가?" — 는 여전히 각 애플리케이션에 흩어진 if문 덩어리로 남아 있는 경우가 대부분입니다.

2026년에 인가(authorization)가 다시 뜨거운 주제가 된 배경은 명확합니다.

  1. 마이크로서비스와 멀티테넌시: 권한 판단 로직이 수십 개 서비스에 중복 구현되면서 일관성과 감사가 불가능해졌습니다.
  2. 협업 기능의 보편화: "이 문서를 이 사람에게 공유"라는 Google Docs식 요구사항은 전통적인 역할 기반 모델로는 표현이 안 됩니다.
  3. AI 에이전트의 등장: 인간이 아닌 주체가 리소스에 접근하면서, 세밀하고(fine-grained) 감사 가능한 인가가 필수가 되었습니다.
  4. 규제: 최소 권한 원칙(least privilege)의 증명을 요구하는 컴플라이언스가 늘었습니다.

이 글은 RBAC → ABAC → ReBAC으로 이어지는 인가 모델의 진화를 추적하고, Google Zanzibar 논문의 핵심 개념과 그 오픈소스 구현체인 OpenFGA의 실전 모델링, 그리고 마이크로서비스 환경에서의 아키텍처 패턴까지 다룹니다.

먼저 용어부터 정리합니다.

용어의미
AuthN (인증)당신이 누구인지 확인 — OIDC, SAML, passkey의 영역
AuthZ (인가)당신이 무엇을 할 수 있는지 결정 — 이 글의 주제
PEPPolicy Enforcement Point — 결정을 집행하는 지점 (API 게이트웨이, 서비스 코드)
PDPPolicy Decision Point — 허용/거부를 결정하는 지점 (인가 엔진)
PAPPolicy Administration Point — 정책을 관리하는 지점
PIPPolicy Information Point — 결정에 필요한 속성을 공급하는 지점

RBAC — 그리고 Role Explosion이라는 저주

RBAC(Role-Based Access Control)은 가장 널리 쓰이는 모델입니다. 사용자에게 역할(role)을 부여하고, 역할에 권한(permission)을 묶습니다.

사용자 ──(할당)──> 역할 ──(보유)──> 권한
jane  ─────────> editor ────────> document:write
john  ─────────> viewer ────────> document:read

RBAC의 장점은 명백합니다. 이해하기 쉽고, 감사가 단순하며("editor 역할 보유자 목록 출력"), NIST RBAC 표준(INCITS 359)이라는 성숙한 이론 기반도 있습니다.

문제는 현실의 권한 요구가 "역할"이라는 단일 차원으로 표현되지 않는다는 점입니다.

  • "editor인데, 자기 부서 문서만"
  • "viewer인데, 근무 시간에만"
  • "manager인데, 자기가 결재 올린 건은 승인 불가"

이런 요구를 RBAC으로 욱여넣으면 역할이 차원의 조합 수만큼 폭증합니다. 이것이 악명 높은 role explosion입니다.

editor                          → 부서 차원 추가 →  editor-sales
                                                    editor-hr
                                                    editor-engineering
                                → 지역 차원 추가 →  editor-sales-kr
                                                    editor-sales-us
                                                    editor-hr-kr
                                                    ... (조합 폭발)

실제 대기업 IGA 감사에서 "사용자 수보다 역할 수가 많은" 사례는 드물지 않습니다. 역할이 수천 개가 되는 순간 RBAC의 유일한 장점이었던 "이해하기 쉬움"이 사라집니다.

그럼에도 RBAC은 죽지 않습니다. 조직 수준의 굵직한 권한 구분(coarse-grained) — 예: admin/member/billing-manager — 에는 여전히 최적입니다. 문제는 리소스 단위의 세밀한 제어를 RBAC으로 하려 할 때 생깁니다.

ABAC — 속성과 정책으로, 그러나 XACML의 교훈

ABAC(Attribute-Based Access Control)은 주체(subject), 리소스(resource), 행위(action), 환경(environment)의 속성을 조건식으로 평가합니다.

허용 조건:
  subject.department == resource.department
  AND action == "edit"
  AND environment.time BETWEEN 09:00 AND 18:00
  AND subject.clearance >= resource.classification

role explosion 문제는 우아하게 해결됩니다. 부서가 100개여도 정책은 한 줄입니다.

ABAC의 표준화 시도가 OASIS XACML이었습니다. XACML은 PEP/PDP/PAP/PIP라는 아키텍처 어휘를 남겼다는 점에서 유산이 크지만, 그 자체는 사실상 실패한 표준으로 평가됩니다. 이유는 다음과 같습니다.

  • XML 기반의 극단적인 장황함: 간단한 규칙 하나가 수십 줄의 XML이 됩니다.
  • 디버깅 불가능성: 정책 충돌(combining algorithm)의 결과를 사람이 예측하기 어렵습니다.
  • 개발자 경험 부재: 정책 작성이 전문 도구 없이는 불가능에 가까웠습니다.

XACML의 정신은 이후 policy-as-code 진영 — 특히 OPA(Open Policy Agent)와 Rego 언어 — 으로 계승됩니다. 같은 ABAC 평가를 훨씬 다루기 쉬운 형태로 구현한 것입니다.

그러나 ABAC에도 구조적 한계가 있습니다. "jane이 이 특정 문서를 볼 수 있는가"는 잘 답하지만, "jane이 볼 수 있는 문서 전체 목록"(reverse query)을 뽑는 데는 취약합니다. 또한 "문서 X를 폴더 Y에 넣었고, 폴더 Y는 팀 Z에 공유됐다"처럼 관계의 연쇄로 권한이 파생되는 시나리오는 속성으로 모델링하기가 부자연스럽습니다.

ReBAC — Google Zanzibar가 제시한 패러다임

이 지점에서 등장한 것이 ReBAC(Relationship-Based Access Control)입니다. 권한을 속성의 조건식이 아니라 주체와 객체 사이의 관계 그래프로 모델링합니다. 그 정점이 2019년 발표된 Google의 Zanzibar 논문입니다. Zanzibar는 Google Docs, Drive, YouTube, Cloud 전체의 인가를 담당하는 단일 글로벌 시스템으로, 수조 개의 ACL을 저장하고 초당 수백만 건의 권한 질의를 10ms 수준의 지연으로 처리합니다.

핵심 개념 1 — Relationship Tuple

Zanzibar의 모든 권한 데이터는 다음 형태의 튜플로 표현됩니다.

object#relation@user

예시:
doc:budget-2026#owner@user:jane          jane은 budget-2026 문서의 owner
doc:budget-2026#viewer@group:finance#member
                                         finance 그룹의 member는 viewer
folder:q1#parent@doc:budget-2026         budget-2026의 부모는 q1 폴더

세 번째 예시처럼 user 자리에 다른 객체의 userset이 올 수 있다는 점이 강력합니다. "finance 그룹의 멤버 전원"이라는 집합을 한 줄로 가리킬 수 있고, 그룹 멤버십이 바뀌어도 문서 쪽 튜플은 그대로입니다.

핵심 개념 2 — Userset Rewrite

튜플이 데이터라면, userset rewrite 규칙은 권한의 파생 로직입니다. 예를 들어 "owner는 자동으로 editor이기도 하다", "부모 폴더의 viewer는 자식 문서의 viewer다" 같은 규칙을 객체 타입별로 선언합니다.

viewer 관계의 평가 규칙 (개념적 표현):
  viewer =
      직접 viewer로 지정된 사용자       (this)
    ∪ editor인 사용자                   (computed userset)
    ∪ parent 폴더에서 viewer인 사용자   (tuple-to-userset)

이 세 가지 — 직접 관계, 계산된 관계(computed userset), 관계를 타고 넘어가는 관계(tuple-to-userset) — 의 합집합/교집합/차집합 조합만으로 Google Drive 수준의 공유 모델이 표현됩니다.

핵심 개념 3 — Zookie와 일관성

분산 시스템에서 인가의 고전적 함정은 **"new enemy problem"**입니다. 예를 들어 (1) jane을 문서에서 제거하고 (2) 문서에 민감한 내용을 추가했는데, 복제 지연 때문에 (2)의 읽기 시점에 (1)이 아직 반영되지 않으면, 제거된 jane이 새 내용을 보게 됩니다. 순서가 보장되지 않는 캐시/복제는 보안 사고가 됩니다.

Zanzibar는 zookie라는 일관성 토큰으로 이를 해결합니다. 콘텐츠를 수정할 때 zookie를 받아 저장해 두고, 권한 체크 시 그 zookie를 함께 보내면 "최소한 그 시점 이후의 ACL 상태"로 평가가 보장됩니다. 외부 일관성(external consistency)과 성능(과감한 캐싱) 사이의 균형을 잡는 우아한 장치입니다. OpenFGA에서는 consistency 파라미터(예: HIGHER_CONSISTENCY)로 유사한 제어를 제공합니다.

OpenFGA 실전 모델링 — 문서 공유 시스템

OpenFGA는 Auth0/Okta가 시작해 CNCF에 기증한 Zanzibar 구현체입니다. DSL이 직관적이어서 ReBAC 입문에 가장 좋습니다. Google Drive를 닮은 문서 공유 시스템을 모델링해 보겠습니다.

model
  schema 1.1

type user

type group
  relations
    define member: [user]

type folder
  relations
    define owner: [user]
    define parent: [folder]
    define editor: [user, group#member] or owner or editor from parent
    define viewer: [user, group#member] or editor or viewer from parent

type doc
  relations
    define parent: [folder]
    define owner: [user]
    define editor: [user, group#member] or owner or editor from parent
    define viewer: [user, group#member] or editor or viewer from parent
    define can_share: owner or editor
    define can_delete: owner

이 모델 하나로 표현되는 것들:

  • 사용자/그룹 단위 공유 (group#member 타입 제약)
  • owner ⊃ editor ⊃ viewer 권한 계층 (computed relation)
  • 폴더 권한의 하위 상속 (편의상 "editor from parent" — tuple-to-userset)
  • 행위 수준 권한 (can_share, can_delete)

튜플을 써 넣고 질의해 봅니다.

# 튜플 쓰기: finance 그룹 멤버에게 q1 폴더 viewer 부여
fga tuple write --store-id "$FGA_STORE_ID" \
  "group:finance#member" viewer "folder:q1"

# jane을 finance 그룹에 추가
fga tuple write --store-id "$FGA_STORE_ID" \
  "user:jane" member "group:finance"

# budget-2026 문서를 q1 폴더에 배치
fga tuple write --store-id "$FGA_STORE_ID" \
  "folder:q1" parent "doc:budget-2026"

# 질의: jane은 budget-2026을 볼 수 있는가?
fga query check --store-id "$FGA_STORE_ID" \
  "user:jane" viewer "doc:budget-2026"
# → allowed: true  (group → folder → doc 관계 연쇄로 파생)

애플리케이션 코드에서는 SDK로 체크합니다.

import { OpenFgaClient } from '@openfga/sdk';

const fga = new OpenFgaClient({
  apiUrl: process.env.FGA_API_URL,
  storeId: process.env.FGA_STORE_ID,
});

// 단건 체크 (PEP에서 호출)
const { allowed } = await fga.check({
  user: 'user:jane',
  relation: 'viewer',
  object: 'doc:budget-2026',
});

// 역질의: jane이 볼 수 있는 문서 목록 (UI 필터링용)
const { objects } = await fga.listObjects({
  user: 'user:jane',
  relation: 'viewer',
  type: 'doc',
});

ABAC이 어려워하던 reverse query(ListObjects)가 1급 API라는 점에 주목하십시오. "내가 접근 가능한 리소스 목록" 화면을 만들 때 결정적인 차이를 만듭니다.

생태계 — SpiceDB, Ory Keto, 그리고 OPA와의 역할 구분

Zanzibar 계열 구현체는 OpenFGA만이 아닙니다.

프로젝트특징스키마 언어
OpenFGACNCF, Auth0 출신, 가장 친절한 DSL과 문서FGA DSL / JSON
SpiceDB (AuthZed)Zanzibar 논문에 가장 충실, caveat(조건부 관계) 지원SpiceDB schema
Ory KetoOry 생태계(Kratos/Hydra)와 통합Ory Permission Language
Permify멀티테넌시 강조Permify schema

그렇다면 OPA/Rego는 어디에 들어갈까요? OPA는 정책 평가 엔진이고, Zanzibar 계열은 관계 데이터 저장소 겸 그래프 평가 엔진입니다. 거칠게 나누면:

  • OPA가 잘하는 것: 입력으로 주어진 컨텍스트에 대한 규칙 평가. "이 K8s 매니페스트가 보안 기준을 만족하는가", "이 요청의 JWT scope가 이 API에 충분한가" 같은 stateless 판단. 정책이 코드로 버전 관리되는 policy-as-code.
  • ReBAC 엔진이 잘하는 것: "jane → 그룹 → 폴더 → 문서"처럼 저장된 관계 데이터를 그래프로 탐색해야 하는 판단. 수억 건의 관계 위에서의 check/list 질의.
요청 → API Gateway/서비스 (PEP)
         ├─ 굵은 판단: JWT scope, 테넌트 검증  → OPA (stateless 정책)
         └─ 세밀한 판단: 이 사용자가 이 문서를? → OpenFGA (관계 그래프)

둘은 경쟁자가 아니라 레이어가 다른 도구이며, 함께 쓰는 조합이 흔합니다. Rego 예시로 감을 잡아 봅니다.

# OPA Rego — coarse-grained API 정책 (개념 예시)
package httpapi.authz

default allow := false

allow if {
    input.method == "GET"
    startswith(input.path, "/api/public/")
}

allow if {
    input.method == "POST"
    input.path == "/api/docs"
    "docs:write" in input.token.scopes
    input.token.tenant == input.headers["x-tenant-id"]
}

아키텍처 패턴 — 중앙집중 PDP vs 라이브러리 임베딩

인가 엔진을 배치하는 방식은 크게 두 갈래입니다.

패턴 A: 중앙집중 PDP                  패턴 B: 사이드카/라이브러리 임베딩
─────────────────────                ─────────────────────────────
svc-a ──┐                            svc-a ── [PDP sidecar/lib]
svc-b ──┼──> AuthZ Service (PDP)     svc-b ── [PDP sidecar/lib]
svc-c ──┘    + 단일 관계 DB          svc-c ── [PDP sidecar/lib]
                                          (정책/데이터 복제 배포)
장점: 단일 진실 공급원,              장점: 지연 최소(로컬 평가),
      감사 용이, 모델 일관성               네트워크 의존 없음
단점: 네트워크 홉 추가,              단점: 데이터 동기화 복잡,
      가용성이 전체에 영향                 일관성 보장 어려움

실무 가이드라인은 다음과 같습니다.

  • 관계 데이터 기반 판단(ReBAC)은 중앙집중이 기본입니다. 관계 그래프를 모든 서비스에 복제하는 것은 일관성 악몽입니다. 지연은 같은 가용 영역 배치 + 캐싱으로 1~5ms 수준까지 줄일 수 있습니다.
  • stateless 정책(OPA)은 임베딩이 자연스럽습니다. OPA는 사이드카/라이브러리로 각 서비스 옆에 두고, 정책 번들을 중앙(PAP)에서 배포하는 모델이 표준입니다.
  • 중앙 PDP의 가용성은 인증 IdP와 동급으로 취급해야 합니다. 다중화, 헬스체크 기반 페일오버, 그리고 "PDP 장애 시 fail-closed"가 원칙입니다(fail-open은 인가 우회입니다).

Keycloak Authorization Services와 외부 엔진의 결합

Keycloak에도 자체 인가 기능(Authorization Services)이 있습니다. UMA 2.0 기반으로 resource/scope/policy/permission을 정의하고, 토큰에 권한(RPT)을 실어 보내는 방식입니다. 그렇다면 Keycloak만으로 충분할까요?

현실적인 역할 분담은 이렇습니다.

레이어담당도구
신원/역할 발급사용자가 누구이고 어떤 굵은 역할을 갖는지 (토큰 클레임)Keycloak
API 수준 정책이 토큰으로 이 엔드포인트 호출 가능 여부Keycloak AuthZ 또는 OPA
리소스 수준 관계이 사용자가 이 문서/프로젝트/티켓에 무엇을OpenFGA/SpiceDB

전형적인 결합 패턴: Keycloak이 발급한 토큰의 sub 클레임을 OpenFGA의 user 식별자로 사용하고, 역할/그룹 클레임의 변경을 이벤트로 받아 FGA 튜플에 동기화합니다.

로그인 → Keycloak → access_token (sub: user:jane, groups: [finance])
요청 → 서비스(PEP) ───────┤
                          ├─ 토큰 검증 (서명, aud, exp)  ← Keycloak 공개키
                          └─ fga.check(user:jane, viewer, doc:X) ← OpenFGA

Keycloak의 그룹 멤버십을 FGA 튜플로 동기화하는 이벤트 리스너 개념 예시입니다.

// Keycloak SPI — 그룹 멤버십 변경을 OpenFGA에 반영하는 이벤트 리스너 (개념 코드)
public class FgaSyncEventListener implements EventListenerProvider {

    @Override
    public void onEvent(AdminEvent event, boolean includeRepresentation) {
        if (event.getResourceType() == ResourceType.GROUP_MEMBERSHIP) {
            String userId = extractUserId(event.getResourcePath());
            String groupId = extractGroupId(event.getResourcePath());

            if (event.getOperationType() == OperationType.CREATE) {
                fgaClient.writeTuple("user:" + userId, "member", "group:" + groupId);
            } else if (event.getOperationType() == OperationType.DELETE) {
                fgaClient.deleteTuple("user:" + userId, "member", "group:" + groupId);
            }
        }
    }
}

마이크로서비스에서 인가 데이터 동기화

ReBAC 엔진을 도입하면 새로운 운영 과제가 생깁니다. 애플리케이션 DB의 사실(소유권, 멤버십, 폴더 구조)과 FGA의 튜플을 어떻게 일치시키는가입니다.

전략은 세 가지입니다.

  1. 동기 이중 쓰기(dual write): 리소스 생성 트랜잭션에서 FGA 쓰기를 함께 수행. 단순하지만 부분 실패(DB는 성공, FGA는 실패) 시 권한 구멍이 생깁니다. 보상 트랜잭션 또는 재시도 큐가 필수입니다.
  2. Transactional Outbox + CDC: 권장 패턴. 리소스 변경과 outbox 레코드를 한 트랜잭션으로 묶고, Debezium 류의 CDC가 outbox를 읽어 FGA에 반영합니다. at-least-once 전달이므로 튜플 쓰기는 멱등하게 설계합니다.
  3. 주기적 재조정(reconciliation): 위 두 전략과 병행하여, 배치로 애플리케이션 DB와 FGA 튜플의 차이를 비교/복구합니다. 인가 데이터의 drift는 곧 보안 취약점이므로 재조정은 선택이 아니라 필수입니다.
서비스 트랜잭션
┌──────────────────────────────┐
│ INSERT INTO documents (...)  │
│ INSERT INTO outbox (         │      CDC (Debezium)        OpenFGA
│   event: doc.created,        │ ───────────────────────> tuple write
│   owner: user:jane )         │      (at-least-once)     (멱등 처리)
└──────────────────────────────┘
              nightly reconciliation ───┘ (drift 탐지/복구)

삭제는 더 까다롭습니다. 리소스가 삭제되면 관련 튜플을 모두 지워야 하며(고아 튜플은 감사 노이즈 + 잠재적 오판), 폴더처럼 계층이 있는 객체는 하위 객체의 튜플 정리 순서까지 설계해야 합니다.

선택 가이드 — 무엇을 언제 쓰는가

상황권장 모델
사내 admin 도구, 역할이 5개 이하로 안정적RBAC (IdP 역할 클레임으로 충분)
테넌트/부서/시간 등 속성 조건이 핵심ABAC (OPA policy-as-code)
문서/폴더/프로젝트 공유, 계층 상속, 협업 기능ReBAC (OpenFGA/SpiceDB)
K8s admission, 인프라 정책, CI 게이트OPA/Rego
규제 산업 + 접근 결정의 중앙 감사 필요중앙 PDP + 결정 로그 파이프라인
위 요구가 섞여 있음 (대부분의 현실)RBAC(굵게) + ReBAC(세밀하게) + OPA(정책) 조합

판단 기준을 질문 형태로 정리하면:

  1. "X를 볼 수 있는 사람 목록" 또는 "내가 볼 수 있는 X 목록"이 필요한가? → 필요하면 ReBAC. ABAC으로는 고통받습니다.
  2. 권한이 리소스 간 관계(폴더→문서, 조직→프로젝트)에서 파생되는가? → ReBAC.
  3. 권한이 요청 컨텍스트(시간, IP, 디바이스 상태)에 의존하는가? → ABAC/OPA. 단, SpiceDB caveat이나 OpenFGA conditions로 ReBAC에 조건을 섞는 것도 가능합니다.
  4. 조직 전체에서 권한 모델을 통일할 준비가 됐는가? → 안 됐다면 한 도메인(예: 문서 서비스)부터 ReBAC을 도입하고 점진 확장하십시오.

안티패턴 모음

  1. JWT에 세밀한 권한을 다 싣기: 토큰에 문서 ID 목록을 넣으면 토큰이 비대해지고, 권한 회수가 토큰 만료까지 지연됩니다. 토큰에는 신원과 굵은 클레임만, 세밀한 판단은 PDP 실시간 질의로.
  2. fail-open PDP: 인가 서비스 장애 시 "일단 허용"은 인가 우회 백도어입니다. fail-closed + 캐시된 결정의 짧은 TTL 허용이 정석입니다.
  3. 프론트엔드 인가를 신뢰: UI에서 버튼을 숨기는 것은 UX이지 보안이 아닙니다. 모든 집행은 서버 측 PEP에서.
  4. 관계 모델 없이 튜플부터 쌓기: 모델(스키마) 리뷰 없이 튜플을 쌓으면 나중에 마이그레이션 지옥이 옵니다. 모델 변경은 코드 리뷰 + 테스트(FGA의 model test)를 거치십시오.
  5. 결정 로그 미수집: "왜 허용됐는가"를 재구성할 수 없으면 감사 대응이 불가능합니다. check 요청/응답을 구조화 로그로 남기십시오.
  6. 이중 쓰기 후 재조정 생략: drift는 반드시 발생합니다. 탐지 없는 drift는 침묵하는 권한 버그입니다.

마치며

인가 모델의 진화는 "표현력"과 "운영 가능성" 사이의 긴장 속에서 진행되어 왔습니다. RBAC은 단순했지만 role explosion으로 무너졌고, ABAC(XACML)은 표현력을 얻었지만 개발자 경험을 잃었으며, Zanzibar/ReBAC은 관계라는 자연스러운 모델과 reverse query, 그리고 zookie라는 일관성 장치로 현재의 균형점을 제시했습니다.

실무 결론은 의외로 보수적입니다. 굵은 권한은 IdP(Keycloak)의 역할로, 정책적 판단은 OPA로, 리소스 수준 관계는 OpenFGA로 — 각 레이어에 맞는 도구를 조합하되, 모든 결정을 감사 가능하게 남기는 것. 그것이 2026년 인가 아키텍처의 모범 답안입니다.

다음 글에서는 이 인가 인프라 위에 올라탈 새로운 주체 — AI 에이전트의 아이덴티티와 MCP 인증을 다룹니다.

참고 자료