역할 vs 권한: RBAC에서 역할 검사를 사용하면 안 되는 이유

발행: (2025년 12월 11일 오후 10:45 GMT+9)
7 min read
원문: Dev.to

Source: Dev.to

멀티‑유저 앱을 만들고 있습니다. 관리자는 제품을 삭제할 수 있고, 영업 담당자는 삭제할 수 없도록 했습니다. 코드 예시는 다음과 같습니다:

// Bad approach – checking roles directly
if (user.role === 'admin') {
  await deleteProduct(id);
} else {
  throw new ForbiddenException('Only admins can delete');
}

이 코드는 동작합니다… 하지만 클라이언트가 다음과 같이 요청합니다: “매니저 역할을 추가하고 싶어요. 매니저는 제품을 삭제할 수는 있지만 사용자를 관리할 수는 없어야 합니다.”
이제 코드는 역할을 확인하고 있기 때문에 권한을 확인하지 못해 곤란해집니다.

역할 검사의 문제점

  • 확장성 – 새로운 역할이 생길 때마다 코드베이스 전체에서 user.role === '…' 검사를 찾아야 합니다.
  • 유지보수 – 역할에 대한 권한을 바꾸려면 여러 곳을 수정해야 하며, 회귀가 발생할 위험이 있습니다.
  • 세분화 – 역할은 포괄적이지만, 권한은 세밀합니다 (예: “보고서 보기” vs. “판매 삭제”).

권한이 더 나은 이유

“당신의 역할은 무엇인가요?” 대신 “이 작업을 수행할 권한이 있나요?” 라고 물어보세요.
권한은 행동을 설명하며, 누구든 원하는 조합으로 가질 수 있습니다.

권한 정의

// constants/permissions.ts
export enum Permission {
  // Products
  VIEW_PRODUCTS = 'VIEW_PRODUCTS',
  CREATE_PRODUCT = 'CREATE_PRODUCT',
  UPDATE_PRODUCT = 'UPDATE_PRODUCT',
  DELETE_PRODUCT = 'DELETE_PRODUCT',

  // Sales
  VIEW_SALES = 'VIEW_SALES',
  CREATE_SALE = 'CREATE_SALE',
  DELETE_SALE = 'DELETE_SALE',

  // Admin
  INVITE_USERS = 'INVITE_USERS',
  VIEW_ACTIVITY_LOGS = 'VIEW_ACTIVITY_LOGS',
}

역할을 권한에 매핑

export const ROLE_PERMISSIONS: Record<string, Permission[]> = {
  admin: [
    Permission.VIEW_PRODUCTS,
    Permission.CREATE_PRODUCT,
    Permission.UPDATE_PRODUCT,
    Permission.DELETE_PRODUCT,
    Permission.VIEW_SALES,
    Permission.CREATE_SALE,
    Permission.DELETE_SALE,
    Permission.INVITE_USERS,
    Permission.VIEW_ACTIVITY_LOGS,
  ],

  sales_rep: [
    Permission.VIEW_PRODUCTS,
    Permission.CREATE_PRODUCT,
    Permission.VIEW_SALES,
    Permission.CREATE_SALE,
  ],
};

헬퍼 함수

export function hasPermission(
  role: UserRole,
  permission: Permission,
): boolean {
  return ROLE_PERMISSIONS[role]?.includes(permission) ?? false;
}

이제 역할은 권한들의 묶음일 뿐입니다. 새로운 역할을 추가하려면 맵만 수정하면 됩니다.

권한 검사로 리팩토링

이전 (역할 검사)

if (user.role === 'admin') {
  await deleteProduct(id);
}

이후 (권한 검사)

if (hasPermission(user.role, Permission.DELETE_PRODUCT)) {
  await deleteProduct(id);
}

이 로직을 가드와 데코레이터에 캡슐화하면 컨트롤러 코드를 깔끔하게 유지할 수 있습니다.

권한 가드

// common/guards/permissions.guard.ts
@Injectable()
export class PermissionsGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const required = this.reflector.get<string[]>(
      'permissions',
      context.getHandler(),
    );

    if (!required) return true;

    const { user } = context.switchToHttp().getRequest();

    return required.every(p => hasPermission(user.role, p));
  }
}

컨트롤러 예시

@Controller('products')
@UseGuards(JwtAuthGuard, PermissionsGuard)
export class ProductsController {
  @Post()
  @RequirePermission(Permission.CREATE_PRODUCT)
  create(@Body() dto: CreateProductDto) {
    return this.productsService.create(dto);
  }

  @Delete(':id')
  @RequirePermission(Permission.DELETE_PRODUCT) // Only admins have this
  remove(@Param('id') id: string) {
    return this.productsService.remove(id);
  }
}

코드가 자체 문서화됩니다: 각 엔드포인트가 필요한 권한을 명시적으로 선언합니다.

역할과 권한 관리

  • 매니저 역할 추가ROLE_PERMISSIONS에 다음을 추가하면 됩니다:

    manager: [
      Permission.VIEW_PRODUCTS,
      Permission.UPDATE_PRODUCT,
      Permission.DELETE_PRODUCT,
      Permission.VIEW_SALES,
    ],
  • 기존 역할에 새 권한 부여 – 해당 역할의 배열을 수정하면 됩니다 (예: sales_repUPDATE_PRODUCT 권한을 부여). 컨트롤러 코드를 바꿀 필요도, 코드베이스 전체를 검색할 필요도 없습니다.

권한 로직 테스트

it('should allow admin to delete product', () => {
  const canDelete = hasPermission('admin', Permission.DELETE_PRODUCT);
  expect(canDelete).toBe(true);
});

it('should deny sales_rep from deleting product', () => {
  const canDelete = hasPermission('sales_rep', Permission.DELETE_PRODUCT);
  expect(canDelete).toBe(false);
});

권한 검사는 이제 격리되어 쉽게 테스트할 수 있습니다.

역할 검사 vs. 권한 검사 언제 사용할까

역할 검사를 사용할 때

  • 1–2개의 고정된 역할만 존재하고 절대 변하지 않을 경우.
  • 앱이 매우 단순할 때 (예: 개인 블로그나 할 일 목록).
  • 프로토타입/MVP를 만들고 나중에 리팩토링을 계획 중일 때.

권한 검사를 사용할 때

  • 비즈니스 요구사항이 변경될 가능성이 있음 (항상 그렇습니다).
  • 2개 이상의 역할이 존재할 때.
  • 프로젝트가 엔터프라이즈 수준이거나 프로덕션용**일 때.
  • 클라이언트가 새로운 역할이나 기능 변경을 요청할 수 있을 때.

확신이 서지 않으면 권한부터 시작하세요— 나중에 역할 중심 코드로 리팩토링하는 것보다 진화시키기 쉽습니다.

빠른 레퍼런스

❌ 나쁨:

if (user.role === 'admin') {
  // do thing
}

✅ 좋음:

if (hasPermission(user.role, Permission.DO_THING)) {
  // do thing
}
  • 권한을 정의합니다 (VIEW, CREATE, UPDATE, DELETE).
  • 역할과 권한을 한 곳에 매핑합니다.
  • 역할이 아니라 권한을 확인합니다.
  • 새 역할을 추가하려면 맵만 업데이트하면 됩니다.

결론

현재 코드베이스를 살펴보세요: user.role === '…'가 들어간 곳이 얼마나 있나요? 새로운 역할을 추가할 때 깨질 지점들입니다. 지금 바로 권한 기반 검사로 리팩토링하면, 제품이 성장할 때 스스로에게 감사하게 될 것입니다.

Back to Blog

관련 글

더 보기 »