역할 vs 권한: RBAC에서 역할 검사를 사용하면 안 되는 이유
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_rep에UPDATE_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 === '…'가 들어간 곳이 얼마나 있나요? 새로운 역할을 추가할 때 깨질 지점들입니다. 지금 바로 권한 기반 검사로 리팩토링하면, 제품이 성장할 때 스스로에게 감사하게 될 것입니다.