롤만으로는 부족할 때: ReBAC와 SpiceDB

롤만으로는 부족할 때: ReBAC와 SpiceDB

1. 들어가며: 롤만으로 감당이 안 되는 순간

데브라임에선 권한 처리는 이렇게 시작합니다.

@AuthorizedRole(roles = { ScrumRoles.ROLE_PROJECT_MANAGER,
                          ScrumRoles.ROLE_PROJECT_MEMBER })
public class FindProductProjFetch extends FetchRequest<ProductRdo> {

"프로젝트 관리자와 프로젝트 멤버만 이 API를 호출할 수 있다." 명확하고, 읽기 쉽고, 대부분의 경우 이걸로 충분합니다. 데브라임 전반에서 이 방식을 쓰고 있습니다.

그런데 어느 순간 이런 요구가 들어옵니다. "스크럼 마스터는 자기가 맡은 프로덕트의 에픽만 수정할 수 있어야 한다"

여기서 문제가 생깁니다. ROLE_SCRUM_MASTER라는 롤을 만들면 "스크럼 마스터인가?"는 답할 수 있습니다. 하지만 "어느 프로덕트의" 스크럼 마스터인지는 롤이 담지 못합니다. 롤은 사람에게 붙는 라벨이고, 우리가 알고 싶은 건 사람과 특정 자원 사이의 관계이기 때문입니다.

물론 억지로 풀 수는 있습니다. SCRUM_MASTER_PRODUCT_A, SCRUM_MASTER_PRODUCT_B 같은 롤을 프로덕트마다 만들거나, 코드 안에서 매번 이렇게 확인하는 방법이 있죠.

ScrumTeam team = scrumTeamLogic.findByProductId(productId);
if (!team.getScrumMaster().getCitizenId().equals(currentCitizenId)) {
    throw new SecurityException("권한이 없습니다");
}

전자는 프로덕트가 늘어날 때마다 롤이 늘어나서 관리가 불가능해지고, 후자는 권한 판단 로직이 서비스 코드 곳곳에 흩어집니다. 에픽에도, 릴리스에도, 백로그에도 비슷한 코드가 복사되기 시작하면 "이 자원은 누가 수정할 수 있는가"를 한눈에 파악할 수 없게 됩니다.

이 글은 이 문제를 풀기 위해 저희가 도입한 ReBAC과 SpiceDB를 소개하고, 데브라임 스크럼에서 실제로 어떻게 쓰고 있는지 정리한 내용입니다.

2. RBAC · ABAC · ReBAC: 세 가지 접근

권한 모델은 크게 세 가지로 나뉩니다. 이름은 비슷하지만 무엇을 근거로 판단하는가가 다릅니다.

RBAC    사람 ──▶ 역할 ──▶ 권한

ABAC    (사람 속성 + 자원 속성 + 환경) ──▶ 규칙 평가 ──▶ 허용 / 거부

ReBAC   사람 ──관계──▶ 자원 ──관계──▶ 자원 ──▶ 허용 / 거부

RBAC (Role-Based Access Control) — 역할로 판단

사용자에게 역할을 부여하고, 역할에 권한을 묶습니다. 가장 오래되고 가장 널리 쓰이는 방식입니다. 예를 들어 "홍길동 → project.manager → 프로젝트 설정 변경 가능" 같은 식입니다.

장점은 단순함입니다. 조직도가 곧 권한 구조가 되기 때문에 비개발자에게 설명하기도 쉽습니다. 사용자 수가 아무리 늘어도 역할 개수는 그대로라 관리 비용이 일정합니다.

한계는 자원별 구분입니다. "관리자인가?"에는 답할 수 있지만 "이 문서의 관리자인가?"에는 답하지 못합니다. 이걸 억지로 표현하려면 역할 개수가 자원 개수만큼 늘어나는데, 이를 롤 폭발(role explosion)이라고 부릅니다.

ABAC (Attribute-Based Access Control) — 속성으로 판단

사용자·자원·환경의 속성을 조합해 규칙으로 판단합니다. "사용자의 부서 == 문서의 부서 AND 현재 시각이 업무 시간 → 허용" 같은 규칙입니다.

장점은 표현력입니다. 세밀한 조건을 그대로 규칙으로 옮길 수 있습니다.

한계는 예측 가능성입니다. 규칙이 쌓이면 "왜 이 사람이 접근할 수 있었지?"를 추적하기 어려워집니다. 규칙끼리 충돌할 수도 있고, 판단 시점의 속성값에 따라 결과가 달라지기 때문에 재현도 까다롭습니다.

ReBAC (Relationship-Based Access Control) — 관계로 판단

사용자와 자원 사이의 관계를 저장하고, 그 관계를 따라가며 판단합니다.

홍길동 ──editor──▶ 프로덕트 A
에픽 #12 ──parent──▶ 프로덕트 A
  ⇒ 홍길동은 에픽 #12를 수정할 수 있다

장점은 자원 단위 권한과 상속입니다. "이 사람이 이 자원과 어떤 관계인가"를 직접 저장하므로 롤 폭발이 없고, 자원 사이에 부모-자식 관계를 걸어두면 권한이 자연스럽게 내려갑니다. 구글 드라이브에서 폴더를 공유하면 그 안의 파일이 전부 공유되는 것과 같은 원리입니다.

한계는 인프라 부담입니다. 관계를 저장하고 그래프를 탐색할 별도의 저장소가 필요하고, 관계 데이터가 원본 도메인과 어긋나지 않게 유지해야 합니다.

셋 중 하나를 고르는 게 아닙니다

셋은 배타적이지 않습니다. 실무에서는 섞어 쓰는 게 자연스럽습니다. 저희도 그렇게 하고 있고, 뒤에서 그 조합을 보여드리겠습니다.

구분

판단 근거

잘 맞는 곳

약한 곳

RBAC

역할

메뉴·기능 단위 접근

자원별 구분

ABAC

속성

조건이 복잡한 정책

추적·재현

ReBAC

관계

자원 단위 권한, 상속

인프라·데이터 정합성

3. SpiceDB: 관계를 저장하고 물어보는 데이터베이스

ReBAC을 직접 구현하려면 관계 테이블을 만들고 재귀 쿼리로 그래프를 타야 합니다. 관계가 몇 단계만 깊어져도 쿼리가 감당이 안 됩니다.

SpiceDB는 이 문제를 대신 풀어주는 오픈소스 권한 데이터베이스입니다. 구글이 2019년에 공개한 논문 Zanzibar를 구현한 제품인데, Zanzibar는 구글 드라이브·유튜브·캘린더의 권한을 실제로 처리하는 시스템입니다. 초당 수백만 건의 권한 질의를 밀리초 단위로 응답하는 게 목표였고, SpiceDB는 그 설계를 따릅니다.

스키마와 관계

SpiceDB에는 두 가지가 들어갑니다. 먼저 스키마 — 어떤 관계가 존재하고, 그 관계로부터 어떤 권한이 파생되는지 정의합니다.

definition resource {
    relation editor: actor | group#member
    relation viewer: citizen
    relation parent: resource

    permission edit = editor + parent->edit
    permission view = viewer
}

읽는 법은 이렇습니다. editor 관계는 개인(actor) 또는 그룹의 멤버로 맺을 수 있습니다. edit 권한은 editor이거나 부모 자원에서 edit 권한을 가진 경우 성립하고(parent->edit), view 권한은 viewer 관계가 있을 때 성립합니다.

여기서 parent->edit 한 줄이 핵심입니다. 이 한 줄로 "부모 자원에서 수정할 수 있으면 자식 자원도 수정할 수 있다"는 상속이 표현됩니다.

그리고 관계 데이터 — 실제로 누가 무엇과 어떤 관계인지 하나씩 저장합니다. 아래 세 줄이 저장돼 있으면 "홍길동이 에픽 E12를 수정할 수 있나?"라는 질문에 SpiceDB가 그래프를 타고 답을 줍니다.

resource:scrum_product-P1   editor   group:scrum_product_sm-P1#member
group:scrum_product_sm-P1   member   actor:홍길동
resource:scrum_epic-E12     parent   resource:scrum_product-P1

  질의: "actor:홍길동이 resource:scrum_epic-E12를 edit할 수 있나?"

    epic E12 ──parent──▶ product P1 ──editor──▶ SM 그룹 ──member──▶ 홍길동
                                                                ⇒ 허용

데브라임에서의 위치

저희는 SpiceDB를 서비스에서 직접 부르지 않습니다. 플랫폼 커널의 Guard 서비스가 SpiceDB 앞에 서고, 각 서비스는 prologue 라이브러리의 GuardAuthorizer를 통해 Guard에 질의합니다.

스크럼 서비스 ──▶ GuardAuthorizer ──▶ Guard ──gRPC──▶ SpiceDB
  (도메인 코드)      (prologue)       (커널)        (권한 저장소)

이렇게 한 덕분에 스크럼 코드에는 SpiceDB 관련 의존성이 전혀 없습니다. 스키마 관리와 접속 정보도 Guard 한 곳에 모입니다.

4. 데브라임에서는 어떻게 쓰고 있나

스크럼 마스터: 그룹을 한 겹 두는 이유

프로덕트를 만들 때, 저희는 프로덕트에 사람을 바로 붙이지 않고 그룹을 먼저 붙입니다.

// Product 생성 시 (최초 1회)
public void linkScrumMasterGroup(String productId) {
    guardAuthorizer.add(Relationship.builder()
            .resource(ProductRelationType.PRODUCT, productId)
            .relation(RelationName.EDITOR)
            .subject(SchemaType.GROUP,
                     ProductGroupType.SCRUM_MASTER.id(productId), "member")
            .build());
}

"프로덕트 P1의 editor는 scrum_product_sm-P1 그룹의 멤버들이다." 그다음 스크럼 팀이 만들어지거나 스크럼 마스터가 바뀔 때는 그룹 멤버만 넣고 뺍니다.

public void addScrumMaster(String productId, String actorId) {
    guardAuthorizer.add(Relationship.builder()
            .resource(SchemaType.GROUP,
                      ProductGroupType.SCRUM_MASTER.id(productId))
            .relation(RelationName.MEMBER)
            .subject(SchemaType.ACTOR, actorId)
            .build());
}

사람을 프로덕트에 직접 붙였다면 스크럼 마스터가 교체될 때마다 프로덕트·에픽·릴리스 각각의 관계를 손봐야 합니다. 그룹을 한 겹 두면 교체가 멤버 한 줄 교체로 끝납니다.

[사람을 직접 연결한 경우]              교체 시 관계 3줄을 전부 수정

    홍길동 ──editor──▶ 프로덕트 P1
    홍길동 ──editor──▶ 에픽 E12
    홍길동 ──editor──▶ 릴리스 R3

[그룹을 한 겹 둔 경우]                 교체 시 멤버 1줄만 수정

    홍길동 ──member──▶ SM 그룹 ──editor──▶ 프로덕트 P1
                                              ▲        ▲
                                       parent │        │ parent
                                          에픽 E12   릴리스 R3

RBAC과 ReBAC을 같이 쓰기

가장 자주 나오는 패턴입니다. 에픽 등록 코드를 보시죠.

public String registerEpic(EpicCdo epicCdo) {
    if (!StageContext.get().getRoles()
                     .contains(ScrumRoles.ROLE_PROJECT_MANAGER)) {
        boolean canEdit = productAuthorizer.canEdit(
                ProductRelationType.PRODUCT, epicCdo.getProductId(),
                StageContext.get().getActorKey().getId());
        if (!canEdit) {
            throw new SecurityException(NOT_ALLOWED);
        }
    }
    ...
    parentAuthorizer.addParent(ProductRelationType.EPIC, epicId,
                               ProductRelationType.PRODUCT,
                               epicCdo.getProductId());
    return epicId;
}

프로젝트 관리자면 롤로 통과시키고, 아니면 관계를 확인합니다. 프로젝트 관리자는 프로젝트 전체를 책임지므로 자원별로 따질 필요가 없고, 그 외 사람은 "이 프로덕트와 관계가 있는가"를 물어봐야 하니까요. RBAC은 넓고 저렴한 1차 관문, ReBAC은 좁고 정확한 2차 관문인 셈입니다.

마지막 줄도 눈여겨볼 만합니다. 에픽을 만들면서 프로덕트를 부모로 걸어둡니다. 이 한 줄 덕분에 이후 에픽 수정 권한을 물어볼 때 스키마의 parent->edit이 알아서 프로덕트까지 거슬러 올라갑니다. 에픽마다 스크럼 마스터를 다시 붙일 필요가 없습니다.

마치며

ReBAC을 한 줄로 요약하면, 권한을 사람에게 붙은 라벨이 아니라 사람과 자원 사이의 관계로 저장하는 것입니다.

도입 전에는 "권한 하나 확인하려고 별도 시스템까지 둬야 하나" 싶었습니다. 그런데 자원이 계층을 이루기 시작하면 — 프로덕트 아래 에픽, 에픽 아래 릴리스 — 롤만으로는 표현이 안 되고, 서비스 코드에 권한 판단이 스며들기 시작합니다. 그 시점에 관계를 밖으로 빼내면 도메인 코드는 "무엇을 하는가"에만 집중할 수 있게 됩니다.

물론 공짜는 아닙니다. 관계 데이터가 원본과 어긋나면 권한이 조용히 잘못되고, 이건 화면 버그보다 알아채기 어렵습니다. 스크럼 마스터를 교체할 때 그룹 멤버를 빼는 걸 잊으면 이전 담당자가 계속 수정할 수 있게 되니까요. 저희가 스크럼 마스터 등록·해제를 ProductAuthorizer 한 클래스에 모아둔 것도 그래서입니다. 관계를 건드리는 코드가 흩어지지 않아야 빠뜨린 곳을 찾을 수 있습니다.

walrus

Site footer