Supabase RLS 테스트는 정책 설명이 아니라 권한 행렬 증명이다
“RLS 켰으니 안전하다”와 “이 사용자가 이 행을 못 본다는 증거가 있다”는 어떻게 다를까?
Supabase RLS 테스트는 정책 설명이 아니라 권한 행렬 증명이다
- 카테고리: web_app_dev
- 예상 읽기 시간: 10분
- 오늘의 질문: “RLS 켰으니 안전하다”와 “이 사용자가 이 행을 못 본다는 증거가 있다”는 어떻게 다를까?
- 핵심 출처:
- Supabase Developer Update - July 2026 - 게시일: 2026-07-09, 확인일: 2026-07-27
- Supabase Changelog: Feature Preview: RLS Tester - 게시일: 2026-04-24, 확인일: 2026-07-27
- Supabase Docs: Testing Overview - 문서, 확인일: 2026-07-27
- Supabase Docs: Row Level Security - 문서, 확인일: 2026-07-27
- unitautogen/rlsautotest README - GitHub README, 확인일: 2026-07-27
1. 왜 지금 봐야 하나
Supabase의 2026년 7월 Developer Update에는 작은 항목처럼 보이지만 웹앱 개발자에게 중요한 신호가 하나 들어 있다. rlsautotest라는 도구가 “Made with Supabase”로 소개되었고, 설명은 간단하다. Supabase RLS 정책으로부터 pgTAP 테스트와 seed data를 생성해, 테이블과 identity별로 누가 어떤 행을 읽거나 쓸 수 있는지 증명한다는 것이다.
몇 달 전인 2026년 4월에는 Supabase Studio의 RLS Tester가 feature preview로 공개됐다. 이 UI는 role impersonation을 기반으로 “로그인 안 한 사용자 / 로그인한 사용자”처럼 특정 사용자로 SQL을 실행하고, 어떤 RLS policy가 평가되는지 보여주는 도구다. Supabase는 RLS 정책의 correctness를 검증하는 일이 gap이었다고 설명한다. 단, preview 시점에는 단순화를 위해 SELECT query만 지원하고, INSERT, UPDATE, DELETE는 trigger나 외부 요청 같은 side effect를 일으킬 수 있어 제외했다.
이 흐름을 하나로 묶으면 메시지가 분명하다. 웹앱에서 RLS는 더 이상 “대시보드에서 토글 켜고 policy 몇 줄 쓰는 설정”으로 끝나지 않는다. AI 코딩 도구가 Supabase schema, Auth, API route를 빠르게 만들어주는 시대일수록, 권한 정책은 코드 리뷰 감이 아니라 반복 가능한 테스트 증거로 다뤄야 한다.
오늘의 핵심 문장: RLS 테스트의 목적은 policy SQL이 예쁘게 보이는지 확인하는 것이 아니라, identity × table × command × row의 권한 행렬이 실제 데이터에서 맞는지 증명하는 것이다.
2. 핵심 개념
Supabase RLS 문서는 Row Level Security를 “Postgres가 쿼리에 자동으로 붙이는 WHERE 조건”처럼 생각할 수 있다고 설명한다. 예를 들어 이런 policy가 있다.
create policy "Individuals can view their own todos."
on todos for select
using ( (select auth.uid()) = user_id );
개발자가 select * from todos를 실행해도, 실제 효과는 다음에 가깝다.
select *
from todos
where auth.uid() = todos.user_id;
이 모델은 이해하기 쉽지만, 실무에서는 세 가지 착시가 생긴다.
첫째, RLS enabled와 policy correctness는 다르다. RLS를 켜면 기본적으로 API에서 데이터가 보이지 않지만, 그 다음에 만든 policy가 너무 넓으면 다른 사용자의 row가 열릴 수 있다. 반대로 policy가 너무 좁으면 정상 사용자도 기능을 쓰지 못한다.
둘째, SELECT 테스트만으로 쓰기 권한을 설명할 수 없다. Supabase 문서는 INSERT policy에는 WITH CHECK를 쓰고, UPDATE policy에는 기존 row를 고르는 USING과 새 row가 만족해야 하는 WITH CHECK가 함께 필요하다고 설명한다. “자기 row만 읽을 수 있다”와 “다른 사람의 user_id를 가진 row를 만들 수 없다”는 다른 명제다.
셋째, 인증 상태는 SQL의 null과 만난다. Supabase 문서는 인증되지 않은 요청에서는 auth.uid()가 null을 반환한다고 설명한다. 그래서 USING (auth.uid() = user_id)는 unauthenticated user에게 조용히 false가 된다. 의도를 명확히 하려면 auth.uid() IS NOT NULL AND auth.uid() = user_id처럼 쓰는 편이 낫다.
따라서 좋은 RLS 테스트는 policy 이름이 있는지 보는 테스트가 아니라 다음 질문에 답해야 한다.
| 축 | 예시 질문 |
|---|---|
| identity | anon, authenticated user A, user B, org admin, org member가 각각 무엇을 할 수 있나? |
| table | profiles, projects, org_members, invoices마다 RLS가 켜져 있나? |
| command | SELECT, INSERT, UPDATE, DELETE가 따로 검증되나? |
| row | 내 row, 남의 row, 다른 tenant row, deleted row, premium row가 구분되나? |
| result | 허용되어야 하는 것은 실제로 허용되고, 금지되어야 하는 것은 실제로 막히나? |
이게 “권한 행렬”이다. 웹앱의 권한 설계는 결국 이 표를 데이터베이스가 강제하는지 확인하는 일이다.
3. 최신 이슈와 연결
이번에 확인한 공식/프로젝트 출처들은 서로 다른 레벨의 테스트를 보여준다.
Supabase Studio RLS Tester: 디버깅용 현미경
Supabase RLS Tester는 dashboard에서 특정 사용자로 SQL을 실행하고 어떤 policy가 평가되는지 보는 데 초점이 있다. SQL뿐 아니라 client library code를 AI Assistant가 SQL로 추론하는 partial support도 언급된다. 예를 들어 client.from('colors').select('*') 같은 코드를 SQL로 바꿔 테스트할 수 있게 하려는 방향이다.
하지만 Supabase는 동시에 주의점을 남긴다. AI가 SQL을 추론하므로 결과를 검증해야 하고, preview 시점에는 SELECT만 지원한다. 즉 이 도구는 “왜 이 row가 보이지 않지?”를 빠르게 확인하는 현미경에 가깝다. 매 PR마다 전체 권한 matrix를 검증하는 CI gate로 바로 보기에는 아직 범위가 제한적이다.
Supabase pgTAP 문서: 테스트를 코드로 남기는 기본기
Supabase Testing Overview는 RLS policy를 pgTAP으로 테스트하는 예시를 제공한다. 핵심 흐름은 명확하다.
begin;
select plan(4);
set local role authenticated;
set local request.jwt.claim.sub = '123e4567-e89b-12d3-a456-426614174000';
select results_eq(
'select count(*) from todos',
ARRAY[2::bigint],
'User 1 should only see their 2 todos'
);
select * from finish();
rollback;
여기서 중요한 것은 begin / rollback이다. 데이터베이스 단위 테스트는 실제 role과 JWT claim을 세팅하고, seed data를 넣고, 실제 쿼리를 실행한 뒤 rollback으로 격리할 수 있다. 앱 레벨 E2E 테스트보다 빠르고 deterministic한 권한 테스트를 만들 수 있다.
rlsautotest: 수작업 테스트를 생성 가능한 산출물로 바꾸기
rlsautotest README는 이 문제를 한 단계 더 밀고 간다. 도구가 database catalog에서 RLS policy를 읽고, owner, other user, anon, role-holder, tenant 같은 identity와 seed data를 생성해 pgTAP suite, access matrix report, CI gate를 만든다고 설명한다.
특히 눈에 띄는 표현은 “false-passing test를 만들지 않는다”는 방향이다. 검증할 수 없는 것은 초록색 체크로 속이지 않고 표시한다는 의미다. README는 --report나 --html 같은 probing command가 실제 SELECT/INSERT/UPDATE/DELETE를 실행하므로, 반드시 disposable copy에 연결하라고 경고한다. transaction으로 rollback되더라도 trigger, lock, sequence 같은 부작용은 발생할 수 있기 때문이다.
이 세 흐름은 서로 대체재라기보다 계층이다.
| 레벨 | 도구 | 좋은 사용처 |
|---|---|---|
| 수동 디버깅 | Supabase Studio RLS Tester | 특정 사용자가 왜 특정 row를 보거나 못 보는지 즉시 확인 |
| 명시적 테스트 | pgTAP / supabase test db | 핵심 policy를 직접 테스트 코드로 고정 |
| 생성형 검증 | rlsautotest | policy와 schema 변화에서 누락된 행렬, RLS-off table, leak를 CI에서 발견 |
4. 개발자 관점 해석
웹앱 팀이 RLS를 쓰는 이유는 클라이언트에서 Supabase API를 직접 호출해도 데이터베이스가 마지막 권한 경계가 되어주기 때문이다. 이 장점은 AI-assisted 개발 흐름에서 더 중요해진다. 에이전트가 화면, query, server action, migration을 빠르게 만들수록 “권한 체크를 어디에 넣었는지”가 사람 머릿속에서 쉽게 흩어진다.
예를 들어 아래 요구사항을 보자.
사용자는 자신이 속한 org의 project만 볼 수 있다. org admin은 member를 초대할 수 있다. 일반 member는 자기 task만 수정할 수 있다. archived project의 task는 아무도 수정할 수 없다.
AI 도구에게 이 문장을 주면 그럴듯한 RLS policy를 만들 수 있다. 문제는 그다음이다.
SELECT는 맞지만UPDATE WITH CHECK가 빠졌는가?- admin policy와 member policy가 permissive하게 OR 결합되어 예상보다 넓어졌는가?
org_id대신user_id만 비교해서 다른 tenant row가 열렸는가?- service role을 쓰는 Edge Function이나 SECURITY DEFINER function이 RLS를 우회하는가?
- 새 테이블이 migration으로 추가됐는데 RLS enable이 빠졌는가?
이런 버그는 화면에서 “내 계정으로 잘 된다”만 테스트하면 잘 보이지 않는다. 특히 권한 버그는 성공 케이스보다 실패 케이스가 중요하다. “내가 볼 수 있다”보다 “남이 못 본다”를 증명해야 한다.
그래서 RLS 테스트는 API 테스트와 다른 습관이 필요하다.
-
허용 테스트와 금지 테스트를 쌍으로 둔다
- owner can read own row
- other user cannot read owner row
- member can update own draft task
- member cannot update archived task
-
읽기와 쓰기를 분리한다
SELECT가 통과해도INSERT,UPDATE,DELETE는 따로 본다.- 특히
UPDATE는 기존 row를 만질 수 있는 조건과, 변경 후 row가 만족해야 하는 조건을 나눠 본다.
-
identity fixture를 도메인 언어로 만든다
user_a,user_b보다org_owner,org_admin,same_org_member,other_org_member,anonymous처럼 권한 의미가 드러나게 만든다.
-
AI가 만든 policy는 AI가 만든 테스트만 믿지 않는다
- AI에게 테스트 초안을 만들게 할 수는 있다.
- 하지만 “실제 row가 있는가”, “negative case가 있는가”, “쓰기 command가 있는가”, “RLS-off table을 잡는가”는 사람이 체크리스트로 검토해야 한다.
5. 내 프로젝트에 적용할 체크포인트
Supabase나 PostgreSQL RLS를 쓰는 프로젝트라면 아래 순서로 점검해보자.
1) exposed schema의 테이블 inventory를 만든다
public schema 또는 API로 노출되는 schema의 테이블을 나열한다.
| table | exposed? | RLS enabled? | anon | authenticated | service/server path |
| --- | --- | --- | --- | --- | --- |
| profiles | yes | yes | read public subset | own read/update | admin sync |
| projects | yes | yes | no | org member read | billing worker |
| org_members | yes | yes | no | org member read | invite function |
| audit_logs | no | yes/no | no | no | server only |
목표는 “policy가 있나?”가 아니라 “이 테이블을 어떤 role이 어떤 경로로 만지나?”를 보는 것이다.
2) command별 policy intent를 문장으로 쓴다
SQL부터 쓰지 말고, 먼저 행렬을 문장으로 고정한다.
projects
- SELECT: 같은 org member만 읽을 수 있다.
- INSERT: org owner/admin만 만들 수 있고, new row의 org_id는 사용자가 속한 org여야 한다.
- UPDATE: org owner/admin만 수정할 수 있다. archived=true로 바꿀 수 있는 것은 owner뿐이다.
- DELETE: owner만 삭제할 수 있다.
이 문장이 있어야 테스트가 “구현을 확인하는 테스트”가 아니라 “요구사항을 검증하는 테스트”가 된다.
3) 최소 pgTAP smoke test를 추가한다
처음부터 모든 policy를 완벽히 테스트하려 하지 말고, 가장 위험한 테이블 하나부터 시작하자.
begin;
select plan(2);
-- same org user
set local role authenticated;
select set_config('request.jwt.claim.sub', '00000000-0000-0000-0000-000000000001', true);
select results_eq(
$$ select count(*) from projects where org_id = '11111111-1111-1111-1111-111111111111' $$,
ARRAY[1::bigint],
'same org member can see one project'
);
-- other org user
select set_config('request.jwt.claim.sub', '00000000-0000-0000-0000-000000000002', true);
select results_eq(
$$ select count(*) from projects where org_id = '11111111-1111-1111-1111-111111111111' $$,
ARRAY[0::bigint],
'other org member cannot see this project'
);
select * from finish();
rollback;
실제 프로젝트에서는 Supabase testing docs와 test helper를 맞춰 써야 하지만, 감각은 같다. role과 claim을 바꾸며 같은 쿼리의 결과가 달라지는지 본다.
4) 생성형 도구는 disposable DB에서만 돌린다
rlsautotest 같은 도구를 검토한다면 README의 경고를 그대로 따른다.
- production DB에 직접 연결하지 않는다.
- migration을 적용한 local DB, branch DB, CI throwaway DB를 사용한다.
--report,--html,--emit은 실제 query와 mutation probe를 실행할 수 있음을 팀에 공유한다.- 생성된 pgTAP suite는 commit해서 review 가능한 산출물로 둔다.
5) RLS 우회 표면도 같이 본다
RLS policy가 맞아도 다음 경로가 권한 모델을 우회할 수 있다.
- 브라우저에 service role key가 들어간 경우
- server route가 사용자 검증 없이 service role로 모든 row를 반환하는 경우
SECURITY DEFINERfunction이 exposed schema에서 호출 가능한 경우- view/function owner 권한 때문에 RLS가 기대와 다르게 적용되는 경우
- 새 테이블 migration에
enable row level security가 빠진 경우
즉 RLS 테스트는 policy SQL만 보는 것이 아니라 “데이터에 닿는 모든 경로”를 줄이는 작업과 함께 가야 한다.
6. 오늘 10분 액션
오늘은 새 도구를 설치하지 않아도 된다. 10분만 써서 권한 행렬 초안을 만들자.
- Supabase에서 가장 민감한 테이블 하나를 고른다. 예:
projects,invoices,org_members,messages. - 그 테이블에 대해 identity 4개를 적는다.
anonymousownersame_org_memberother_org_member
- command 4개를 열로 만든다.
SELECTINSERTUPDATEDELETE
- 각 칸에 “허용 / 거부 / 조건부 허용”을 적는다.
- 가장 위험한 negative case 하나를 테스트 후보로 바꾼다.
예시는 이렇게 시작할 수 있다.
| identity | SELECT | INSERT | UPDATE | DELETE |
| --- | --- | --- | --- | --- |
| anonymous | deny | deny | deny | deny |
| owner | same org rows | own org only | same org rows | same org rows |
| same_org_member | same org rows | deny | own draft task only | deny |
| other_org_member | deny | deny | deny | deny |
그다음 AI 코딩 도구에 이렇게 지시하자.
이 권한 행렬을 기준으로 Supabase pgTAP RLS 테스트 초안을 만들어줘. 허용 케이스와 금지 케이스를 모두 포함하고, SELECT뿐 아니라 INSERT/UPDATE의 WITH CHECK 실패 케이스도 포함해줘. 테스트 데이터는 owner, same_org_member, other_org_member가 실제로 다른 org row를 갖도록 만들어줘.
AI가 만든 결과를 그대로 믿지 말고, 오늘 배운 체크포인트로 검토하면 된다. 테스트가 실제 row를 만들고 있는지, negative case가 빈 테이블 때문에 우연히 통과하는 것은 아닌지, production DB를 건드리지 않는지 확인하라.
7. 더 볼 자료
- Supabase Row Level Security: RLS enabled, policy,
auth.uid(),USING,WITH CHECK, role 지정의 기본 문서 - Supabase Testing Overview: pgTAP, application-level testing, CI testing, RLS 테스트 예시
- Supabase pgTAP extension docs: policy presence와 query result를 검증하는 pgTAP 함수들
- Feature Preview: RLS Tester: Studio에서 role impersonation으로 RLS를 디버깅하는 preview 기능
- rlsautotest README: RLS policy에서 pgTAP suite, seed data, access matrix, CI gate를 생성하는 beta 도구
중복 회피 메모
로컬 content/generated와 Supabase 최근 web_app_dev 글을 확인했다. 최근 글은 Supabase Realtime binary payload 호환성, Hono null-prototype request parsing, Next.js Partial Prefetching/App Shell, Supabase Pipelines/CDC, Next.js 월간 보안 릴리스, SWC WASM/lockfile, React Router Web Streams, TypeScript 7, pg_graphql introspection, PostgREST JWT kid, Supabase Auth URL 라우팅을 다뤘다. 이번 글은 Supabase를 다시 다루지만 Realtime, CDC, Auth callback, JWT 검증이 아니라 RLS 정책을 identity × table × command × row 권한 행렬로 테스트하고, Supabase RLS Tester·pgTAP·rlsautotest를 어떻게 서로 다른 검증 레이어로 볼지에 집중한다.
핵심 출처
로그인하면 이 글을 북마크하고, 나만 보는 한 줄 메모를 남길 수 있어요.
댓글 0
최신순 ▾혹시 이 글을 읽는 동료 개발자가 있다면, GitHub으로 로그인하고 한 줄 흔적을 남겨줘요. (스팸 방지용 로그인이에요)