Postgres Row Level Security 알아보기
Table of Contents
RLS란 무엇인가?
Row Level Security(RLS) 는 PostgreSQL에서 제공하는 행 단위 접근 제어 기능입니다. 사용자마다 테이블의 어떤 행을 읽고, 수정하고, 삭제할 수 있는지를 DB 레벨에서 결정할 수 있어 보안성과 안전성이 높아집니다.
PostgreSQL의 RLS는 테이블에 대해 다음과 같은 세부 권한 제어를 제공합니다.
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
USING vs WITH CHECK 차이
RLS 정책에는 두 가지 조건절이 있습니다
| 절 | 작동 시점 | 설명 |
|---|---|---|
| USING | 읽기/삭제/수정할 기존 행을 대상으로 작동 | 이 행을 내가 SELECT / UPDATE / DELETE 할 수 있는가? |
| WITH CHECK | 삽입 또는 수정할 새 행을 대상으로 작동 | 이 행을 내가 INSERT / UPDATE 할 수 있는가? |
예시
CREATE POLICY user_posts_policy ON posts
FOR ALL TO app_user
USING (user_id = current_setting('jwt.claims.user_id')::uuid)
WITH CHECK (user_id = current_setting('jwt.claims.user_id')::uuid);
이 정책은 다음과 같은 상황을 제어합니다:
- SELECT, UPDATE, DELETE: 기존 행의 user_id가 나인지 확인 (USING)
- INSERT, UPDATE: 삽입 또는 수정된 행의 user_id가 나인지 확인 (WITH CHECK)
SQL 명령어별 적용 관계
| 명령어 | USING 적용 | WITH CHECK 적용 | 함께 사용 가능 |
|---|---|---|---|
| SELECT | O | X | X |
| INSERT | X | O | O (의미 없음) |
| UPDATE | O | O | O (주 사용) |
| DELETE | O | X | X |
| ALL | O | O | O |
- UPDATE: 두 조건 모두 필요
- ALL: 모든 작업에 대한 공통 정책 작성 시 편리
- INSERT: WITH CHECK만 의미 있음
웹 사용자 기준으로 RLS 적용하기 (Golang 예시 포함)
웹 사용자 기준으로 정책을 만들기 위해선 PostgreSQL 세션 변수를 설정해야 합니다.
SET LOCAL jwt.claims.user_id = 'abc-123';
Golang에서 적용 방법 (표준 database/sql)
tx, _ := db.BeginTx(ctx, nil)
defer tx.Rollback()
// 사용자 ID를 세션에 설정
tx.ExecContext(ctx, "SET LOCAL jwt.claims.user_id = $1", userID)
// 이후 쿼리들은 모두 RLS 정책이 적용됨
tx.QueryContext(ctx, "SELECT title FROM posts")
tx.Commit()
SET LOCAL은 트랜잭션 내에서만 유효 → 반드시 트랜잭션을 사용해야 함
정책 예시
CREATE POLICY posts_rls ON posts
FOR ALL TO app_user
USING (user_id = current_setting('jwt.claims.user_id')::uuid)
WITH CHECK (user_id = current_setting('jwt.claims.user_id')::uuid);
정책이 여러개일 경우
PostgreSQL의 Row Level Security(RLS)는 하나의 테이블에 여러 정책(POLICY)을 정의할 수 있으며, 이 경우 “OR 조건” 으로 동작합니다.
핵심 개념
- 여러 RLS 정책이 존재하면, 그 중 하나라도 통과하면 행에 접근이 허용됨
- 즉, 모든 정책이 AND 조건이 아니라 OR 조건으로 평가됨
정책 우선순위는?
-
순서에 관계 없이 모두 평가됩니다.
-
PostgreSQL은 정책을 모두 수집해서 조건을 OR로 결합하여 최종 실행 계획에 적용합니다.
너무 느슨한 조건을 추가하면 모두 열림
CREATE POLICY allow_all ON posts
FOR SELECT TO app_user
USING (true);
- 이 정책 하나가 있으면, 다른 모든 SELECT 제한이 무의미해집니다.
- 모든 행이 조건 true를 만족하기 때문
RLS를 꼭 써야 할까?
RLS는 강력하지만 복잡합니다. 아래 기준에 따라 사용 여부를 판단하세요.
| 조건 | RLS 추천 여부 | 이유 |
|---|---|---|
| 사용자별 데이터 격리 필요 | O | 보안을 DB 레벨에서 보장 |
| 단일 사용자 앱 / 내부 툴 | X | 과도한 설정일 수 있음 |
| 인증 미들웨어가 부족함 | O | 코드 실수를 막아줌 |
| 성능 중요 / 쿼리 튜닝 필요 | 신중 | 쿼리 계획이 복잡해질 수 있음 |
| ORM 주 사용 | 신중 | ORM과 충돌 가능성 있음 |
정리
- USING: 기존 행의 접근 제어 (SELECT, UPDATE, DELETE)
- WITH CHECK: 새 행의 제어 (INSERT, UPDATE)
- UPDATE는 두 조건 모두 필요
- SET LOCAL로 세션에 사용자 정보 전달 → 반드시 트랜잭션 안에서 실행
- Golang에서는 tx.Exec(“SET LOCAL …”) → tx.Query(…) 흐름으로 안전하게 관리
- 초기 서비스에는 과할 수 있지만, 보안이 중요한 멀티유저 앱이라면 충분히 투자할 가치 있음