기본 패턴
-- 1. RLS 활성화
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- 2. 정책 생성: 자신의 게시글만 읽기/쓰기
CREATE POLICY posts_owner
ON posts
USING (author_id = current_user_id()) -- SELECT/UPDATE/DELETE 필터
WITH CHECK (author_id = current_user_id()); -- INSERT/UPDATE 검증
-- 3. 세션 컨텍스트 설정 후 쿼리 — 자동으로 WHERE 절이 삽입된다
SET app.current_user_id = '42';
SELECT * FROM posts;
-- 실제 실행: SELECT * FROM posts WHERE author_id = 42설명
RLS는 WHERE 절을 모든 쿼리에 자동으로 삽입하는 방식으로 동작한다
RLS를 활성화하면 PostgreSQL은 해당 테이블에 대한 모든 쿼리(SELECT, INSERT, UPDATE, DELETE)를 실행하기 전에 활성화된 정책의 표현식을 WHERE 절로 자동 삽입한다. 애플리케이션 코드가 WHERE 조건을 빠뜨려도 데이터베이스 레벨에서 보호된다.
-- 테이블 및 RLS 설정
CREATE TABLE documents (
id bigserial PRIMARY KEY,
owner_id bigint NOT NULL,
dept_id bigint NOT NULL,
content text,
is_public boolean DEFAULT false
);
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
-- 정책 1: 공개 문서는 누구나 볼 수 있다
CREATE POLICY doc_public_read
ON documents
FOR SELECT
USING (is_public = true);
-- 정책 2: 소유자는 자신의 문서를 모두 볼 수 있다
CREATE POLICY doc_owner_all
ON documents
FOR ALL
USING (owner_id = current_setting('app.user_id')::bigint);
-- 여러 정책은 OR로 결합된다 (PERMISSIVE 기본값)
-- 실제 SELECT: WHERE (is_public = true) OR (owner_id = ?)
-- 정책 간 관계: RESTRICTIVE는 AND로 결합
CREATE POLICY doc_dept_restrict
ON documents
AS RESTRICTIVE
FOR SELECT
USING (dept_id = current_setting('app.dept_id')::bigint);
-- 실제 SELECT: WHERE ((is_public OR owner_id=?) AND dept_id=?)PERMISSIVE(기본값) 정책들은 OR로 결합되어 하나라도 통과하면 접근이 허용된다. RESTRICTIVE 정책은 AND로 결합되어 반드시 통과해야 한다. 두 종류를 혼합하면 (PERMISSIVE 조건들) AND (RESTRICTIVE 조건들)이 된다.
USING은 읽기 필터, WITH CHECK는 쓰기 필터다
USING은 기존 행을 볼 수 있는지 결정한다. SELECT뿐 아니라 UPDATE와 DELETE의 대상 행 필터에도 적용된다. WITH CHECK는 새로 쓰려는 행이 정책을 만족하는지 검증한다. INSERT와 UPDATE의 결과 행에 적용된다.
-- USING만 있는 정책: SELECT/UPDATE/DELETE 필터로만 동작
-- INSERT/UPDATE의 결과 행 검증은 WITH CHECK가 USING과 동일하게 적용
CREATE POLICY orders_read
ON orders
FOR SELECT
USING (customer_id = current_setting('app.user_id')::bigint);
-- WITH CHECK만 있는 정책: INSERT/UPDATE 결과 검증
CREATE POLICY orders_insert
ON orders
FOR INSERT
WITH CHECK (customer_id = current_setting('app.user_id')::bigint);
-- 두 절을 모두 사용하는 패턴 (UPDATE)
CREATE POLICY orders_update
ON orders
FOR UPDATE
USING (customer_id = current_setting('app.user_id')::bigint) -- 수정 대상 필터
WITH CHECK (customer_id = current_setting('app.user_id')::bigint); -- 수정 후 검증
-- 잘못된 예: customer_id를 다른 사용자로 변경 시도
-- USING 통과 (기존 행이 내 행) → WITH CHECK 실패 (결과 행이 다른 사람 소유)
UPDATE orders SET customer_id = 999 WHERE id = 1;
-- ERROR: new row violates row-level security policy for table "orders"WITH CHECK를 생략하면 USING 표현식이 쓰기 검증에도 동일하게 사용된다. INSERT는 기존 행이 없으므로 USING은 적용되지 않고 WITH CHECK만 유효하다.
current_user / current_setting으로 세션 컨텍스트를 정책에 활용하는 패턴
RLS 정책에서 세션 컨텍스트를 전달하는 가장 흔한 방법은 SET LOCAL로 트랜잭션 범위의 설정값을 지정하는 것이다. 애플리케이션 서버가 요청마다 사용자 ID를 세션 변수로 설정하면, 정책 표현식에서 current_setting()으로 읽어 동적 필터를 구성한다.
-- 애플리케이션에서 트랜잭션 시작 시 컨텍스트 설정
BEGIN;
SET LOCAL app.user_id = '42';
SET LOCAL app.org_id = '7';
SET LOCAL app.role = 'member';
-- 정책에서 컨텍스트 참조
CREATE POLICY resource_policy ON resources
USING (
org_id = current_setting('app.org_id')::bigint
AND (
owner_id = current_setting('app.user_id')::bigint
OR current_setting('app.role') = 'admin'
)
);
-- missing_ok 파라미터: 설정이 없을 때 NULL 반환 (오류 방지)
CREATE POLICY safe_policy ON items
USING (
owner_id = current_setting('app.user_id', true)::bigint
);
-- Supabase/PostgREST 패턴: auth.uid() 함수 사용
CREATE POLICY supabase_pattern ON profiles
USING (id = auth.uid());
COMMIT;SET(세션 전체)보다 SET LOCAL(트랜잭션 범위)이 안전하다. 커넥션 풀 환경에서 세션이 재사용될 때 이전 사용자의 컨텍스트가 남아있는 오염을 방지한다.
테이블 소유자와 SUPERUSER는 RLS를 우회한다 — BYPASSRLS 역할 부여 위험
테이블 소유자(owner)와 SUPERUSER 권한을 가진 역할은 RLS 정책을 무시하고 모든 행에 접근할 수 있다. 이를 의도적으로 활용하려면 FORCE ROW LEVEL SECURITY를 사용한다. BYPASSRLS 역할은 비슈퍼유저에게도 RLS 우회를 허용하므로 부여에 주의해야 한다.
-- 테이블 소유자도 RLS를 따르게 강제
ALTER TABLE sensitive_data FORCE ROW LEVEL SECURITY;
-- BYPASSRLS 역할 확인
SELECT rolname, rolbypassrls FROM pg_roles WHERE rolbypassrls = true;
-- 애플리케이션 역할에는 BYPASSRLS를 주지 않는다
CREATE ROLE app_user LOGIN PASSWORD '...';
-- GRANT BYPASSRLS TO app_user; ← 절대 하지 말 것
-- 관리용 역할만 BYPASSRLS 허용
CREATE ROLE admin_user BYPASSRLS LOGIN;
-- RLS 정책 목록 확인
SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual, with_check
FROM pg_policies
WHERE tablename = 'documents';빠른 정리
| 상황 | 적합한 선택 |
|---|---|
| 사용자별 행 접근을 제한해야 한다 | ENABLE ROW LEVEL SECURITY + CREATE POLICY |
| 읽기 접근만 제어한다 | USING 절만 사용 |
| 쓰기 결과 행을 검증해야 한다 | WITH CHECK 절 추가 |
| 여러 조건 중 하나라도 통과하면 된다 | PERMISSIVE 정책 (기본값, OR 결합) |
| 모든 조건을 반드시 통과해야 한다 | RESTRICTIVE 정책 (AND 결합) |
| 소유자도 RLS를 따르게 해야 한다 | FORCE ROW LEVEL SECURITY |
| 트랜잭션 범위 컨텍스트가 필요하다 | SET LOCAL app.xxx = ... |
주의할 점
ENABLE ROW LEVEL SECURITY 후 정책을 하나도 만들지 않으면 기본 deny-all이 적용되어 아무 행도 반환되지 않는다. 슈퍼유저와 테이블 소유자만 접근 가능하다.
-- ❌ 정책 없이 RLS 활성화 — 모든 비소유자 쿼리가 빈 결과 반환
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- 일반 사용자로 쿼리
SET ROLE app_user;
SELECT * FROM orders; -- 0 rows (오류 없이 빈 결과)
-- ✅ RLS 활성화와 동시에 정책을 반드시 정의한다
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY orders_isolation
ON orders
FOR ALL
USING (tenant_id = current_setting('app.tenant_id')::bigint)
WITH CHECK (tenant_id = current_setting('app.tenant_id')::bigint);