테이블 단위가 아니라 행 단위로 접근을 통제하는 기능(PostgreSQL의 RLS).
무엇을 푸나 — 멀티테넌시
한 테이블에 여러 고객사(tenant) 의 데이터가 섞여 있다
애플리케이션이 매 쿼리에 WHERE tenant_id = ? 를 붙인다
- 한 군데라도 빠뜨리면 남의 데이터가 보인다
- 코드 리뷰로 100% 막는 것은 불가능에 가깝다
RLS는 그 조건을 DB가 강제로 붙여 준다.
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.tenant_id')::BIGINT);
-- 애플리케이션은 커넥션마다 자기 테넌트를 설정한다
SET app.tenant_id = '42';
SELECT * FROM orders; -- tenant_id = 42 인 행만 보인다. WHERE 를 안 써도 된다
이제 WHERE를 빠뜨려도 남의 데이터가 나오지 않는다.
읽기와 쓰기를 나눠 건다
CREATE POLICY tenant_select ON orders FOR SELECT
USING (tenant_id = current_setting('app.tenant_id')::BIGINT);
CREATE POLICY tenant_insert ON orders FOR INSERT
WITH CHECK (tenant_id = current_setting('app.tenant_id')::BIGINT);
- USING — 읽을 수 있는 행을 정한다 (SELECT · UPDATE 대상)
- WITH CHECK — 쓸 수 있는 값을 정한다 (INSERT · UPDATE 결과)
WITH CHECK를 빠뜨리면 남의 테넌트 id로 데이터를 넣을 수 있다.
주의할 점
- 테이블 소유자와 슈퍼유저는 기본적으로 RLS 를 우회한다
- FORCE ROW LEVEL SECURITY 로 소유자에게도 적용할 수 있다
- 커넥션 풀에서 SET 값이 남는다
- 반납 시 초기화하거나 SET LOCAL (트랜잭션 범위) 을 쓴다
- 정책 조건이 인덱스를 타는지 확인한다 (tenant_id 인덱스 필수)
SET LOCAL app.tenant_id = '42'; -- 트랜잭션이 끝나면 사라진다 (풀 환경에서 안전)
MySQL에는 RLS가 없어 뷰 + 권한으로 흉내 낸다.
감사 — 누가 무엇을 했나
-- PostgreSQL: pgaudit 확장
SHOW pgaudit.log; -- write, ddl, role ...
-- 애플리케이션 레벨 이력은 트리거로 남긴다
CREATE TRIGGER trg_audit AFTER INSERT OR UPDATE OR DELETE ON account
FOR EACH ROW EXECUTE FUNCTION write_audit_log();
면접 함정
- ❌ "애플리케이션에서 걸러면 충분" → 경로가 여럿이고 한 번만 빠뜨려도 유출이다.
- ❌ "RLS를 켜면 자동으로 안전" → 소유자 우회·
WITH CHECK누락·커넥션 풀 잔존값이 실제 구멍이다.