Skip to content

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 적용

security_definer (기본값)

View owner

❌ 우회됨

security_invoker = true

호출자 (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

See also