
RBAC vs ABAC: 세밀한 권한 관리 설계
admin/user 두 역할로 시작했는데, 요구사항이 복잡해지면서 RBAC만으로 부족해졌다. ABAC까지 고려한 권한 설계를 정리했다.

admin/user 두 역할로 시작했는데, 요구사항이 복잡해지면서 RBAC만으로 부족해졌다. ABAC까지 고려한 권한 설계를 정리했다.
프론트엔드 개발자가 알아야 할 4가지 저장소의 차이점과 보안 이슈(XSS, CSRF), 그리고 언제 무엇을 써야 하는지에 대한 명확한 기준.

왜 CPU는 빠른데 컴퓨터는 느릴까? 80년 전 고안된 폰 노이만 구조의 혁명적인 아이디어와, 그것이 남긴 치명적인 병목현상에 대해 정리했습니다.

Debug에선 잘 되는데 Release에서만 죽나요? 범인은 '난독화'입니다. R8의 원리, Mapping 파일 분석, 그리고 Reflection을 사용하는 라이브러리를 지켜내는 방법(@Keep)을 정리해봤습니다.

ChatGPT는 질문에 답하지만, AI Agent는 스스로 계획하고 도구를 사용해 작업을 완료한다. 이 차이가 왜 중요한지 정리했다.

처음엔 간단했다. admin과 user 두 역할만 있으면 충분할 줄 알았다. admin은 모든 권한, user는 읽기만. 깔끔하고 명확했다.
그런데 실제 요구사항이 들어왔다. "사용자는 자기가 쓴 글만 수정할 수 있어야 해요." 순간 멈칫했다. user 역할에 '글 수정' 권한을 주면 모든 사용자 글을 수정할 수 있고, 안 주면 자기 글도 못 수정한다.
역할만으로는 "누가 누구의 리소스에 접근하는가"를 표현할 수 없었다. 호텔 키카드에 비유하면, "3층 모든 방 출입 가능" 같은 역할 기반 권한은 쉽지만, "내가 예약한 방만 출입 가능"이라는 조건은 키카드만으로 표현이 안 된다.
이때 처음으로 RBAC(Role-Based Access Control)과 ABAC(Attribute-Based Access Control)의 차이가 와닿았다.
먼저 헷갈리는 개념부터 정리했다.
RBAC과 ABAC은 둘 다 Authorization의 방법론이다.
호텔 키카드 메타포가 정확하다. 프론트 데스크 직원은 마스터키(admin), 청소 직원은 전체 층 접근키(manager), 손님은 자기 방만(guest).
// RBAC 구조
interface User {
id: string;
name: string;
roles: Role[]; // 한 사용자가 여러 역할을 가질 수 있음
}
interface Role {
id: string;
name: string; // 'admin', 'manager', 'user'
permissions: Permission[];
}
interface Permission {
id: string;
resource: string; // 'posts', 'users', 'comments'
action: string; // 'create', 'read', 'update', 'delete'
}
역할에 권한을 묶어서 관리한다. 새로운 manager가 입사하면 'manager' 역할만 부여하면 끝. 권한을 일일이 설정할 필요 없다.
역할 계층(Role Hierarchy)도 구현할 수 있다. admin > manager > user 순서로, 상위 역할은 하위 역할의 모든 권한을 상속받는다.
// RBAC 권한 체크 예시
function canUpdatePost(user: User, post: Post): boolean {
return user.roles.some(role =>
role.permissions.some(perm =>
perm.resource === 'posts' && perm.action === 'update'
)
);
}
// 문제: 모든 post에 대한 update 권한을 확인할 뿐
// "내가 쓴 글인지"는 체크하지 못함
여기서 한계가 드러났다. RBAC은 "누가 무엇을 할 수 있는가"만 표현한다. "누가 누구의 무엇을 언제 어디서 할 수 있는가" 같은 조건부 권한은 표현하기 어렵다.
ABAC은 역할 대신 속성(Attributes)으로 권한을 결정한다. 사용자 속성, 리소스 속성, 환경 속성을 모두 고려한다.
비유하자면, 은행 금고실 출입이다. "임원 역할"만으로 들어가는 게 아니라, "임원이면서 + 업무 시간이고 + 본인 지점이고 + 2단계 인증 완료"라는 여러 속성이 모두 만족되어야 들어갈 수 있다.
// ABAC 정책 예시
interface Policy {
id: string;
name: string;
effect: 'allow' | 'deny';
conditions: Condition[];
}
interface Condition {
attribute: string; // 'user.id', 'resource.ownerId', 'environment.time'
operator: string; // 'equals', 'greaterThan', 'in'
value: any;
}
// "사용자는 자기가 쓴 글만 수정 가능" 정책
const editOwnPostPolicy: Policy = {
id: 'edit-own-post',
name: 'Edit Own Post',
effect: 'allow',
conditions: [
{ attribute: 'user.id', operator: 'equals', value: 'resource.authorId' },
{ attribute: 'resource.type', operator: 'equals', value: 'post' },
{ attribute: 'action', operator: 'equals', value: 'update' }
]
};
이제 "본인 글만 수정 가능"이 표현된다. user.id와 resource.authorId가 일치하는지 확인하면 된다.
-- users 테이블
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- roles 테이블
CREATE TABLE roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(50) UNIQUE NOT NULL, -- 'admin', 'manager', 'user'
description TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
-- permissions 테이블
CREATE TABLE permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
resource VARCHAR(50) NOT NULL, -- 'posts', 'users'
action VARCHAR(50) NOT NULL, -- 'create', 'read', 'update', 'delete'
description TEXT,
UNIQUE(resource, action)
);
-- role_permissions: 역할에 권한 할당
CREATE TABLE role_permissions (
role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
permission_id UUID REFERENCES permissions(id) ON DELETE CASCADE,
PRIMARY KEY (role_id, permission_id)
);
-- user_roles: 사용자에게 역할 할당
CREATE TABLE user_roles (
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
PRIMARY KEY (user_id, role_id)
);
이 구조면 역할과 권한을 유연하게 관리할 수 있다. 새로운 권한이 추가되면 permissions 테이블에 넣고, 필요한 역할에 연결하면 된다.
Supabase의 Row Level Security(RLS)는 ABAC의 실제 구현이다. 각 테이블에 정책을 설정해서, 행 단위로 접근을 제어한다.
-- posts 테이블
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
author_id UUID REFERENCES users(id) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- RLS 활성화
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- 정책 1: 모든 사용자는 모든 글을 읽을 수 있음
CREATE POLICY "Anyone can read posts"
ON posts FOR SELECT
TO authenticated, anon
USING (true);
-- 정책 2: 사용자는 자기 글만 수정 가능
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
TO authenticated
USING (auth.uid() = author_id)
WITH CHECK (auth.uid() = author_id);
-- 정책 3: 사용자는 자기 글만 삭제 가능
CREATE POLICY "Users can delete own posts"
ON posts FOR DELETE
TO authenticated
USING (auth.uid() = author_id);
-- 정책 4: admin은 모든 글을 수정/삭제 가능
CREATE POLICY "Admins can do anything"
ON posts FOR ALL
TO authenticated
USING (
EXISTS (
SELECT 1 FROM user_roles ur
JOIN roles r ON ur.role_id = r.id
WHERE ur.user_id = auth.uid() AND r.name = 'admin'
)
);
auth.uid() = author_id 조건이 핵심이다. 현재 로그인한 사용자 ID와 글 작성자 ID를 비교한다. 이게 ABAC의 속성 기반 판단이다.
백엔드에서 권한을 체크하는 것도 중요하지만, 프론트엔드에서 UI를 조건부로 보여주는 것도 필요하다. CASL.js가 그 역할을 한다.
import { createMongoAbility, AbilityBuilder } from '@casl/ability';
// 사용자 권한 정의
function defineAbilitiesFor(user: User) {
const { can, cannot, build } = new AbilityBuilder(createMongoAbility);
if (user.roles.includes('admin')) {
can('manage', 'all'); // admin은 모든 것을 할 수 있음
} else {
can('read', 'Post');
can('create', 'Post');
can('update', 'Post', { authorId: user.id }); // 자기 글만
can('delete', 'Post', { authorId: user.id }); // 자기 글만
}
return build();
}
// React 컴포넌트에서 사용
function PostActions({ post, user }) {
const ability = defineAbilitiesFor(user);
return (
<div>
{ability.can('update', 'Post', { authorId: post.authorId }) && (
<button onClick={handleEdit}>수정</button>
)}
{ability.can('delete', 'Post', { authorId: post.authorId }) && (
<button onClick={handleDelete}>삭제</button>
)}
</div>
);
}
{ authorId: post.authorId } 조건을 넘겨서 ABAC 스타일로 권한을 체크한다. 프론트엔드에서도 "내가 쓴 글인지" 판단할 수 있다.
현실적으로는 둘을 섞어 쓴다. 넓은 범위의 권한은 RBAC으로, 세밀한 조건은 ABAC으로.
// 하이브리드 권한 체크
async function canAccessResource(
user: User,
resource: Resource,
action: string
): Promise<boolean> {
// 1단계: RBAC 체크 (역할 기반 넓은 권한)
const hasRolePermission = await checkRolePermission(user, resource.type, action);
if (hasRolePermission && user.roles.includes('admin')) {
return true; // admin은 무조건 통과
}
// 2단계: ABAC 체크 (속성 기반 세밀한 조건)
const policies = await getPoliciesForAction(resource.type, action);
for (const policy of policies) {
if (evaluatePolicy(policy, user, resource)) {
return policy.effect === 'allow';
}
}
return false; // 기본값은 거부
}
결국 실제로는 "역할로 큰 틀을 잡고, 정책으로 예외를 처리한다"는 흐름이 와닿았다.
RBAC은 권한 관리의 기본이다. 역할을 정의하고 권한을 할당하면 관리가 쉽다. 하지만 "본인 리소스만", "특정 조건에서만" 같은 세밀한 요구사항은 표현하기 어렵다.
ABAC은 사용자, 리소스, 환경의 속성을 기반으로 권한을 판단한다. 유연하지만 정책 관리가 복잡해질 수 있다.
실제로는 RBAC으로 넓은 권한을 정의하고, ABAC으로 예외와 조건을 처리하는 하이브리드 접근이 효과적이다. Supabase RLS로 데이터베이스 레벨 보안을 강화하고, CASL.js로 프론트엔드 UI를 조건부로 제어하면 견고한 권한 시스템을 만들 수 있다.
"admin/user만 있으면 되지 않을까?"라는 초기 생각은 첫 번째 복잡한 요구사항에서 무너졌다. 하지만 그 덕분에 권한 설계의 깊이를 이해하게 되었다.