2025-09-20 15:26
-
행 수준 보안(RLS)은 데이터베이스 자체에서 데이터 접근 권한을 행(row) 단위로 제어하는 강력한 보안 기능이다.
-
애플리케이션 코드 수정 없이 중앙화된 보안 정책을 통해 여러 사용자와 역할을 관리하며, 복잡한 데이터 접근 제어 요구사항을 해결한다.
-
성능 저하 가능성을 인지하고 정책을 신중하게 설계해야 하며, 멀티 테넌시, 규정 준수 등 현대적인 아키텍처에서 필수적인 역할을 수행한다.
데이터베이스 보안의 최종 방어선 RLS 완벽 핸드북
데이터베이스는 모든 서비스의 심장과 같다. 이 심장에 흐르는 데이터라는 혈액을 어떻게 안전하게 보호하고 필요한 곳에만 공급할 수 있을까? 과거에는 애플리케이션이라는 외부 경비원에게 모든 출입 통제를 맡겼다. 하지만 경비원이 실수하거나, 여러 경로로 데이터에 접근하려는 시도가 늘어나면서 새로운 방어선이 필요해졌다. 바로 데이터베이스 심장 자체에 내장된 정교한 판막 시스템, **행 수준 보안(Row-Level Security, RLS)**이다.
이 핸드북은 RLS가 왜 탄생했으며, 어떤 구조로 동작하고, 어떻게 사용해야 하는지에 대한 모든 것을 담고 있다. 단순한 개념 설명을 넘어, RLS를 실제 시스템에 적용할 때 마주할 수 있는 깊이 있는 고민과 해결책까지 제시할 것이다.
1. RLS의 탄생 배경: 왜 필요했는가?
RLS를 이해하려면 먼저 ‘RLS가 없던 시절’을 상상해야 한다. 특정 사용자 그룹은 특정 데이터만 봐야 한다는 요구사항은 언제나 존재했다. 예를 들어, 한 회사 내에서 영업팀장은 자기 팀원의 실적만, 각 팀원은 자신의 실적만 봐야 한다.
과거의 방식과 그 한계
-
애플리케이션 계층에서의 필터링 (Application-Level Filtering)
-
방법: 개발자가 애플리케이션 코드 내에서 사용자의 역할이나 ID를 확인하고, 모든 데이터베이스 쿼리에
WHERE user_id = '현재 사용자'와 같은 조건을 직접 추가하는 방식. -
한계:
-
실수의 위험: 단 하나의 쿼리라도 필터링 조건을 누락하면 심각한 데이터 유출 사고로 이어진다.
-
유지보수의 어려움: 보안 정책이 변경될 때마다 애플리케이션의 수많은 코드를 찾아 수정하고 재배포해야 한다.
-
우회 가능성: 사용자가 데이터베이스에 직접 접근할 수 있는 권한(예: 분석 도구, SQL 클라이언트)을 가졌을 경우, 애플리케이션의 보안 로직은 무용지물이 된다.
-
-
-
뷰(View)를 이용한 데이터 제한
-
방법: 사용자 역할별로 접근 가능한 데이터만 필터링하는 뷰(View)를 생성하고, 사용자에게는 원본 테이블 대신 뷰에 대한 접근 권한만 부여하는 방식. 예를 들어
sales_team_A_view는 A팀의 데이터만 보여준다. -
한계:
-
관리의 복잡성: 역할이나 팀이 늘어날 때마다 새로운 뷰를 계속해서 생성하고 권한을 관리해야 한다. 수백 개의 팀이 있다면 수백 개의 뷰가 필요해진다.
-
유연성 부족: 동적이고 복잡한 규칙(예: 관리자는 자기 부서의 모든 데이터를 보지만, 특정 등급 이하는 제외)을 구현하기 어렵다.
-
제한적인 DML: 뷰를 통한 데이터 수정(INSERT, UPDATE, DELETE)은 복잡한 제약 조건이 따르거나 불가능한 경우가 많다.
-
-
이러한 한계들은 명확한 메시지를 던졌다. “보안 정책은 데이터가 저장된 곳, 바로 데이터베이스에서 직접, 그리고 자동으로 적용되어야 한다.” 이 요구에 대한 데이터베이스 시스템의 응답이 바로 RLS였다.
2. RLS의 구조와 작동 원리
RLS는 데이터베이스에 접속한 사용자가 누구인지에 따라, 같은 쿼리를 실행하더라도 서로 다른 결과(행의 집합)를 보도록 만드는 기능이다. 마치 보이지 않는 WHERE 절이 모든 쿼리에 자동으로 추가되는 것과 같다.
핵심 구성 요소: 보안 정책(Security Policy)
RLS의 핵심은 보안 정책이다. 이 정책은 특정 테이블에 대해 특정 사용자가 어떤 조건의 행에 접근할 수 있는지를 정의하는 규칙의 집합이다.
-
정책(Policy): 규칙 그 자체. 예를 들어, “사용자는 자신의 데이터만 볼 수 있다”는 정책.
-
대상 테이블(Target Table): 정책이 적용될 테이블. 예를 들어,
employees테이블. -
조건(Predicate / Expression): 행의 접근 가능 여부를 판단하는
TRUE또는FALSE를 반환하는 조건식. 이 조건식이 RLS의 핵심이다.employee_id = current_user_id()와 같은 형태다. -
적용 대상 명령어(Commands):
SELECT,INSERT,UPDATE,DELETE등 어떤 명령어에 정책을 적용할지 지정한다. -
적용 대상 역할(Roles): 특정 데이터베이스 역할(사용자 그룹)에만 정책을 적용할 수 있다.
동작 메커니즘: 쿼리 재작성(Query Rewriting)
사용자가 쿼리를 실행하면, 데이터베이스의 옵티마이저(Query Planner)는 쿼리를 분석하고 실행 계획을 세운다. 이때, RLS 정책이 활성화된 테이블에 대한 쿼리가 감지되면 옵티마이저는 다음과 같은 일을 수행한다.
-
사용자가
SELECT * FROM sales_records;라는 쿼리를 실행한다. -
옵티마이저는
sales_records테이블에 “팀장은 자기 팀의 데이터만 볼 수 있다”는 RLS 정책이 걸려있는 것을 확인한다. -
정책의 조건식은
team_id = get_user_team_id(current_user)이다. -
옵티마이저는 사용자의 원래 쿼리를 내부적으로 다음과 같이 재작성한다.
SELECT * FROM sales_records WHERE team_id = get_user_team_id(current_user); -
재작성된 쿼리에 대한 최적의 실행 계획을 수립하고 실행한다.
이 모든 과정은 사용자에게는 투명하게, 데이터베이스 커널 수준에서 자동으로 일어난다. 개발자는 더 이상 모든 쿼리에 WHERE 절을 신경 쓸 필요가 없어진다.
비유로 이해하기
RLS를 마법 안경에 비유할 수 있다.
-
데이터베이스 테이블: 모든 정보가 적힌 거대한 책.
-
사용자: 책을 읽으려는 사람.
-
RLS 정책: 사용자마다 다르게 제작된 마법 안경. 모든 사용자는 같은 책을 보지만, A 사용자의 안경은 A와 관련된 문단만 보이게 하고, B 사용자의 안경은 B와 관련된 문단만 보이게 필터링해준다. 사용자는 자신이 모든 내용을 보고 있다고 생각할 수 있지만, 실제로는 RLS라는 안경을 통해 걸러진 내용만 보는 것이다.
3. RLS 사용법: 실제 구현 예시 (PostgreSQL 기준)
개념을 이해했으니 실제 코드로 RLS를 구현해보자. 가장 널리 쓰이는 오픈소스 데이터베이스인 PostgreSQL을 기준으로 설명한다.
시나리오: 간단한 멀티 테넌트(Multi-tenant) 블로그 서비스. 여러 사용자가 글을 작성하지만, 각 사용자는 자신이 작성한 글만 보고 수정/삭제할 수 있다.
1. 테이블 및 샘플 데이터 생성
-- 사용자 정보를 저장할 테이블 (간소화)
CREATE TABLE users (
username TEXT PRIMARY KEY,
role TEXT NOT NULL DEFAULT 'user'
);
-- 게시글 테이블
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
owner_username TEXT NOT NULL REFERENCES users(username)
);
-- 샘플 데이터 삽입
INSERT INTO users (username, role) VALUES ('alice', 'user'), ('bob', 'user'), ('admin', 'admin');
INSERT INTO posts (title, content, owner_username) VALUES
('Alice의 첫 글', '안녕하세요.', 'alice'),
('Bob의 생각', 'RLS는 흥미롭네요.', 'bob'),
('Alice의 두 번째 글', '날씨가 좋습니다.', 'alice');
2. RLS 활성화
RLS 정책을 만들기 전에, 해당 테이블에 RLS를 사용하겠다고 명시적으로 선언해야 한다.
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
중요: RLS를 활성화하는 순간, 기본적으로는 아무도 데이터에 접근할 수 없다 (Default Deny). 접근을 허용하는 정책을 명시적으로 만들어주기 전까지 테이블은 비어있는 것처럼 보인다. 이는 보안의 기본 원칙인 ‘최소 권한의 원칙’을 따른 것이다.
3. 보안 정책 생성
이제 핵심인 보안 정책을 만든다.
정책 1: 사용자는 자신의 글만 볼 수 있다 (SELECT)
CREATE POLICY select_own_posts ON posts
FOR SELECT
USING (owner_username = current_user);
-
CREATE POLICY select_own_posts ON posts:posts테이블에select_own_posts라는 이름의 정책을 생성한다. -
FOR SELECT:SELECT쿼리에 대해서만 적용된다. -
USING (owner_username = current_user): 가장 중요한 부분.SELECT시owner_username컬럼의 값이 현재 데이터베이스에 접속한 사용자(current_user)와 같은 행만 보이도록 허용하는 조건식이다.
정책 2: 사용자는 자신의 이름으로만 글을 쓸 수 있다 (INSERT)
CREATE POLICY insert_own_posts ON posts
FOR INSERT
WITH CHECK (owner_username = current_user);
WITH CHECK:INSERT나UPDATE시, 새로 입력되거나 수정될 행이 이 조건을 만족하는지 검사한다. 만약bob이owner_username을alice로 설정하여 글을 쓰려고 하면, 이CHECK조건에 위배되어 쿼리가 실패한다.USING이 기존 데이터를 필터링한다면,WITH CHECK는 들어올 데이터를 검증한다.
정책 3: 사용자는 자신의 글만 수정/삭제할 수 있다 (UPDATE, DELETE)
SELECT와 UPDATE/DELETE의 조건을 동일하게 가져가려면 ALL을 사용할 수 있다.
CREATE POLICY manage_own_posts ON posts
FOR ALL -- SELECT, INSERT, UPDATE, DELETE 모두에 적용
USING (owner_username = current_user)
WITH CHECK (owner_username = current_user);
이렇게 하면 SELECT 할 때는 USING 조건이, INSERT/UPDATE 할 때는 WITH CHECK 조건이 적용된다.
4. 테스트
이제 각 사용자로 접속하여 결과를 확인해보자.
-- alice로 접속했다고 가정
SET ROLE alice;
SELECT * FROM posts;
-- 결과: alice가 작성한 2개의 글만 보인다.
INSERT INTO posts (title, content, owner_username)
VALUES ('Alice의 새 글', '...', 'alice'); -- 성공
INSERT INTO posts (title, content, owner_username)
VALUES ('Bob의 글 훔치기', '...', 'bob'); -- 오류 발생! (WITH CHECK 위반)
-- bob으로 접속했다고 가정
SET ROLE bob;
SELECT * FROM posts;
-- 결과: bob이 작성한 1개의 글만 보인다.
정책 4: 관리자(admin)는 모든 글을 볼 수 있다
역할 기반으로 정책을 만들 수도 있다.
-- 기존 정책을 수정하거나 새로운 정책을 추가할 수 있다.
-- 관리자 역할을 위한 정책을 추가
CREATE POLICY admin_all_access ON posts
FOR ALL
USING (current_user = 'admin') -- 현재 사용자가 admin이면 모든 행에 접근 허용
WITH CHECK (current_user = 'admin'); -- admin은 어떤 데이터든 쓸 수 있도록 허용
-- 테스트
SET ROLE admin;
SELECT * FROM posts;
-- 결과: 모든 사용자(alice, bob)의 글 3개가 전부 보인다.
참고: 한 테이블에 여러 정책이 있을 경우, 각 정책의 USING 조건들은 OR로 결합된다. 즉, “자신의 글이거나” 또는 “현재 사용자가 admin이면” 보이게 된다.
4. 심화 내용: RLS, 제대로 사용하기
RLS는 강력하지만, 잘못 사용하면 성능 저하나 보안 허점을 낳을 수 있다.
성능 고려사항
RLS 정책의 USING 조건식은 모든 쿼리에 추가되는 WHERE 절과 같다. 따라서 이 조건식의 성능이 전체 데이터베이스 성능에 직접적인 영향을 미친다.
-
인덱싱: 정책 조건식에 사용되는 컬럼(위 예시에서는
owner_username)은 반드시 인덱싱해야 한다. 인덱스가 없으면 매 쿼리마다 테이블 전체를 스캔(Full Table Scan)하는 재앙이 발생할 수 있다. -
함수 사용 주의:
USING (complex_function(column) = 'value')와 같이 조건식에 복잡한 함수를 사용하면 인덱스를 활용하지 못할 수 있다. 함수의 종류가IMMUTABLE(입력이 같으면 항상 출력이 같은 순수 함수)로 선언되지 않았다면 특히 성능 저하가 크다. 가능한 한 컬럼 자체를 직접 비교하는 간단한 조건식을 사용하는 것이 좋다. -
JOIN 주의: 정책 조건식 내부에서 다른 테이블을
JOIN하는 경우, 쿼리 계획이 매우 복잡해지고 성능이 저하될 수 있다. 꼭 필요한 경우가 아니라면 피하는 것이 좋다.
RLS vs 다른 보안 방식 비교
| 특징 | 행 수준 보안 (RLS) | 뷰 기반 보안 | 애플리케이션 수준 보안 |
|---|---|---|---|
| 보안 정책 위치 | 데이터베이스 (중앙화) | 데이터베이스 (분산) | 애플리케이션 코드 |
| 투명성 | 높음 (자동 적용) | 중간 (뷰를 통해 접근) | 낮음 (코드에 의존) |
| 유지보수 | 용이 (정책만 수정) | 복잡 (역할마다 뷰 관리) | 매우 어려움 (코드 수정/배포) |
| 우회 가능성 | 낮음 | 중간 | 높음 (직접 DB 접근 시) |
| 유연성 | 매우 높음 (동적 규칙 가능) | 낮음 | 중간 (코드 구현에 따라) |
| 성능 오버헤드 | 존재 (정책 복잡도에 따라) | 존재 (뷰 정의에 따라) | 존재 (네트워크/코드 실행) |
RLS 사용 시 흔히 저지르는 실수와 베스트 프랙티스
-
실수:
ENABLE후 정책을 만들지 않는 것ALTER TABLE ... ENABLE ROW LEVEL SECURITY;를 실행하면 Default Deny 원칙에 따라 해당 테이블에 대한 모든 접근이 막힌다. 슈퍼유저조차 데이터를 볼 수 없어 당황하는 경우가 많다. 반드시 접근을 허용할 정책을 먼저 생성해야 한다.
-
실수:
USING과WITH CHECK의 차이를 혼동하는 것-
USING은 읽을 때 필터링하는 조건. -
WITH CHECK는 쓸 때 검증하는 조건. -
두 가지를 명확히 구분하고 목적에 맞게 사용해야 데이터 무결성을 지킬 수 있다.
-
-
베스트 프랙티스: 슈퍼유저와 소유자 예외 처리
- 기본적으로 RLS 정책은 테이블 소유자(owner)나 슈퍼유저에게도 적용된다. 때로는 관리 목적으로 이런 제약을 피하고 싶을 수 있다. PostgreSQL에서는
ALTER TABLE ... FORCE ROW LEVEL SECURITY;를 통해 소유자에게도 RLS를 강제하거나,BYPASSRLS속성을 가진 역할을 만들어 예외를 둘 수 있다.
- 기본적으로 RLS 정책은 테이블 소유자(owner)나 슈퍼유저에게도 적용된다. 때로는 관리 목적으로 이런 제약을 피하고 싶을 수 있다. PostgreSQL에서는
-
베스트 프랙티스: 정책의 단순화 및 중앙화
- 하나의 테이블에 너무 많은
OR조건의 정책을 중첩하는 것은 성능과 관리 측면에서 좋지 않다. 역할을 활용하고, 보안 컨텍스트를 제공하는 함수(get_user_team_id()등)를 만들어 정책을 최대한 단순하고 재사용 가능하게 만드는 것이 좋다.
- 하나의 테이블에 너무 많은
결론: 데이터 보안의 새로운 표준
RLS는 더 이상 일부 고급 데이터베이스의 특별한 기능이 아니다. 멀티 테넌시가 일반화되고, 데이터 프라이버시 규정(GDPR, CCPA 등)이 강화되며, 다양한 채널로 데이터에 접근하는 현대적인 아키텍처에서 RLS는 필수적인 보안 계층으로 자리 잡았다.
애플리케이션 코드에서 보안 로직을 걷어내고 데이터베이스에 그 책임을 위임함으로써, 우리는 더 안전하고, 더 깨끗하며, 더 유지보수하기 쉬운 시스템을 구축할 수 있다. RLS라는 강력한 방패를 올바르게 이해하고 사용한다면, 데이터라는 가장 중요한 자산을 훨씬 더 견고하게 지켜낼 수 있을 것이다.