2025년 3월 03일·6분 읽기

멀티테넌트 앱을 위한 PostgreSQL 행 수준 보안 패턴

테넌트 격리와 역할 규칙을 위한 실용적 패턴으로 PostgreSQL 행 수준 보안(RLS)을 배우고, 접근 제어를 앱이 아닌 데이터베이스에서 강제하는 방법을 알아보세요.

멀티테넌트 앱을 위한 PostgreSQL 행 수준 보안 패턴

데이터베이스 차원의 접근 통제가 중요한 이유

비즈니스 앱에는 보통 “사용자는 자기 회사의 데이터만 볼 수 있어야 한다”거나 “관리자만 환불을 승인할 수 있다” 같은 규칙이 있습니다. 많은 팀이 이런 규칙을 UI나 API에서만 강제하고 그것으로 충분하다고 가정하죠. 문제는 데이터베이스에 닿는 경로가 하나 더 늘어날수록 데이터 유출 가능성이 커진다는 점입니다: 내부 관리자 도구, 백그라운드 잡, 분석 쿼리, 잊혀진 엔드포인트, 또는 어느 한 검사(check)를 누락하는 버그 등입니다.

테넌트 격리는 한 고객(테넌트)이 실수로라도 다른 고객의 데이터를 읽거나 변경할 수 없게 만드는 것을 뜻합니다. 역할 기반 접근 제어는 같은 테넌트 내부에서도 에이전트, 매니저, 재무 등 권한이 다른 사람들을 구분합니다. 이런 규칙은 설명하기는 쉽지만, 여러 곳에 흩어져 있으면 일관되게 유지하기 어렵습니다.

PostgreSQL의 행 수준 보안(RLS)은 데이터베이스가 어떤 행을 볼 수 있고 변경할 수 있는지 결정하게 합니다. 앱의 모든 쿼리가 올바른 WHERE 절을 기억하기를 기대하는 대신, 데이터베이스가 정책을 자동으로 적용합니다.

RLS가 만능 방패는 아닙니다. 스키마 설계나 인증을 대신해주지 않고, 이미 강력한 데이터베이스 권한(예: superuser)을 가진 사람을 막아주지도 않습니다. 또한 읽기 정책이 없는데 쓰기만 허용되는 식의 논리적 실수를 막으려면 읽기와 쓰기 모두에 대한 정책을 작성해야 합니다.

하지만 얻는 이점은 분명합니다:

  • 데이터베이스에 접근하는 모든 코드 경로에 대해 한 벌의 규칙을 적용
  • 새로운 기능 배포 시 발생하는 실수 감소
  • SQL에서 접근 규칙을 확인할 수 있어 감사가 명확해짐
  • API 버그가 지나칠 때 더 강한 방어선 제공

설정 비용은 작지 않습니다. “이 사용자가 누구인지”와 “어떤 테넌트인지”를 데이터베이스에 일관되게 전달하는 방식이 필요하고, 앱이 성장할수록 정책을 유지해야 합니다. SaaS나 민감한 고객 데이터를 다루는 내부 도구라면 투자 대비 효과가 큽니다.

전문 용어 없이 보는 행 수준 보안 기초

행 수준 보안(RLS)은 쿼리가 볼 수 있거나 변경할 수 있는 행을 자동으로 필터링합니다. 화면이나 API, 리포트가 규칙을 스스로 기억하기를 기대하는 대신, 데이터베이스가 규칙을 적용합니다.

PostgreSQL RLS에서는 SELECT, INSERT, UPDATE, DELETE마다 검사되는 정책을 작성합니다. 정책이 “이 사용자는 테넌트 A의 행만 볼 수 있다”고 말하면, 잊힌 관리자 페이지나 새 쿼리, 급한 핫픽스도 같은 보호를 받습니다.

RLS는 GRANT/REVOKE와 다릅니다. GRANT는 특정 역할이 테이블에 접근할 수 있는지(또는 특정 열에 접근할 수 있는지)를 결정하고, RLS는 그 테이블 내부에서 어떤 행에 접근할 수 있는지를 정합니다. 보통은 둘을 함께 씁니다: GRANT로 테이블 접근을 제한하고 RLS로 접근할 수 있는 행을 제한합니다.

현실에서도 잘 작동합니다. 뷰는 일반적으로 RLS를 준수하고, 조인과 서브쿼리도 필터링되므로 사용자가 조인을 이용해 다른 사람의 데이터에 접근할 수 없습니다. 정책은 어떤 클라이언트가 쿼리를 실행하든 적용됩니다: 앱 코드, SQL 콘솔, 백그라운드 잡, 리포팅 도구 등.

RLS는 테넌트 격리가 강하게 요구되거나 동일한 테이블을 여러 방식으로 조회하거나 많은 역할이 같은 테이블을 공유하는 환경(SaaS 및 내부 도구)에 적합합니다. 단일 신뢰 백엔드만 있는 작은 앱이나 민감하지 않은 데이터에는 과할 수 있습니다. 관리자 도구, 데이터 추출, BI, 스크립트 같은 진입점이 하나라도 더해지면 RLS가 돈값을 합니다.

테넌트, 역할, 데이터 소유권을 먼저 정리하세요

정책을 하나도 작성하기 전에 누가 무엇을 소유하는지 명확히 하세요. PostgreSQL RLS는 데이터 모델이 이미 테넌트, 역할, 소유권을 반영하고 있을 때 가장 잘 동작합니다.

테넌트부터 시작하세요. 대부분의 SaaS 앱에서 가장 단순한 규칙은: 고객 데이터를 담는 모든 공유 테이블에는 tenant_id가 있다입니다. 인보이스 같은 명백한 테이블뿐 아니라 첨부 파일, 댓글, 감사 로그, 백그라운드 잡 같은 자주 잊히는 테이블도 포함됩니다.

다음으로 실제로 사용하는 역할 이름을 정하세요. 집합을 작고 사람 친화적으로 유지하세요: owner, manager, agent, read-only 같은 비즈니스 역할을 정책 검사에 매핑합니다(데이터베이스 역할과는 다릅니다).

그다음 레코드 소유권을 결정하세요. 어떤 테이블은 단일 사용자 소유(예: 개인 노트)이고, 어떤 테이블은 팀 소유(예: 공유 인박스)일 수 있습니다. 두 방식을 계획 없이 섞으면 정책이 읽기 어렵고 우회하기 쉬워집니다.

각 테이블에 대해 같은 질문에 답하는 방식으로 규칙을 문서화하세요:

  • 테넌트 경계는 무엇인가(어떤 열이 그것을 강제하는가)?
  • 누가 행을 읽을 수 있는가(역할과 소유권 기준)?
  • 누가 행을 생성하고 업데이트할 수 있는가(어떤 조건에서)?
  • 누가 행을 삭제할 수 있는가(보통 가장 엄격함)?
  • 어떤 예외가 허용되는가(지원 인력, 자동화, 내보내기 등)?

예: “Invoices”는 매니저가 테넌트의 모든 인보이스를 볼 수 있게 하고, 에이전트는 할당된 고객의 인보이스만, 읽기 전용은 조회만 가능하고 수정 불가로 정할 수 있습니다. 테넌트 격리나 삭제처럼 반드시 엄격해야 할 규칙과 매니저에게 추가 가시성을 줄 수 있는 유연한 규칙을 미리 나눠두세요. AppMaster 같은 노코드 도구로 빌드한다면 이 매핑은 UI 기대치와 데이터베이스 규칙을 일치시키는 데 도움이 됩니다.

멀티테넌트 테이블을 위한 설계 패턴

멀티테넌트 RLS는 테이블 구조가 예측 가능할 때 가장 잘 동작합니다. 각 테이블이 테넌트를 다른 방식으로 저장하면 정책이 퍼즐처럼 복잡해집니다. 일관된 형태는 정책을 읽고 테스트하고 유지하기 쉽게 만듭니다.

먼저 하나의 테넌트 식별자를 정하고 어디서나 사용하세요. UUID는 추측하기 어렵고 많은 시스템에서 생성하기 쉬워서 흔히 사용됩니다. 내부용이라면 정수도 괜찮습니다. 사람이 읽기 쉬운 슬러그(예: "acme")는 바뀔 수 있으니 표시용으로 취급하고 핵심 키로는 쓰지 마세요.

테넌트 범위 데이터에는 tenant_id 열을 추가하고 가능하면 NOT NULL로 만드세요. 테넌트 없이 존재할 수 있는 행은 보통 냄새가 납니다: 전역과 테넌트 데이터를 한 테이블에 섞으면 RLS 정책이 어렵고 취약해집니다.

인덱싱은 단순하지만 중요합니다. SaaS 앱의 대부분 쿼리는 먼저 테넌트로 필터링한 뒤 상태나 날짜 같은 비즈니스 필드로 좁힙니다. 기본은 tenant_id 인덱스이고, 트래픽이 많은 테이블은 (tenant_id, created_at)(tenant_id, status) 같은 복합 인덱스가 좋습니다.

어떤 테이블이 전역인지 테넌트 범위인지 빨리 결정하세요. 전역 테이블 예: 국가, 통화 코드, 요금제 정의. 테넌트 범위 테이블 예: 고객, 인보이스, 티켓 등 테넌트가 소유하는 것.

유지보수 가능한 규칙을 원하면 범위를 좁게 유지하세요:

  • 테넌트 범위 테이블: tenant_id NOT NULL, RLS 활성화, 정책은 항상 tenant_id를 검사
  • 전역 참조 테이블: tenant_id 없음, 테넌트 정책 없음, 대부분 역할에 대해 읽기 전용
  • 공유되지만 통제되는 테이블: 개념별로 테이블 분리(전역과 테넌트 행 혼합 금지)

AppMaster 같은 도구로 빌드하면 tenant_id가 표준 필드일 때 일관된 패턴을 여러 모듈에서 재사용할 수 있습니다.

단계별: 첫 번째 테넌트 정책 만들기

비즈니스 규칙을 한곳에 모으기
역할, 소유권, 승인 규칙을 중앙화된 백엔드 로직으로 바꿔 권한 검사를 반복하지 마세요.
프로젝트 생성

PostgreSQL RLS로 얻는 첫 번째 실용적 승리는, 특정 테이블이 현재 세션의 테넌트 내부에서만 읽히도록 하는 것입니다. 요점은 간단합니다: API에서 WHERE 절을 잊어도 데이터베이스가 다른 테넌트의 행을 반환하지 않습니다.

먼저 tenant_id 열이 있는 테이블에서 시작하세요:

ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;

RLS를 활성화하면 기본 동작이 사람들을 놀라게 할 수 있습니다: 역할이 RLS의 대상인데 일치하는 정책이 없으면 SELECT는 0행을 반환하고(쓰기 또한 실패합니다). 처음에는 이 상태가 바람직합니다.

이제 최소한의 읽기 정책을 추가하세요. 예제는 앱이 로그인 후 app.tenant_id 같은 세션 변수를 설정한다고 가정합니다:

CREATE POLICY invoices_tenant_read
ON invoices
FOR SELECT
USING (tenant_id = current_setting('app.tenant_id')::uuid);

다음으로 쓰기 규칙을 추가하세요. RLS에서 USING은 기존 행을 건드릴 수 있는지를 제어하고, WITH CHECK는 허용되는 새 값인지 검사합니다.

CREATE POLICY invoices_tenant_insert
ON invoices
FOR INSERT
WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);

CREATE POLICY invoices_tenant_update
ON invoices
FOR UPDATE
USING (tenant_id = current_setting('app.tenant_id')::uuid)
WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);

CREATE POLICY invoices_tenant_delete
ON invoices
FOR DELETE
USING (tenant_id = current_setting('app.tenant_id')::uuid);

정책은 기본적으로 PERMISSIVE입니다. 즉 한 정책만 허용하면 접근이 허용됩니다. 여러 규칙이 모두 통과해야 할 때는 RESTRICTIVE를 선택하세요(예: “활성 계정인 경우” 같은 두 번째 가드 추가에 유용).

정책은 작고 역할 중심으로 유지하세요. 많은 OR을 가진 거대한 규칙 하나 대신 청중별로 별도 정책을 만들면(예: invoices_tenant_read_app_user, invoices_tenant_read_support_agent) 테스트와 리뷰가 쉬워지고 나중에 안전하게 변경할 수 있습니다.

테넌트와 사용자 컨텍스트 안전하게 전달하기

RLS가 작동하려면 데이터베이스는 “누가 호출했는가”와 “어떤 테넌트에 속하는가”를 알아야 합니다. RLS 정책은 쿼리 시점에 데이터베이스가 읽을 수 있는 값과만 비교할 수 있으므로 그 컨텍스트를 세션에 전달해야 합니다.

일반적인 패턴은 인증 후 세션 변수를 설정하고 정책에서 current_setting()으로 읽는 것입니다. 앱은 JWT 같은 것으로 신원을 증명한 뒤 필요한 필드(tenant_id, user_id, role)만 데이터베이스 연결에 기록합니다.

-- 요청당(혹은 트랜잭션당) 한 번 실행
SELECT set_config('app.tenant_id', '3f2a0c3e-9c7b-4d3f-9c5c-3c5e9c5d1a11', true);
SELECT set_config('app.user_id',   '8d9c6b1a-6b6d-4e32-9c0d-2bfe6f6c1111', true);
SELECT set_config('app.role',      'support_agent', true);

-- 정책에서
-- tenant_id 열은 UUID
USING (tenant_id = current_setting('app.tenant_id', true)::uuid);

세 번째 인수 true를 쓰면 현재 트랜잭션에 로컬하게 설정됩니다. 커넥션 풀링을 사용하는 경우 재사용된 연결에 어제의 테넌트 컨텍스트가 남아 있지 않게 하려면 이것이 중요합니다.

JWT 클레임에서 컨텍스트 채우기

API가 JWT를 사용한다면 클레임을 곧이곧대로 신뢰하지 말고 입력으로 취급하세요. 토큰 서명과 만료를 검증한 뒤 필요한 필드(tenant_id, user_id, role)만 세션 설정으로 복사하세요. 클라이언트가 직접 헤더나 쿼리 파라미터로 이 값을 보내게 하지 마세요.

컨텍스트 누락 또는 잘못된 값: 기본 거부

설계할 때 설정 누락이 0행을 반환하도록 만드세요.

current_setting('app.tenant_id', true)를 사용하면 누락 시 NULL을 반환합니다. 적절한 타입으로 캐스트(예: ::uuid)해서 형식이 잘못되면 빠르게 실패하게 하세요. 테넌트/사용자 컨텍스트를 설정할 수 없으면 추정하지 말고 요청을 실패시키세요.

이 방식은 쿼리가 UI를 우회하거나 새 엔드포인트가 추가된 경우에도 접근 제어 일관성을 유지합니다.

유지보수하기 쉬운 실용적 역할 패턴

UI와 접근성 정렬 유지
UI 접근을 백엔드 권한과 일치시키는 웹 및 네이티브 앱을 만드세요.
앱 빌드

RLS 정책을 읽기 쉽게 유지하는 가장 쉬운 방법은 정체(identity)와 권한(permissions)을 분리하는 것입니다. 기본은 users 테이블과 사용자-테넌트-역할을 연결하는 memberships 테이블을 두는 것입니다. 그러면 정책은 한 가지 질문에 답할 수 있습니다: “현재 사용자가 이 행에 대해 적절한 멤버십을 가지고 있는가?”

역할 이름은 직함보다 실제 행동에 연결하세요. “invoice_viewer”나 “invoice_approver” 같은 이름이 “manager”보다 정책을 더 오래 견디고 명확합니다.

유지보수가 쉬운 역할 패턴 몇 가지:

  • 소유자 전용: 행에 created_by_user_id(또는 owner_user_id)가 있고 일치 여부를 검사
  • 팀 전용: 행에 team_id가 있고 정책이 같은 테넌트 내에서 사용자가 그 팀의 멤버인지 검사
  • 승인된 항목만: status = 'approved'일 때만 읽기 허용, 쓰기는 승인자만 허용
  • 혼합 규칙: 처음엔 엄격하게 시작하고 작은 예외를 추가(예: “지원팀은 읽기 허용, 단 테넌트 범위 내”)

크로스-테넌트 관리자(다중 테넌트에 접근하는 관리자)는 많은 팀이 실수하는 부분입니다. 이를 숨겨진 “슈퍼유저” 지름길로 처리하지 말고 명시적으로 다루세요. platform_admin 같은 전역 개념을 만들고 정책에서 의도적으로 검사하게 하세요. 가능하면 크로스-테넌트 접근은 기본적으로 읽기 전용으로 두고 쓰기는 더 높은 허들을 요구하세요.

문서화는 생각보다 중요합니다. 각 정책 위에 의도를 설명하는 짧은 주석을 달아두세요(예: “Approvers는 상태를 변경할 수 있다. Viewers는 승인된 인보이스만 읽을 수 있다.”). 6개월 후 그 짧은 설명이 정책 수정 시 큰 도움이 됩니다.

AppMaster 같은 노코드 도구로 빌드해도 이 패턴은 유효합니다. UI와 API는 빠르게 움직여도 데이터베이스 규칙은 멤버십과 명확한 역할 의미에 의존해 안정적으로 유지됩니다.

예시 시나리오: 인보이스와 지원을 가진 간단한 SaaS

RLS 친화적 앱 흐름 테스트
테넌트 격리 워크플로를 빠르게 프로토타입하고 기술 부채 없이 반복하세요.
지금 시작

작은 SaaS를 상상해보세요. 여러 회사를 서비스하고 각 회사는 하나의 테넌트입니다. 앱에는 인보이스(금융)와 지원 티켓(일상 지원)이 있습니다. 사용자는 agent, manager, support와 같은 역할을 가질 수 있습니다.

간단한 데이터 모델: 모든 인보이스와 티켓 행에 tenant_id가 있습니다. 티켓에는 assignee_user_id도 있습니다. 앱은 로그인 직후 데이터베이스 세션에 현재 테넌트와 사용자를 설정합니다.

PostgreSQL RLS가 일상 위험을 어떻게 바꾸는지 보세요.

Tenant A의 사용자가 Tenant B의 인보이스 ID를 추측해 보거나(또는 UI가 실수로 보냈을 때) 쿼리가 실행되더라도, 정책이 invoice.tenant_id = current_tenant_id를 요구하므로 데이터베이스는 0행을 반환합니다. “접근 거부”가 유출되는 대신 빈 결과가 옵니다.

한 테넌트 내부에서는 역할이 접근을 더 좁힙니다. 매니저는 테넌트의 모든 인보이스와 티켓을 보고, 에이전트는 자신에게 할당된 티켓과 자신의 드래프트만 볼 수 있습니다. 필터가 선택적일 때 API에서 여기서 실수하기 쉽습니다.

지원팀은 특수한 경우입니다. 고객 지원을 위해 인보이스를 조회할 수는 있지만 amount, bank_account, tax_id 같은 민감한 필드는 바꿀 수 없어야 합니다. 현실적인 패턴은:

  • 지원 역할에 대해 인보이스 SELECT 허용(여전히 테넌트 범위)
  • UPDATE는 “안전한” 경로로만 허용(예: 편집 가능한 열만 노출하는 뷰 또는 민감 필드 변경을 거부하는 엄격한 업데이트 정책)

이제 “실수로 필터를 적용하지 않은 API” 시나리오를 보세요. RLS가 없으면 크로스-테넌트 인보이스가 유출될 수 있습니다. RLS가 있으면 데이터베이스가 세션 테넌트 외의 행을 반환하지 않아 버그는 화면 깨짐으로 끝나고 데이터 유출로 이어지지 않습니다.

AppMaster로 이런 SaaS를 빌드하더라도 데이터베이스에 규칙을 두는 것이 좋습니다. UI 검사는 유용하지만, 무언가 빠졌을 때 버티는 것은 데이터베이스 규칙입니다.

흔한 실수와 회피 방법

RLS는 강력하지만 작은 실수가 보안이 ‘안전한 척’ 하는 상황을 만들 수 있습니다. 문제는 보통 새 테이블 추가, 역할 변경, 잘못된 DB 사용자로 테스트할 때 나타납니다.

자주 발생하는 실패는 새 테이블에서 RLS를 활성화하는 것을 잊는 것입니다. 주요 테이블에 대해 신중히 정책을 작성한 뒤에 “notes”나 “attachments” 같은 테이블을 추가하면서 전체 접근을 열어두고 배포하는 경우가 있습니다. 규칙으로 만드세요: 새 테이블 = RLS 활성화 + 적어도 하나의 정책.

또 다른 함정은 동작별로 정책이 일치하지 않는 것입니다. INSERT는 허용하지만 SELECT는 차단하는 정책은 데이터를 생성한 뒤 바로 “데이터가 사라졌다”는 식의 문제를 만듭니다. 반대 경우도 마찬가지입니다: 사용자는 읽을 수는 있지만 만들 수 없어서 UI에서 우회하려고 합니다. 흐름을 생각하세요: “생성 후 조회”, “수정 후 다시 열기”, “삭제 후 목록 보기”.

SECURITY DEFINER 함수를 조심하세요. 해당 함수는 함수 소유자의 권한으로 실행되어 RLS를 우회할 수 있습니다. 사용해야 한다면 작게 만들고 입력을 검증하며 동적 SQL 사용을 피하세요.

또한 앱 측 필터링에만 의존하면서 데이터베이스 접근을 열어두지 마세요. 잘 설계된 API도 새 엔드포인트, 백그라운드 잡, 관리자 스크립트가 생기면 확장됩니다. 데이터베이스 역할이 모든 것을 읽을 수 있다면 언젠가 문제가 생깁니다.

문제를 조기에 잡으려면 실무적인 검사를 유지하세요:

  • 개인 관리자 계정이 아닌 프로덕션 앱이 사용하는 동일한 DB 역할로 테스트
  • 테이블당 하나의 음성 테스트(다른 테넌트 사용자가 0행을 보아야 함)
  • 각 테이블이 예상하는 동작(SELECT, INSERT, UPDATE, DELETE)을 지원하는지 확인
  • SECURITY DEFINER 사용 검토 및 필요성 문서화
  • 코드 리뷰와 마이그레이션 체크리스트에 “RLS 활성화?” 항목 포함

예: 지원 에이전트가 인보이스 메모를 생성했는데 바로 읽을 수 없다면 보통 INSERT 정책은 있지만 SELECT 정책이 없거나 세션의 테넌트 컨텍스트가 설정되지 않았기 때문입니다.

RLS 설정을 검증하는 빠른 체크리스트

더 안전한 백엔드 빠르게 출시
요구사항이 바뀔 때도 깔끔한 소스 코드로 프로덕션 준비된 API를 생성하세요.
백엔드 빌드

RLS는 검토상으로는 맞아 보이지만 실제 사용에서는 실패할 수 있습니다. 검증은 정책을 읽는 것보다 현실적인 계정과 쿼리로 깨보는 것이 더 중요합니다. 앱이 사용할 방식대로 테스트하세요.

먼저 작은 테스트 신원을 만드세요. 최소 두 개의 테넌트(Tenant A, Tenant B)를 사용하고 각 테넌트에 일반 사용자와 관리자/매니저 역할을 하나씩 만드세요. “지원 에이전트”나 “읽기 전용” 역할을 지원한다면 그 계정도 추가하세요.

그다음 다음과 같은 반복 가능한 검사를 통해 RLS를 압박 테스트하세요:

  • 각 역할에 대해 핵심 동작 실행: 목록 조회, ID로 단일 행 조회, 삽입, 수정, 삭제. 각 동작에 대해 허용되어야 할 경우와 차단되어야 할 경우를 모두 시도.
  • 테넌트 경계 증명: Tenant A로서 Tenant B 데이터의 ID로 읽기/수정을 시도. 항상 0행 또는 권한 오류가 나와야 하며 일부 행이 보이면 안 됩니다.
  • 조인 누출 테스트: 보호된 테이블을 다른 테이블(조회 테이블 포함)과 조인해서 연결된 행이 외부 테넌트에서 끌려오지 않는지 확인.
  • 컨텍스트 누락/잘못된 경우 차단 확인: 요청당 설정하는 테넌트/사용자 컨텍스트를 지우고 재시도. “컨텍스트 없음”은 닫힘(fail closed)이어야 함. 잘못된 tenant id도 시도.
  • 기본 성능 확인: 쿼리 플랜을 보고 인덱스가 테넌트 필터 패턴(보통 tenant_id와 정렬/검색 기준)을 지원하는지 확인.

테스트에서 놀라운 결과가 나오면 정책이나 컨텍스트 설정을 먼저 고치세요. UI나 API로 패치하고 데이터베이스 규칙이 “대체로 지켜지길” 바라는 식으로 해결하지 마세요.

다음 단계: 안전하게 롤아웃하고 일관성 유지하기

PostgreSQL RLS를 안전장치로 다루세요: 신중하게 도입하고 자주 검증하며 팀이 따르기 쉬운 단순한 규칙을 유지하세요.

작게 시작하세요. 유출이 가장 큰 피해를 주는 테이블(결제, 인보이스, 개인정보, 고객 메시지)에 우선 RLS를 적용하세요. 초반의 작은 성공이 모두에게 이해되지 않은 대규모 롤아웃보다 낫습니다.

실용적인 롤아웃 순서는 보통 이렇습니다:

  • 먼저 핵심 소유 테이블(행이 명확히 하나의 테넌트에 속하는 테이블)
  • 개인 데이터(PII)가 있는 테이블
  • 테넌트로 필터링되는 공유 테이블(보고서, 분석 등)
  • 조인 테이블과 엣지 케이스(다대다 관계)
  • 기본이 안정되면 나머지 테이블

테스트를 필수로 만드세요. 자동화된 테스트는 서로 다른 테넌트와 역할로 같은 쿼리를 실행하고 변경 사항을 확인해야 합니다. “허용해야 함”과 “거부해야 함” 검사를 모두 포함하세요. 과허용되는 실수는 발견이 늦을수록 비용이 큽니다.

요청 흐름에서 세션 컨텍스트를 설정하는 한 곳을 명확히 하세요. tenant id, user id, role은 한 번, 일찍 적용하고 나중에 추측하지 마세요. 트랜잭션 중간에 컨텍스트를 설정하면 결국 누락되거나 오래된 값으로 쿼리하는 경우가 생깁니다.

AppMaster로 빌드할 때는 생성된 백엔드 API와 PostgreSQL 정책 사이의 일관성을 계획하세요. 모든 엔드포인트에서 동일한 세션 변수를 사용하도록 표준화하면 정책이 어디서나 동일하게 동작합니다. AppMaster를 appmaster.io에서 사용 중이라면 UI에서도 접근을 제한하더라도 RLS를 테넌트 격리에 대한 최종 권위로 다루는 것이 좋습니다.

마지막으로 실패를 관찰하세요. 권한 실패는 롤아웃 직후 특히 유용한 신호입니다. 반복되는 거부를 추적하고, 실제 공격인지, 클라이언트 플로우가 깨진 것인지, 정책이 너무 엄격한 것인지 조사하세요.

RLS를 건강하게 유지하는 짧은 습관 목록:

  • 기본-거부 사고방식과 의도적인 예외 추가
  • 명확한 정책 이름(table + action + audience)
  • 정책 변경은 코드 변경처럼 리뷰
  • 롤아웃 초기에 거부 로그 수집 및 검토
  • RLS가 적용된 새 테이블마다 작은 테스트 세트 추가
쉬운 시작
멋진만들기

무료 요금제로 AppMaster를 사용해 보세요.
준비가 되면 적절한 구독을 선택할 수 있습니다.

시작하다