NestJS와 TypeScript로 구현하는 효율적인 RBAC 시스템
RBAC(Role-Based Access Control)는 역할 기반 액세스 제어로,
회사의 IT 시스템 내에서 사용자가 수행할 수 있는 작업을 역할 단위로 제어하는 방식입니다.
각 사용자는 하나 이상의 역할(Role)을 부여받고,
각 역할에는 특정 리소스(Resource)에 대해 수행 가능한 권한(Permission)이 포함됩니다.
1. 주요 구성 요소
RBAC는 다음 네 가지 핵심 요소로 구성됩니다.
- 사용자(User): 시스템을 사용하는 주체
- 역할(Role): 권한을 묶어 관리하는 단위
- 권한(Permission): 특정 리소스에 대해 허용되는 액션
- 리소스(Resource): 보호되어야 할 대상
사용자는 직접 권한을 부여받지 않고,
역할을 통해 간접적으로 권한을 획득합니다.
2. RBAC 변형 모델
기본 RBAC 모델을 확장해 다양한 요구사항을 수용할 수 있습니다.
- RBAC0 (기본 모델)
사용자–역할–권한의 단순 매핑 구조 - RBAC1 (계층적 RBAC)
역할 간 상속 구조를 도입
예) 최고 관리자는 관리자의 모든 권한을 포함 - RBAC2 (제약 기반 RBAC)
역할 할당 시 추가 조건을 요구
예) 직급이 ‘사원’일 때만 특정 역할 할당 가능 - RBAC3
RBAC1 + RBAC2 결합 모델
3. 장단점
장점
- 단순성: 개념이 직관적이고 구현이 비교적 쉬움
- 확장성: 사용자 수 증가 시에도 역할 재사용으로 관리 용이
- 가시성: 역할–권한 관계가 명확해 감사 및 검토에 유리
단점
- 경직성: 복잡한 조건 기반 정책에는 한계
- 세분화 비용: 세밀한 권한 제어를 위해 설계 부담 증가
- 역할 폭증 문제: 조직이 커질수록 역할 관리 난이도 상승
4. 구현 방향
RBAC 개념을 바탕으로, 실제 서비스에서는 다음과 같은 방식으로 구현했습니다.
- 언어 / 프레임워크: TypeScript + NestJS
- RBAC 적용 단위: API (Controller) 기준
NestJS는 Guard와 Decorator를 활용한 기본적인 RBAC 패턴을 공식 문서에서 안내하고 있습니다.
참고:
https://docs.nestjs.com/security/authorization#basic-rbac-implementation
4.1 API 단위로 RBAC를 구현한 이유
제가 선택한 방식은 리소스를 API 단위로 간주하는 RBAC 설계입니다.
- 사용자는 역할(Role)을 부여받음
- 각 API(Controller 혹은 Handler)는 접근 가능한 역할을 명시
- 요청 시 사용자의 역할과 API에 정의된 역할을 비교하여 접근 여부 판단
여기서 흔히 나오는 질문이 하나 있습니다.
“권한은 ‘리소스 + 액션’인데,
권한을 명시적으로 정의하지 않아도 괜찮은가?”
이 부분은 API 설계 방식에서 자연스럽게 해소되었습니다.
4.2 권한을 API 설계로 흡수하기
대부분의 REST API는 다음과 같이 CRUD 단위로 분리되어 설계됩니다.
- 조회 (GET)
- 생성 (POST)
- 수정 (PUT / PATCH)
- 삭제 (DELETE)
즉,
GET /usersPOST /usersDELETE /users/:id
각 API 자체가 이미 명확한 액션을 내포하고 있습니다.
이 구조에서는:
- API = 리소스 + 액션
- 별도의 Permission 엔티티 없이도 권한 표현이 가능
따라서,
“이 API에 접근 가능한 역할은 무엇인가?”
만 정의해주면 RBAC 요구사항을 충분히 만족할 수 있었습니다.
4.3 NestJS에서의 실제 구현 구조
구현은 다음과 같은 구성으로 이루어졌습니다.
1) Role 데코레이터
export const Roles = (...roles: RoleType[]) => SetMetadata('roles', roles);
2) RolesGuard
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<RoleType[]>(
'roles',
context.getHandler(),
);
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest();
return requiredRoles.includes(user.role);
}
}
3) Controller 적용
@Roles('ADMIN')
@UseGuards(RolesGuard)
@Post('/users')
createUser() {
...
}
이 방식으로 API 단위 RBAC를 일관되게 적용했습니다.
4.4 이 방식의 한계
이 접근 방식은 단순하고 실용적이지만, 위에서 언급한 단점의 한계도 분명합니다.
- 조건 기반 권한 제어가 어려움
예) “본인이 생성한 리소스만 수정 가능” - 컨텍스트 기반 권한(ABAC) 표현 불가
예) 시간, 조직, 상태 값에 따른 접근 제어 - 역할이 세분화될수록 API에 역할 정의가 복잡해짐
마무리
RBAC는 단순하지만 강력한 접근 제어 모델이고
API 중심의 서버에서는 API 단위 RBAC만으로도 상당 부분의 요구사항을 충족할 수 있습니다.