복잡한 서비스에서 사용자별 접근 권한을 효율적으로 관리하려면 데이터 구조와 검증 방식에 대한 신중한 설계가 필요합니다. 비트필드는 여러 권한을 하나의 정수로 표현하여 저장 공간과 권한 검사 과정을 간소화할 수 있는 기법으로, 적절한 환경에서는 데이터베이스 접근 비용을 줄이고 애플리케이션의 구조를 단순하게 만드는 데 도움이 됩니다.
1. 세분화된 권한 관리 시스템에서 발생하는 문제
사용자와 기능이 늘어날수록 권한 관리 구조는 복잡해집니다. 특히 여러 부서와 업무 영역을 다루는 내부 관리 시스템에서는 단순한 역할 구분만으로 모든 접근 제어 요구사항을 충족하기 어렵습니다.
기능과 작업 유형에 따른 권한 분리
서울대병원 연구단 내부 백오피스를 개발하면서 고려해야 했던 주요 과제는 사용자마다 서로 다른 접근 권한을 부여하고 이를 일관성 있게 검증하는 것이었습니다. 시스템은 연구와 행정 업무에 필요한 민감한 데이터를 다루므로, 사용자에게 허용된 작업만 수행할 수 있도록 통제해야 했습니다.
권한은 크게 두 가지 기준으로 나눌 수 있습니다.
- 기능별 권한: 사용자 관리, 휴가 관리, 계약 관리, 연구비 관리 등 각 업무 모듈에 대한 접근 범위를 정의합니다.
- 행위별 권한: 데이터 조회, 수정, 삭제, 승인 등 특정 기능에서 허용할 작업을 구분합니다.
이러한 기준을 조합하면 권한의 종류가 빠르게 증가합니다. 예를 들어 계약 정보를 조회할 수 있지만 삭제할 수는 없는 사용자와, 계약을 승인할 수 있지만 다른 업무에는 접근할 수 없는 사용자를 각각 구분해야 합니다.
RBAC 방식에서 고려한 데이터베이스 비용
일반적으로 접근 제어 시스템을 설계할 때 널리 활용하는 방식 중 하나는 **RBAC(Role-Based Access Control)**입니다. 역할에 권한을 연결하고 사용자에게 역할을 부여하는 구조이므로 권한 정책을 체계적으로 관리하기에 적합합니다.
그러나 프로젝트의 요구사항과 데이터 구조에 따라 역할, 사용자, 권한 사이의 관계를 확인하기 위한 추가 조회가 필요할 수 있습니다. 특히 사용자별 권한 정보를 여러 테이블에 분산해 저장한다면, 요청을 처리할 때 관계 테이블을 조회하거나 JOIN 연산을 수행해야 할 가능성이 있습니다.
당시에는 15개 이상의 업무 모듈과 다양한 작업 권한을 다뤄야 했습니다. 전체 사용자 수가 매우 많은 시스템은 아니었지만, 반복적으로 실행되는 권한 검증 과정에서 불필요한 데이터베이스 I/O를 줄이는 것을 중요한 설계 목표로 삼았습니다.
2. 비트필드(Bitfield)의 원리와 비트 연산 이해하기
비트필드는 여러 개의 참과 거짓 상태를 하나의 정수에 압축하여 표현하는 데이터 관리 기법입니다. 각 권한에 고유한 비트 위치를 할당하면 정수 하나만으로 여러 권한의 활성화 상태를 관리할 수 있습니다.
2의 거듭제곱으로 권한을 표현하는 방식
컴퓨터에서 비트는 0 또는 1의 값을 갖습니다. 비트필드는 이러한 비트 각각을 독립적인 권한 플래그로 활용합니다.
각 권한에 (2^n)에 해당하는 값을 할당하면 서로 다른 비트 위치를 사용할 수 있습니다. 예를 들어 다음과 같이 세 가지 권한을 정의할 수 있습니다.
권한십진수 값2진수 표현
| 권한 A | 1 | 0001 |
| 권한 B | 2 | 0010 |
| 권한 C | 4 | 0100 |
| 권한 A + B | 3 | 0011 |
| 권한 A + C | 5 | 0101 |
| 권한 A + B + C | 7 | 0111 |
각 권한은 서로 겹치지 않는 비트 자리를 사용하므로 여러 권한을 하나의 값으로 결합해도 개별 권한을 구분할 수 있습니다.
OR 연산으로 권한을 결합하고 AND 연산으로 확인하기
비트필드에서 핵심적으로 사용하는 연산은 **비트 OR(|)과 비트 AND(&)**입니다.
OR 연산은 여러 권한을 하나의 정수로 합칠 때 사용합니다. 예를 들어 권한 A의 값이 1이고 권한 B의 값이 2라면, 두 값을 OR 연산한 결과는 3입니다.
const PERMISSION_A = 1n << 0n; // 1
const PERMISSION_B = 1n << 1n; // 2
const permissions = PERMISSION_A | PERMISSION_B;
console.log(permissions); // 3n
권한을 확인할 때는 AND 연산을 사용합니다.
const hasPermission = (permissions: bigint, permission: bigint) =>
(permissions & permission) === permission;
console.log(hasPermission(permissions, PERMISSION_A)); // true
console.log(hasPermission(permissions, PERMISSION_B)); // true
AND 연산 결과에 검사하려는 권한의 비트가 남아 있다면 해당 권한을 보유하고 있다고 판단할 수 있습니다. 이러한 검사는 데이터베이스에서 권한 관계를 다시 조회하지 않고도 메모리에 있는 정수값만으로 처리할 수 있습니다.
다만 비트 연산 자체가 빠르다는 사실과 전체 요청 처리 시간이 반드시 단축된다는 것은 구분해야 합니다. 실제 성능 개선 효과는 데이터 조회 방식, 캐시 활용 여부, 권한 정보의 전달 구조 등 여러 요소에 따라 달라집니다.
3. Discord의 권한 관리 방식에서 얻은 설계 아이디어
비트필드를 실제 프로젝트에 적용하기 전에는 복잡한 권한 구조를 다루는 서비스의 구현 사례를 살펴보는 것이 도움이 됩니다. Discord는 서버와 채널 등 다양한 범위에서 권한을 관리하는 서비스로, 권한 비트와 비트 연산을 활용하는 방식을 공식 개발 문서에서 설명하고 있습니다.

여러 권한을 하나의 값으로 관리하기
Discord의 권한 체계는 각 권한에 고유한 비트 값을 할당하고, 여러 권한을 하나의 값으로 표현하는 방식을 사용합니다. 이러한 접근법은 권한 플래그를 개별적으로 저장하고 확인하는 대신 비트 연산을 통해 필요한 상태를 빠르게 판별할 수 있다는 장점이 있습니다.
이 사례를 참고하면서 연구단 백오피스에도 동일한 원리를 적용할 수 있다고 판단했습니다. 다만 Discord와 같은 대규모 서비스에서 사용한다는 이유만으로 특정 설계가 모든 프로젝트에 적합한 것은 아닙니다. 실제로는 시스템의 권한 구조와 데이터 접근 패턴을 기준으로 판단해야 합니다.
TypeScript BigInt를 활용한 권한 상수 정의
프로젝트에서는 권한별 비트 위치를 명시적으로 지정하고, TypeScript의 BigInt 타입을 사용하여 권한 값을 정의했습니다.
export const PERMISSION = {
기초연구팀: 1n << 0n,
임상연구팀: 1n << 1n,
초과근무승인관리권한: 1n << 7n,
초과근무수당관리권한: 1n << 8n,
세부기관연구비읽기권한: 1n << 17n,
세부기관연구비삭제권한: 1n << 18n,
계약읽기권한: 1n << 25n,
계약승인권한: 1n << 26n,
계약삭제권한: 1n << 27n,
} as const;
위 코드는 실제 권한 정의의 일부를 보여주는 예시입니다. 1n << n은 숫자 1의 비트를 왼쪽으로 n칸 이동하는 연산으로, 각 권한에 서로 다른 비트 값을 할당합니다.
JavaScript의 일반적인 Number 타입은 정수를 정확하게 표현할 수 있는 범위에 제한이 있습니다. 반면 BigInt는 더 큰 정수를 정밀하게 다룰 수 있으므로 권한 비트가 증가하는 구조에서 유용합니다.
또한 BigInt 연산에는 일반 숫자와 구분되는 규칙이 있습니다. 비트 연산에 사용하는 값은 BigInt로 통일해야 하며, 일반적인 JSON 직렬화 과정에서도 별도의 변환 처리가 필요합니다.
데이터베이스 BIGINT와 애플리케이션 타입 맞추기
권한 값을 데이터베이스에 저장할 때는 애플리케이션의 자료형과 데이터베이스 컬럼의 표현 범위를 함께 고려해야 합니다. 해당 프로젝트에서는 큰 정수값을 저장하기 위해 BIGINT를 사용했습니다.
다만 데이터베이스의 BIGINT가 항상 부호 없는 64비트 정수를 의미하는 것은 아닙니다. 예를 들어 PostgreSQL의 BIGINT는 부호 있는 64비트 정수이므로, 최상위 비트를 권한 플래그로 활용할 때는 값의 범위와 부호 처리에 주의해야 합니다.
TypeScript와 데이터베이스 사이에서 권한 값을 주고받는 과정에서도 정밀도가 손실되지 않도록 해야 합니다. 특히 큰 정수값을 일반 JavaScript 숫자로 변환하거나 JSON으로 전달하면 예상하지 못한 오류가 발생할 수 있으므로, 문자열 직렬화와 역직렬화 등 명확한 변환 규칙을 마련하는 것이 좋습니다.
4. 비트필드 도입을 결정하기 전에 검토한 세 가지 질문
비트필드는 구현이 간결하다는 장점이 있지만, 모든 권한 관리 문제를 해결해 주는 만능 도구는 아닙니다. 프로젝트에 적용하기 전에는 개발 환경과 운영 요구사항을 기준으로 장단점을 비교했습니다.
질문 1. 소규모 백오피스에도 비트필드가 필요한가?
가장 먼저 고려한 부분은 기술 도입에 따른 복잡성과 실제 성능 개선 효과였습니다.
일반적인 백오피스는 초대규모 트래픽을 처리하는 서비스와 환경이 다릅니다. 따라서 권한 검증 속도만을 이유로 비트필드를 도입하면 오히려 유지보수 비용이 증가할 수 있습니다.
그럼에도 해당 프로젝트에서는 반복적인 데이터베이스 접근을 줄이고, 이미 조회한 사용자 객체에 포함된 권한 값만으로 검증을 수행하는 구조를 목표로 삼았습니다.
중요한 점은 비트필드 자체가 아니라 권한 검증 과정에서 불필요한 데이터베이스 접근을 줄이는 데 있습니다. 권한 정보가 메모리에 적절하게 유지되지 않거나 요청마다 별도의 조회가 계속 발생한다면, 비트필드를 사용하더라도 기대한 효과를 얻기 어렵습니다.
질문 2. 프런트엔드와 백엔드 사이의 권한 정의는 어떻게 일치시킬 것인가?
비트필드 기반 권한 체계에서 주의할 부분 중 하나는 권한 값의 의미가 모든 구성 요소에서 동일하게 유지되어야 한다는 것입니다.
예를 들어 프런트엔드는 특정 비트가 계약 삭제 권한이라고 판단하지만 백엔드는 다른 권한으로 해석한다면, 화면 표시와 실제 서버 동작 사이에 불일치가 발생할 수 있습니다.
당시 프로젝트는 프런트엔드와 백엔드를 한 명의 개발자가 함께 구현하는 환경이었습니다. 덕분에 권한 상수를 단일 기준으로 관리하고 양쪽 코드에서 동일한 정의를 활용하기가 상대적으로 수월했습니다.
하지만 여러 개발자가 참여하는 프로젝트에서는 이러한 장점이 자동으로 보장되지 않습니다. 공통 패키지로 권한 정의를 관리하거나 스키마를 기반으로 코드를 생성하고, 권한별 테스트를 작성하는 등 별도의 관리 체계가 필요합니다.
또한 프런트엔드의 권한 검사는 사용자 경험을 개선하기 위한 수단으로 활용해야 합니다. 실제 데이터 접근을 허용하거나 거부하는 최종 판단은 반드시 백엔드에서 수행해야 합니다. 화면에서 버튼을 숨기는 것만으로 보안이 확보되는 것은 아닙니다.
질문 3. 상위 권한과 하위 권한 사이의 관계는 어떻게 표현할 것인가?
실제 업무에서는 특정 권한을 보유한 사용자에게 여러 하위 권한을 함께 부여해야 하는 상황이 발생합니다. 예를 들어 상위 업무 관리 권한에 조회, 수정, 승인 권한이 포함되는 구조를 생각할 수 있습니다.
프로젝트에서는 이러한 관계를 처리하기 위해 권한 번들링(Bundling) 방식을 고려했습니다. 상위 권한을 부여할 때 관련 하위 권한 비트도 OR 연산으로 결합하는 방식입니다.
const READ_PERMISSION = 1n << 0n;
const WRITE_PERMISSION = 1n << 1n;
const DELETE_PERMISSION = 1n << 2n;
const ADMIN_PERMISSION =
READ_PERMISSION |
WRITE_PERMISSION |
DELETE_PERMISSION;
이 예시에서 관리자 권한은 조회, 수정, 삭제 권한을 모두 포함합니다. 이후 권한 검증 코드는 각각의 권한 비트를 확인하는 단순한 형태로 유지할 수 있습니다.
다만 비트필드 자체에는 상하위 권한 관계가 내장되어 있지 않습니다. 상위 권한이 어떤 하위 권한을 포함하는지는 애플리케이션의 정책으로 명시해야 합니다. 또한 권한 비트를 추가하거나 제거할 때 기존 번들 정의와 사용자별 권한 데이터가 일관성을 유지하도록 관리해야 합니다.
5. 비트필드 운영 과정에서 확인한 한계와 주의사항
비트필드는 권한 검사에 유용하지만, 데이터를 검색하고 사람이 해석하며 정책을 변경하는 과정에서는 별도의 고려사항이 필요합니다.
특정 권한을 보유한 사용자 검색과 인덱스 문제
비트필드 값은 여러 권한을 하나의 정수로 합쳐 저장하기 때문에 특정 권한을 가진 사용자를 찾을 때 비트 연산 조건이 필요합니다.
예를 들어 특정 권한을 가진 사용자를 검색하려면 다음과 같은 조건을 사용할 수 있습니다.
SELECT *
FROM users
WHERE (permissions & 33554432) = 33554432;
여기서 permissions는 권한 비트필드를 저장한 컬럼이며, 예시의 숫자는 검사할 권한에 해당하는 비트 값입니다.
이러한 연산 조건은 일반적인 B-tree 인덱스를 이용한 단순한 동등 비교와 다르게 동작할 수 있습니다. 데이터베이스가 적절한 인덱스를 활용하지 못하면 많은 행을 검사해야 하므로, 데이터 규모가 커질수록 조회 비용이 증가할 수 있습니다.
PostgreSQL에서는 실제 쿼리와 데이터 분포를 분석한 뒤 함수 기반 인덱스나 표현식 인덱스를 검토할 수 있습니다. 다만 비트 연산 조건에 대한 인덱스 적용 가능성은 쿼리 형태와 데이터베이스의 실행 계획에 따라 달라지므로, EXPLAIN ANALYZE 등을 통해 실제 효과를 확인하는 과정이 필요합니다.
특정 권한을 가진 사용자를 자주 검색하고 권한 종류도 계속 증가한다면, 별도의 권한 관계 테이블을 유지하는 방식이 더 적합할 수도 있습니다.
숫자로 저장된 권한 정보의 가독성 저하
비트필드의 또 다른 단점은 데이터베이스에 저장된 숫자만 보고 실제 권한을 직관적으로 파악하기 어렵다는 것입니다.
예를 들어 권한 값이 134217731로 저장되어 있다면, 별도의 해석 과정 없이 어떤 권한이 활성화되어 있는지 알아내기 어렵습니다. 운영 중 발생한 권한 오류를 조사하거나 특정 사용자의 접근 권한을 검토할 때 불편함이 생길 수 있습니다.
이를 보완하려면 권한 숫자를 사람이 읽을 수 있는 권한 이름으로 변환하는 디버깅 도구를 마련하는 것이 좋습니다. 관리자 화면에서 권한별 활성화 상태를 표시하거나, 로그에 권한 이름과 원본 비트필드 값을 함께 기록하는 방법도 활용할 수 있습니다.
권한 정의가 변경되었을 때 과거 데이터가 어떤 의미를 갖는지 추적할 수 있도록, 권한 상수와 정책 변경 이력을 함께 관리하는 것도 중요합니다.
권한 변경과 보안 정책의 일관성 유지
권한을 비트 단위로 저장하면 값을 변경하는 작업 자체는 간단하지만, 실제 운영 정책까지 단순해지는 것은 아닙니다.
예를 들어 권한을 회수할 때는 AND 연산과 비트 NOT 연산을 조합할 수 있습니다.
const updatedPermissions =
permissions & ~READ_PERMISSION;
이 코드는 지정한 조회 권한 비트를 제거하는 일반적인 방법입니다. 다만 BigInt의 비트 NOT 연산은 임의 정밀도 정수의 비트 표현에 적용되므로, 데이터베이스에 저장할 값의 범위와 실제 권한 정의를 함께 고려해야 합니다.
또한 권한을 변경한 뒤 캐시나 세션에 이전 값이 남아 있다면, 사용자가 이미 회수된 권한을 일시적으로 계속 사용할 가능성이 있습니다. 민감한 데이터에 접근하는 시스템에서는 권한 변경 시점과 캐시 무효화 정책, 서버 측 재검증 방식을 함께 설계해야 합니다.
6. 프로젝트 환경에 맞는 권한 관리 구조를 선택하는 방법
권한 관리 설계에서 중요한 것은 특정 기술의 우수성을 증명하는 것이 아니라, 시스템의 요구사항에 적합한 방식을 선택하는 것입니다.
비트필드는 권한의 종류가 비교적 명확하고, 권한 검증이 빈번하며, 권한 값을 정수로 효율적으로 전달하고 관리할 수 있는 환경에서 유용합니다. 반면 권한별 메타데이터가 많거나, 권한을 동적으로 생성해야 하거나, 권한과 사용자 사이의 관계를 자주 검색해야 한다면 관계형 테이블 기반 구조가 더 적합할 수 있습니다.
실제 설계에서는 다음 기준을 함께 검토하는 것이 좋습니다.
- 권한 검증 빈도: 요청마다 반복되는 권한 검증이 성능에 얼마나 영향을 주는지 확인합니다.
- 권한 구조의 변화: 권한 종류가 자주 추가되거나 정책이 복잡하게 변경되는지 살펴봅니다.
- 검색 요구사항: 특정 권한을 가진 사용자 목록을 얼마나 자주 조회하는지 분석합니다.
- 개발 및 유지보수 환경: 여러 개발자가 공통 권한 정의를 안전하게 관리할 수 있는지 검토합니다.
- 보안 요구사항: 권한 회수, 캐시 무효화, 백엔드 검증이 일관되게 이루어지는지 확인합니다.
서울대병원 연구단 백오피스 프로젝트에서는 제한된 개발 인력과 반복적인 권한 검증이라는 조건을 고려해 비트필드를 선택했습니다. 이 결정은 모든 서비스에 적용할 수 있는 정답이라기보다 당시의 제약과 목표에 맞춘 설계 선택이었습니다.
좋은 설계는 유행하는 기술을 적용하는 데서 끝나지 않습니다. 해결하려는 문제를 명확히 정의하고, 구현의 이점과 운영상의 단점을 비교하며, 서비스가 성장했을 때 발생할 수 있는 변화까지 고려해야 합니다. 비트필드 역시 이러한 기준에 따라 선택하고 검증할 때 비로소 가치 있는 도구가 됩니다.