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. 장단점

장점

  1. 단순성: 개념이 직관적이고 구현이 비교적 쉬움
  2. 확장성: 사용자 수 증가 시에도 역할 재사용으로 관리 용이
  3. 가시성: 역할–권한 관계가 명확해 감사 및 검토에 유리

단점

  1. 경직성: 복잡한 조건 기반 정책에는 한계
  2. 세분화 비용: 세밀한 권한 제어를 위해 설계 부담 증가
  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 /users
  • POST /users
  • DELETE /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만으로도 상당 부분의 요구사항을 충족할 수 있습니다.