지난 편까지 데이터를 어떻게 저장하고, 조회하고, 넣는가를 다뤘습니다. 이번 편은 방향이 조금 다릅니다. 그 데이터를 누가 어떻게 볼 것인가 쪽으로 넘어옵니다.
BigQuery의 접근 제어는 여러 층이 겹쳐 있습니다. IAM으로 큰 틀을 잡고, 그 위에 행 단위(RLS), 컬럼 단위(policy tag), 마스킹이 각각 다른 규칙으로 얹혀 있습니다. 각 층이 뭘 담당하고 어떻게 상호작용하는지, 그 그림을 이번 편에서 잡아 봅니다.
1. 네 층의 지형
BigQuery 접근 제어는 네 개의 층에 위임 메커니즘 하나가 더 얹혀 있는 구조입니다. 가장 바깥의 IAM이 "이 사용자가 이 리소스에 접근 가능한가"를 결정합니다. Organization부터 Table까지 이어지는 리소스 계층에 role을 붙이고, 그 role이 하위로 상속됩니다. 그 위에 RLS(Row-Level Security)가 각 사용자에게 보이는 행을 CREATE ROW ACCESS POLICY로 필터링하고, CLS(Column-Level Security)는 Data Catalog의 policy tag로 컬럼 단위 접근을 통제합니다. 마지막으로 Dynamic Data Masking이 policy tag 위에 얹혀, 원본 값을 어떻게 변환해서 보여줄지를 결정합니다.
여기에 Authorized View / Dataset / Routine이 한 층 더 있습니다. IAM 관점에서 base 테이블 접근권한이 없는 사용자에게도 view의 결과만 노출해 주는 위임 메커니즘입니다.
이 다섯 층이 서로 어떻게 상호작용하는지가 헷갈리는 지점입니다. 예를 들어 authorized view가 base 테이블의 IAM은 우회하지만 RLS는 계속 평가한다든가, 마스킹된 컬럼을 RLS 조건에 참조해도 일반 정책이면 그대로 동작하지만 서브쿼리가 들어간 정책은 참조 컬럼에 unmasked 접근이 필요하다든가 하는 규칙들이 각각 다른 문서에 흩어져 있습니다. 이 규칙들은 §7에서 한자리에 모읍니다.
2. IAM — 큰 틀
2-1. 리소스 계층과 상속
BigQuery IAM role은 다음 리소스에 붙일 수 있습니다.
Organization
└── Folder
└── Project
└── Dataset
└── Table / View / Routine상위에서 부여한 role은 하위로 상속됩니다. Organization 레벨에 bigquery.dataViewer를 주면 그 조직 아래의 모든 프로젝트·데이터셋·테이블에 read 권한이 생깁니다.
여기서 흔히 틀리는 게 있는데, Row와 Column은 IAM role을 붙이는 지점이 아닙니다. 위 계층 다이어그램에 Row/Column을 이어 그리면 부정확합니다. 행 단위 제어는 RLS 정책, 컬럼 단위 제어는 policy tag라는 별도 시스템으로 처리되고, 두 시스템 모두 IAM과 나란히 서 있는 다른 레이어입니다.
2-2. 주요 Predefined role
BigQuery의 predefined role은 20개가 넘는데, 자주 쓰는 것만 정리하면 이렇습니다.
조회자에게 붙이는 표준 조합은 `bigquery.dataViewer`(데이터 읽기) + `bigquery.jobUser`(쿼리 실행)입니다. 두 role 모두 필요합니다. dataViewer 단독으로는 jobs.create가 없어 쿼리를 실행할 수 없고, jobUser 단독으로는 데이터를 읽지 못합니다.
이름 때문에 bigquery.user를 조회자용으로 오해하기 쉬운데, 이 role에는 tables.getData가 없어서 남이 만든 테이블의 데이터를 읽지 못합니다. 대신 datasets.create가 들어 있어(본인이 만든 dataset에는 dataOwner가 자동 부여) 조회 전용 사용자에게는 오히려 과한 권한입니다. bigquery.user만 주고 "왜 데이터가 안 보이지"로 헤매는 팀을 여럿 봤습니다.
2-3. IAM Deny — Allow보다 먼저 평가
일반적인 IAM binding은 "허용(allow)"입니다. 여기에 IAM Deny 정책을 조직·폴더·프로젝트에 붙이면 특정 권한을 명시적으로 차단할 수 있고, deny는 allow보다 항상 먼저 평가됩니다.
즉 어떤 사용자에게 bigquery.dataViewer가 상속되어 있어도, 상위에 bigquery.tables.getData deny가 걸려 있으면 read가 안 됩니다. 조직 차원에서 특정 데이터셋 접근을 강제로 막을 때 씁니다.
2-4. Dataset ACL과의 관계
Dataset에는 IAM binding 외에 Dataset ACL이라는 예전 방식의 접근 리스트가 여전히 남아 있습니다. UI의 "Sharing" 다이얼로그에서 특정 사용자·그룹·다른 view를 추가하는 게 여기에 해당합니다. 최근 BigQuery는 두 시스템을 IAM 쪽으로 통합하는 방향으로 정리해 왔는데, 두 곳 모두 확인해 두는 게 안전합니다. 특히 authorized view 등록은 여전히 dataset ACL을 통합니다.
3. 행 단위 보안 (Row-Level Security)
3-1. CREATE ROW ACCESS POLICY
RLS는 테이블에 붙이는 필터입니다. 특정 grantee에게는 특정 조건을 만족하는 행만 보여 줍니다.
CREATE ROW ACCESS POLICY apac_filter
ON `project.dataset.sales`
GRANT TO ("group:sales-apac@example.com")
FILTER USING (region = "APAC");이 정책이 걸린 뒤로 sales-apac@ 그룹 사용자는 sales 테이블을 조회하면 region = "APAC" 행만 반환됩니다. 사용자가 직접 WHERE region = "US" 조건을 걸어도 apac 필터가 우선 적용되어 US 행은 안 보입니다.
3-2. 여러 정책은 OR로 합쳐진다
한 테이블에 여러 RLS 정책이 있고 한 사용자가 여러 정책의 grantee면, 그 사용자는 각 정책이 허용하는 모든 행의 합집합(UNION)을 볼 수 있습니다.
CREATE ROW ACCESS POLICY apac_filter
ON ... GRANT TO ("group:sales-apac@") FILTER USING (region = "APAC");
CREATE ROW ACCESS POLICY emea_filter
ON ... GRANT TO ("group:sales-emea@") FILTER USING (region = "EMEA");두 그룹 모두에 속한 사용자는 region IN ("APAC", "EMEA") 행을 볼 수 있습니다. 정책을 여러 개 걸어 "이 그룹은 이 영역, 저 그룹은 저 영역"으로 나누는 식으로 많이 씁니다.
3-3. 정책이 있으면 기본은 deny
주의할 규칙이 있습니다. RLS 정책이 하나라도 걸린 테이블에서, 어떤 정책의 grantee도 아닌 사용자는 단 한 행도 보지 못합니다. IAM으로 dataViewer를 받았어도 grantee 리스트에 없으면 마찬가지입니다. 이때 쿼리가 에러로 실패하는 게 아닙니다. 쿼리는 정상적으로 성공하고, 0행이 반환됩니다. "결과가 행 접근 정책에 의해 필터링되었을 수 있다"는 안내 메시지가 표시될 뿐입니다. access denied 에러가 나는 경우는 RLS와 무관하게 테이블 IAM 권한(bigquery.tables.getData) 자체가 없을 때뿐입니다.
즉 RLS는 "이 정책의 조건에 맞는 행을 보여준다"인 동시에 "이 정책의 grantee만 이 테이블을 볼 수 있다"이기도 합니다. 정책을 걸기 시작하면 기존 IAM 접근자를 grantee에 명시적으로 포함시키지 않으면 그 사람들도 빈 결과만 받게 됩니다. 에러 한 줄 없이 조용히 0행만 돌아오니 알아채기도 어렵습니다.
관리자도 예외가 아닙니다. bigquery.admin이나 bigquery.dataOwner role을 가진 사용자·서비스 계정도 RLS 필터를 자동으로 우회하지 못합니다. 정책은 만들고 지울 수 있지만, 정작 쿼리할 때는 grantee 리스트에 없으면 다른 사용자와 똑같이 필터에 걸립니다. 그래서 정책 하나를 새로 추가할 때는 관리자·ETL 서비스 계정처럼 전체 행이 필요한 principal에게 FILTER USING (TRUE) 정책을 별도로 만들어 grantee로 넣어 두는 게 표준입니다. 이 조치가 없으면 "정책 걸었더니 admin인 내가 못 본다"거나 "ETL이 조용히 빈 결과를 받는다" 상황으로 이어집니다.
-- 관리자·ETL용 전체 접근 정책
CREATE ROW ACCESS POLICY admin_full_access
ON `project.dataset.sales`
GRANT TO ("group:data-platform-admin@example.com",
"serviceAccount:etl@my-project.iam.gserviceaccount.com")
FILTER USING (TRUE);내부적으로 이 "grantee만 볼 수 있다"는 규칙은 `roles/bigquery.filteredDataViewer`라는 role로 구현됩니다. 정책의 grantee로 등록하면 BigQuery가 이 role(권한은 rowAccessPolicies.getFilteredData 하나)을 자동으로 부여합니다. predefined role 목록에 있어 기술적으로는 IAM으로 직접 붙일 수도 있지만, 공식 문서는 정책 grantee 등록을 정상 경로로 안내합니다. IAM 감사에서 이 role이 보이면 대개 "어딘가의 RLS 정책 grantee"라는 뜻으로 읽으면 됩니다.
3-4. SESSION_USER()로 사용자 매칭
필터 조건에 IAM principal을 직접 참조할 수 있습니다.
CREATE ROW ACCESS POLICY user_own_rows
ON `project.dataset.user_events`
GRANT TO ("domain:example.com")
FILTER USING (email = SESSION_USER());SESSION_USER()는 현재 쿼리를 실행하는 사용자의 이메일을 돌려줍니다. 이런 정책이면 각 사용자는 email 컬럼이 본인 이메일인 행만 볼 수 있게 됩니다. multi-tenant 데이터셋에서 "각 사용자는 자기 데이터만"을 구현할 때 사실상 공식처럼 쓰이는 방식입니다.
3-5. RLS 호환성 제약
정책이 걸린 테이블은 몇 가지 기능과 호환되지 않습니다.
- 콘솔의 테이블 미리보기(Preview)가 안 됩니다. 공식 문서도 "Previewing or browsing tables is incompatible with row-level security"라고 못 박고 있습니다. 스키마는 메타데이터라 RLS와 무관하게 그대로 보이고, 막히는 건 데이터 몇 줄을 미리 보는 기능입니다
- 테이블 샘플링(
TABLESAMPLE)도 호환 대상이 아닙니다
미리보기가 안 되는 건 이 기능이 쿼리를 거치지 않고 저장된 행을 그대로 읽는 경로이기 때문입니다. "테이블 미리보기가 갑자기 안 된다"는 문의가 오면 대개 그 사이 RLS가 걸린 겁니다.
4. 컬럼 단위 보안 (Policy Tags)
4-1. Data Catalog taxonomy
컬럼 단위 보안(CLS)은 Data Catalog의 policy tag taxonomy를 통해 관리합니다. Data Catalog 제품 자체는 Knowledge Catalog(구 Dataplex Universal Catalog)로 이관되는 중이지만, CLS용 policy tag와 taxonomy는 지원 중단 대상이 아니고 BigQuery 문서도 여전히 이 개념 그대로 서술합니다. 조직 단위로 taxonomy를 만들고 그 안에 tag를 계층적으로 정의한 다음, BigQuery 테이블의 컬럼에 tag를 붙이는 흐름입니다.
Taxonomy: "PII"
├── high_sensitivity
│ ├── ssn
│ └── credit_card
└── medium_sensitivity
└── email컬럼에 ssn 태그를 붙이면, 그 컬럼은 접근 권한이 있는 사용자에게만 노출됩니다.
Taxonomy는 리전 단위 리소스라, 대상 테이블과 같은 리전에 있어야 태그를 부착할 수 있습니다. 예를 들어 us-central1에 있는 테이블에는 asia-northeast3(서울)에서 만든 taxonomy 태그를 붙일 수 없습니다. 여러 리전에 같은 태그 체계를 쓰려면 taxonomy를 각 리전으로 복제해야 합니다.
4-2. Fine-Grained Reader role
Policy tag가 걸린 컬럼을 조회하려면 `roles/datacatalog.categoryFineGrainedReader` role이 그 tag(또는 상위 tag)에 부여돼 있어야 합니다. 없으면 그 컬럼은 접근 불가.
사용자 A: PII/medium_sensitivity 에 fineGrainedReader
→ email 컬럼 O, ssn/credit_card 컬럼 X
사용자 B: PII 최상위에 fineGrainedReader
→ 모든 하위 태그 컬럼 O계층 구조 덕에 상위 태그 권한을 주면 하위 태그가 자동으로 열립니다.
4-3. 컬럼당 태그 1개, SELECT *는 실패
한 컬럼에는 policy tag를 하나만 붙일 수 있습니다. 여러 관점(예: PII이자 동시에 재무 데이터)이 필요하면 taxonomy를 그렇게 설계해야 하지, 태그를 여러 개 얹을 수는 없습니다.
그리고 **SELECT *는 접근 못하는 컬럼이 있으면 쿼리가 실패합니다.** BigQuery는 접근 안 되는 컬럼을 자동으로 제외해 주지 않습니다. 오류 메시지에 "접근 불가한 컬럼 목록"이 뜨고, 사용자는 명시적으로 컬럼을 나열해 다시 쿼리해야 합니다.
Error: Access Denied: BigQuery BigQuery: User does not have
permission to access policy tag "..." on columns
project:dataset.users.ssn, project:dataset.users.credit_card.이 동작 때문에 dbt 같은 도구에서 SELECT * 관행을 유지하면 policy tag가 붙은 순간 파이프라인이 깨집니다. downstream 쿼리들을 명시적 컬럼 나열로 바꿔 두는 사전 작업이 필요합니다.
5. 동적 데이터 마스킹 (Dynamic Data Masking)
5-1. Data Policy가 policy tag에 masking rule을 연결
CLS는 컬럼을 아예 안 보여주는 방식이라 downstream이 깨지기 쉽습니다. 그 대안이 Dynamic Data Masking입니다. 컬럼은 보여주되 값을 변환해서 원본을 감춥니다.
Policy tag (예: PII/email)
↓
Data Policy (masking rule + principal 매핑)
↓
컬럼 조회 시 principal에 맞는 rule로 변환된 값 반환Data Policy는 "이 tag가 붙은 컬럼에 대해, 이 principal에게는 이 masking 함수를 적용한다"를 정의합니다.
5-2. 지원 masking routine
BigQuery가 제공하는 masking 함수는 크게 이 정도입니다.
다만 Default masking value의 숫자 0은 실제 원본이 0인 행(예: 잔액 0원, 조회수 0)과 구분되지 않아 downstream에서 오해를 부릅니다. 감사·집계 쿼리가 이 두 종류를 섞어 계산해 집계가 오염되기 쉬워, default보다는 Nullify나 Custom으로 명시적으로 구분되는 값을 반환하는 편이 낫습니다.
Custom과 Hash가 가장 많이 쓰입니다. Custom은 사내 정책에 맞춘 임의 변환(예: 이메일 도메인만 남기기)이 필요할 때, Hash는 downstream에서 원본 값 없이도 그룹화·조인이 필요할 때입니다.
5-3. 다중 규칙 우선순위
한 사용자에게 여러 data policy가 동시에 적용될 수 있는 상황이면, 다음 순서 중 가장 위에 있는 rule이 적용됩니다.
1. Custom masking routine
2. Random Hash
3. Hash (SHA-256)
4. Email mask
5. Last four characters
6. First four characters
7. Date year mask
8. Default masking value
9. NullifyCustom이 가장 강한 우선순위입니다. 조직 정책에 맞춘 UDF가 다른 표준 마스킹을 override합니다.
5-4. Fine-Grained Reader vs Masked Reader
Policy tag가 걸린 컬럼을 볼 수 있는 사용자는 두 종류로 나뉩니다.
- `roles/datacatalog.categoryFineGrainedReader` — 원본(unmasked) 값 조회
- `roles/bigquerydatapolicy.maskedReader` — masking 적용된 값 조회
한 사용자가 두 role을 다 가지면 어느 쪽이 적용될지는 policy tag 계층에서 각 role이 부여된 위치로 갈립니다. BigQuery는 컬럼에 붙은 태그에서 출발해 계층을 거슬러 올라가며 처음 만나는 role을 적용합니다. Masked Reader가 하위 태그(컬럼에 가까운 쪽), Fine-Grained Reader가 상위 태그에 있으면 원본이 아니라 마스킹된 값이 반환됩니다. Data Policy와 두 role의 조합으로 "PII 팀은 원본, 나머지 분석가는 마스킹된 값" 같은 세팅을 짭니다.
6. Authorized View / Dataset / Routine
6-1. Base IAM 우회 원리
Authorized view는 사용자가 view의 base 테이블에 직접 IAM 접근권한이 없어도 view를 조회할 수 있게 하는 위임 메커니즘입니다. 흐름은 이렇습니다.
- Dataset A에
sales_raw테이블 있음 (분석가는 접근 불가) - Dataset B에
sales_summary라는 view를 만듦 (sales_raw를 집계) - Dataset A의 ACL에서
sales_summaryview를 authorized view로 등록 - 이제 분석가에게 Dataset B의
dataViewerrole만 주면 view 조회 가능.sales_raw에 대한 권한은 필요 없음
Authorized dataset은 여러 view를 하나로 묶어 dataset 단위로 authorize하는 형태입니다. Dataset ACL 하나에 최대 2,500개의 authorized resource를 등록할 수 있습니다.
Authorized routine은 같은 개념의 UDF·프로시저 버전입니다. 함수가 base 테이블에 접근할 수 있지만, 함수 호출자에겐 그 테이블 권한이 없어도 됩니다.
routine이 remote function처럼 외부 리소스를 호출하는 구조라면 권한 축이 하나 더 붙습니다. 외부 연결(connection)에 대한 `roles/bigquery.connectionUser`가 호출자에게 별도로 필요합니다. authorized routine 권한만 챙기고 connection 권한을 빠뜨리면 함수 호출이 실패하는데, 에러 메시지가 routine이 아니라 connection을 가리켜서 원인을 찾는 데 시간이 걸립니다. remote function으로 복호화나 API 연동을 하는 팀이 자주 밟는 지뢰입니다.
6-2. Authorized view는 IAM은 우회하지만 RLS는 우회 못 함
Authorized view가 우회해 주는 건 base 테이블의 IAM binding뿐입니다. RLS는 여전히 view 조회자를 기준으로 평가됩니다.
Base 테이블 `sales_raw`
- IAM: 분석가에게 접근 없음
- RLS: apac 그룹만 APAC 행 접근 가능
Authorized view `sales_summary` (base를 집계)
- 분석가에게 view의 dataViewer 부여
- 하지만 분석가가 apac 그룹이 아니면 → 쿼리는 성공하지만 0행 반환
(base의 RLS가 view 조회자 기준으로 평가되어 모든 행이 걸러짐)즉 authorized view로 IAM을 우회하려면 base의 RLS 정책에도 view 조회자를 grantee로 추가해야 합니다. 이 조건을 놓치면 "view는 만들었는데 다들 빈 결과만 받는다"가 됩니다. 에러가 안 나니 원인 찾기가 더 어렵습니다.
Materialized view도 마찬가지입니다. RLS가 걸린 base 위 MV는 가속 대상에서 빠집니다. 조회자마다 결과가 달라져야 하는 성격상 MV·BI Engine 같은 사전 계산 기반 가속이 적용되지 않는다는 게 공식 서술입니다. RLS를 유지하면서 MV 이점을 살리고 싶으면 base에 RLS를 걸지 않고 view 층에서 필터를 거는 구조 같은 대안을 고려해야 합니다.
7. 평가 순서와 상호작용
위 다섯 층이 하나의 쿼리에 동시에 걸릴 때 어떤 순서로 평가되는지는 실무에서 사고가 가장 많이 나는 영역입니다. 문서 곳곳에 흩어져 있는 조각들을 모아 봅니다.
7-1. 공식 문서에서 확인 가능한 조각들
우선 밝혀 둘 게 있습니다. "IAM → RLS → CLS → Masking" 전체 순서를 하나의 문장으로 못박은 공식 문서는 없습니다. 대신 여러 문서의 근거를 조합하면 다음이 나옵니다.
- IAM Deny > IAM Allow: Deny 정책이 항상 먼저 평가됨
- RLS 필터 이후 Masking: "Data masking is applied on top of row-level security" (공식 표현). 일반(non-subquery) RLS 정책이 마스킹된 컬럼을 참조하면 필터는 원본 값으로 평가되고 결과만 마스킹됨
- 서브쿼리 RLS 정책은 참조 컬럼에 unmasked 권한 필요: 조건식에 서브쿼리가 들어간 정책이 마스킹·CLS 제한 컬럼을 참조하면, 조회자에게 그 컬럼의 fineGrainedReader가 없을 때 에러
- CLS 체크가 RLS보다 먼저: 쿼리가 policy tag로 제한된 컬럼을 명시적으로 참조하면 RLS 필터가 개입하기 전에 column-level 검사에서 access denied
- Authorized view는 base IAM 우회하지만 RLS는 유지: view 조회자에게 base RLS grant 필요
참고로 이 다섯 층 바깥에 VPC Service Controls라는 네트워크 경계 층이 하나 더 있습니다. IAM을 다 통과해도 perimeter 규칙에서 막힐 수 있어서, "권한은 다 맞는데 왜 안 되지" 싶을 때 가장 먼저 의심할 층입니다. 이번 편 범위는 아니라 존재만 짚어 둡니다.
이를 이어 붙이면 대략 이런 순서가 그려집니다.
1. IAM Deny 체크 (하나라도 걸리면 즉시 거부)
2. IAM Allow 체크 (테이블/뷰 접근 가능한가)
- Authorized view면 base IAM은 우회
3. Column-level check (쿼리가 참조하는 컬럼의 policy tag 접근권)
4. RLS 정책 평가 (grantee인가 → 필터 적용)
5. Masking 적용 (Data Policy에 따라 값 변환)세부 규칙은 아래 사례가 말해 줍니다.
7-2. 층이 겹칠 때 나는 사고들
겹침 사고 A: 마스킹된 컬럼을 참조하는 RLS 정책 (일반 vs 서브쿼리)
-- 일반(non-subquery) RLS 정책이 마스킹된 컬럼을 참조
CREATE ROW ACCESS POLICY user_own
ON `project.dataset.orders`
GRANT TO ("group:analysts@")
FILTER USING (customer_email = SESSION_USER());
-- → 문제없이 동작. 필터는 마스킹 전 원본 값으로 평가되고,
-- 결과로 반환되는 customer_email 값만 마스킹됨
-- 서브쿼리가 들어간 정책이 마스킹·CLS 제한 컬럼을 참조
CREATE ROW ACCESS POLICY allow_listed
ON `project.dataset.orders`
GRANT TO ("group:analysts@")
FILTER USING (customer_email IN (SELECT email FROM `project.dataset.allowlist`));
-- → 조회자에게 customer_email의 fineGrainedReader가 없으면 에러일반 정책과 서브쿼리 정책의 동작이 다르다는 비대칭 자체가 함정입니다. 공식 문서의 예시로, location = "US" 정책이 걸려 있고 location이 마스킹된 경우 사용자는 US 행을 정상 조회하되 location 값만 마스킹된 채 받습니다. 마스킹이 행 필터링 위에(on top of) 얹히기 때문입니다. 반면 서브쿼리 정책은 참조 컬럼에 unmasked 접근을 요구합니다. 정책을 단순 비교식으로 유지할 수 있으면 이 비대칭을 신경 쓸 일이 없으니, 서브쿼리 정책이 꼭 필요할 때만 참조 컬럼의 권한을 함께 챙기면 됩니다.
겹침 사고 B: authorized view + RLS 있는 base
앞서 §6-2에서 봤지만 다시 짚습니다. 위임된 view라도 RLS는 base 테이블의 정책이 그대로 평가됩니다. authorized view로 사용자에게 view를 노출할 때, 반드시 base의 RLS 정책에도 그 사용자(또는 그룹)를 grantee로 추가해야 합니다. 놓치면 view가 에러 없이 0행만 반환합니다.
겹침 사고 C: MV on RLS base
§6-2에서 봤듯 RLS 있는 base 위 MV는 가속 대상에서 빠집니다. BI Engine도 마찬가지입니다. 대시보드 성능을 위해 MV·BI Engine을 얹어야 한다면 RLS 위치를 다시 검토(예: view 층으로 이동)하는 편이 나은 경우가 많습니다.
**겹침 사고 D: SELECT *가 policy tag를 만나면 파이프라인이 깨진다**
dbt·Airflow 같은 파이프라인이 관행적으로 SELECT *를 쓰면, policy tag가 새로 붙는 순간 그 downstream 잡이 전부 실패합니다(§4-3). 미리 파이프라인의 SELECT *를 명시적 컬럼 나열로 리팩터링해 두거나, tag가 붙을 컬럼에 대해 파이프라인 계정에 masked reader role을 부여해 마스킹된 값이라도 통과하게 만들어 두면 막을 수 있습니다.
8. 실무 모니터링과 함정
8-1. INFORMATION_SCHEMA.OBJECT_PRIVILEGES로 권한 감사
특정 dataset·table에 어떤 principal이 어떤 role을 가지고 있는지 감사 쿼리로 뽑을 수 있습니다. 단, 이 뷰는 조회 조건이 꽤 까다롭습니다. WHERE 절로 dataset이나 table 하나를 특정해야만 조회됩니다. dataset 자체의 binding은 object_name에 dataset 이름을, 특정 table의 binding은 object_schema(dataset)와 object_name(table)을 함께 지정합니다.
-- dataset 자체의 binding
SELECT object_name, privilege_type, grantee
FROM `region-us`.INFORMATION_SCHEMA.OBJECT_PRIVILEGES
WHERE object_name = 'my_dataset';
-- 특정 table의 binding
SELECT object_name, privilege_type, grantee
FROM `region-us`.INFORMATION_SCHEMA.OBJECT_PRIVILEGES
WHERE object_schema = 'my_dataset' AND object_name = 'my_table';WHERE 없이 실행하면 이런 에러로 거부됩니다.
All OBJECT_PRIVILEGES queries must have a WHERE clause
which specifies 'object_name =' at the root levelobject_name = 등호 조건이 강제라 IN이나 LIKE로 여러 객체를 한 번에 훑을 수 없습니다. 이 제약이 "권한 감사" 용도에 직접 걸립니다. 한 dataset의 전체 테이블 권한을 나열하려면 INFORMATION_SCHEMA.TABLES로 테이블 목록을 먼저 뽑고, 테이블마다 이 뷰를 조회하는 2단 루프를 돌아야 합니다.
또 하나, 이 뷰는 객체에 직접 설정된(explicit) binding만 보여 줍니다. 상위 리소스에서 상속된 binding은 안 나옵니다. dataset 레벨에서 role을 받은 사용자는 그 안의 테이블 권한을 상속받지만 테이블 쪽 조회 결과에는 나타나지 않으니, 전체 권한을 감사하려면 상위 레벨의 binding도 함께 조회해야 합니다. Row-Level Access Policy와 Data Policy(masking)도 이 뷰에 안 나옵니다.
8-2. RLS 정책 조회와 감사 로그의 사각지대
RLS 정책 목록은 SQL로 조회할 수 없습니다. BigQuery INFORMATION_SCHEMA에 row access policy용 뷰가 없기 때문입니다(접근 제어 관련 뷰는 OBJECT_PRIVILEGES뿐). 정책은 콘솔에서 테이블 상세의 View row access policies 버튼으로 보거나, bq CLI 또는 REST API의 rowAccessPolicies.list로 뽑아야 합니다.
# 특정 테이블의 RLS 정책 목록
bq ls --row_access_policies my_dataset.my_table정작 문제는 감사 로그 쪽입니다. 정책의 생성·삭제는 Cloud Audit Log에 남지만, 로그 엔트리 안에 담기는 정책 본문 중 `filter_expression`과 `grantee_list`는 민감 정보를 담을 수 있어 로그에서 제외됩니다. 그리고 정책의 조회·열람(list/get) 이벤트는 아예 감사 로그에 안 남습니다.
감사 관점에서 정리하면 이런 그림이 됩니다.
- 정책을 누가 언제 만들고 지웠는지는 남습니다
- 그 정책의 필터·grantee가 뭐였는지는 로그만으로는 재구성이 안 됩니다 (현재 값은
bq ls --row_access_policies나 API로만 확인 가능) - 누가 정책 목록을 조회했는지는 아예 기록되지 않습니다
정책 이력을 완전히 남기려면 별도로 정책 스냅샷을 주기적으로 저장하는 파이프라인을 두는 게 안전합니다. SQL 뷰가 없으니 rowAccessPolicies.list API를 주기 호출(Cloud Scheduler + Functions 등)해 결과를 테이블에 append해 두고, 변경분 diff를 감사 자료로 씁니다.
8-3. RLS 필터는 파티션·클러스터 pruning에 참여 안 함
RLS 필터 표현식 자체는 파티션 pruning이나 클러스터 filter 최적화에 참여하지 않습니다. 공식 문서는 이 제약이 메인 쿼리 실행을 크게 느리게 하지는 않는다고 명시하지만, RLS 조건 자체가 pruning에 기여를 못 한다는 점은 그대로 남습니다.
-- 사용자가 이렇게 조회
SELECT * FROM sales WHERE order_date = '2026-08-24' AND region = 'APAC';
-- 그런데 RLS가 이렇게 걸려 있으면
CREATE ROW ACCESS POLICY apac_only
ON sales GRANT TO ("group:analysts@")
FILTER USING (region = 'APAC');
-- order_date 파티션 필터는 사용자 쿼리에 있으니 pruning 정상 동작.
-- RLS의 region 조건은 pruning에 못 씀 → 사용자 쿼리에 region 필터가 없으면
-- APAC이 아닌 파티션·클러스터도 함께 스캔될 수 있음.핵심은 사용자 쿼리 쪽에 파티션·클러스터 필터를 명시적으로 유지하는 것입니다. 사용자 쿼리에 필터가 있으면 pruning이 정상 동작하고, RLS는 그 위에서 각 사용자에게 보이는 행만 걸러 냅니다. RLS 자체가 필터라고 안심하고 사용자 쿼리에서 조건을 뺐다가 스캔량이 늘어나는 케이스가 대표적입니다. 안전판으로 maximum_bytes_billed 상한을 함께 걸어 두는 것도 좋습니다(8편 §10-3 참조).
8-4. Result cache와 RLS
BigQuery의 query result cache는 기본적으로 사용자별·프로젝트별로 유지됩니다(Enterprise 이상에서는 같은 프로젝트 내 cross-user 캐시 공유 예외는 있습니다). 그런데 RLS 테이블은 아예 예외로 빠집니다. RLS로 보호되는 테이블을 조회한 결과는 캐시되지 않습니다. 공식 문서의 캐시 예외 목록에 명시된 규칙이라, "조건에 따라 안 될 수도 있다"가 아니라 항상 cache miss입니다. CLS(policy tag)·masking이 걸린 테이블은 "캐시되지 않을 수 있음(might not)"으로 한 단계 약하게 구분되어 있습니다.
대시보드 뒤에 붙는 쿼리가 RLS 테이블을 참조한다면 캐시 이점은 처음부터 없다고 잡는 게 맞습니다. 실제 상태는 INFORMATION_SCHEMA.JOBS의 cache_hit 컬럼으로 관측할 수 있습니다.
마무리
BigQuery의 접근 제어는 IAM 하나로 끝나지 않고, RLS·CLS·Masking·Authorized View가 각각 다른 관점으로 얹혀 있는 구조입니다. 그래서 "왜 이 사용자에게 이 데이터가 안 보이지"를 추적할 때는 한 층씩 벗겨 봐야 합니다. IAM Deny → RLS grantee → policy tag → 마스킹 룰 순서로 확인하면 대부분 어딘가에서 걸립니다.
특히 놓치기 쉬운 건 문서 곳곳에 흩어진 세부 규칙들입니다. authorized view는 base IAM만 우회할 뿐 RLS는 그대로 평가하고, SELECT *는 policy tag 붙은 컬럼 하나에 파이프라인째 깨지며, RLS 필터는 파티션 pruning에 아무 기여도 하지 않습니다. 각각 다른 문서에 적혀 있어서 하나씩 걸려 보기 전까지는 잘 모릅니다.
다음 편에서는 시리즈의 마지막 주제로 리전, 복제본, 재해복구를 다룹니다. cross-region replication과 backup·restore의 실제 동작, 그리고 DR(disaster recovery) 관점에서 BigQuery 설계가 어떻게 달라져야 하는지를 짚어보겠습니다.