PostgreSQL:View
security_invoker
PostgreSQL 15+에서 security_invoker = true 옵션을 사용하면, View를 호출하는 사용자의 권한으로 실행되어 원본 테이블의 RLS 정책이 그대로 적용된다.
-- 새 View 생성 시
CREATE VIEW my_view
WITH (security_invoker = true)
AS
SELECT * FROM my_table;
-- 기존 View에 적용
ALTER VIEW my_view SET (security_invoker = true);
| 옵션 | 실행 권한 | RLS 적용 |
| | View owner | ❌ 우회됨 |
| | 호출자 (caller) | ✅ 적용됨 |
주의사항:
- View 자체에
ENABLE ROW LEVEL SECURITY를 직접 걸 수는 없다. 항상 원본 테이블의 RLS에 의존한다. - 여러 테이블을 JOIN하는 View의 경우, 각 테이블의 RLS가 모두 적용된다.
- Supabase Dashboard에서 View를 만들 때 이 옵션이 빠지기 쉬우므로, SQL Editor에서 직접 작성하는 것이 안전하다.
View와 RLS
PostgreSQL에서 View는 기본적으로 View owner의 권한(security_definer)으로 실행되기 때문에, 원본 테이블에 RLS가 걸려 있어도 View를 통해 조회하면 RLS 정책이 우회된다. #security_invoker 항목 참조.
예제: my_orgs View
현재 로그인한 사용자가 속한 조직 목록을 조회하는 View 예제:
-- VIEW: my_orgs
-- Current user's organization list with plan, role, and member count
CREATE OR REPLACE VIEW public.my_orgs
WITH (security_invoker = true)
AS
SELECT
o.id,
o.icon,
o.name,
o.slug,
COALESCE(s.plan_code, 'free'::public.plan_code) AS plan,
r.name AS role,
(SELECT COUNT(*)::int FROM public.org_members m WHERE m.org_id = o.id) AS member_count,
o.created_at
FROM public.orgs o
JOIN public.org_members om ON om.org_id = o.id
JOIN public.org_roles r ON r.id = om.role_id
LEFT JOIN public.subscriptions s ON s.org_id = o.id
AND s.status IN ('active', 'trialing')
WHERE om.user_id = auth.uid();
-- Grant access to authenticated users
GRANT SELECT ON public.my_orgs TO authenticated;
설계 포인트:
-
security_invoker = true— 원본 테이블들의 RLS가 정상 적용됨 -
WHERE om.user_id = auth.uid()— View 레벨에서도 현재 사용자만 필터링 (이중 보안) -
COALESCE(..., 'free')— 구독이 없는 조직은 free 플랜으로 기본값 처리 - 구독 상태 필터 —
active,trialing만 JOIN하여 만료/취소된 구독 제외
성능 고려:
-
member_count서브쿼리는 행마다 실행되는 correlated subquery이나, 사용자가 속한 조직 수가 일반적으로 적으므로 실용적으로 충분하다. -
security_invoker로 인해 각 테이블의 RLS 조건과 View의 WHERE 절이 이중으로 걸리므로, 성능 이슈 발생 시EXPLAIN ANALYZE로 쿼리 플랜을 확인할 것.
구독 중복 방어: 한 조직에 active/trialing 구독이 동시에 존재할 가능성이 있다면 LATERAL 서브쿼리로 최신 1건만 가져오는 방식을 고려:
LEFT JOIN LATERAL (
SELECT plan_code
FROM public.subscriptions
WHERE org_id = o.id AND status IN ('active', 'trialing')
ORDER BY created_at DESC
LIMIT 1
) s ON true