---
term: '행 수준 보안 (RLS)'
seoTitle: '행 수준 보안(RLS)이란? 정책, 우회, 성능 가이드'
headline: '행 수준 보안(RLS)이란?'
slug: row-level-security
category: database
shortDefinition: '행 수준 보안(RLS)은 모든 쿼리에 적용되는 정책으로 각 사용자가 보거나 변경할 수 있는 행을 걸러 내는 데이터베이스 메커니즘입니다.'
relatedTerms:
  - access-control-lists-acl
  - class-level-permissions-clp
  - role-based-access-control-rbac
  - tenant-isolation
  - data-layer-vs-application-layer-security
contrastsWith:
  - class-level-permissions-clp
faq:
  - question: '행 수준 보안을 쉽게 설명하면 무엇입니까?'
    answer: '강제되는, 보이지 않는 WHERE 절입니다. 테이블에 정책을 붙이면 데이터베이스가 모든 쿼리에 그 조건을 자동으로 적용합니다. 어떤 쿼리를 작성하든, 어떤 도구를 쓰든 각 사용자는 정책이 허용하는 행만 보고 수정합니다. 필터가 데이터베이스 안에 있으므로 어떤 애플리케이션 코드 경로도 이를 빠뜨릴 수 없습니다.'
  - question: '행 수준 보안은 어떻게 작동합니까?'
    answer: '테이블에서 RLS를 활성화하고 불리언 조건식을 담은 정책을 만듭니다. 보통 행의 소유자 열이나 테넌트 열을 현재 사용자 또는 세션 변수와 비교하는 조건입니다. 쿼리 시점에 엔진은 사용자 자신의 조건보다 먼저 행마다 이 조건식을 평가해 읽기를 걸러 내고 허용되지 않은 쓰기를 차단합니다. 정책 없이 RLS만 활성화하면 기본값은 전체 거부입니다.'
  - question: 'USING과 WITH CHECK의 차이는 무엇입니까?'
    answer: '방향입니다. USING은 사용자에게 무엇이 존재하는지를 거릅니다. 즉 SELECT로 보이는 행과 UPDATE 또는 DELETE의 대상이 될 수 있는 행입니다. WITH CHECK는 사용자가 쓰는 내용을 검증합니다. 삽입되는 행과 갱신 후의 새 값입니다. WITH CHECK를 생략하면 USING 조건식이 양쪽에 적용되지만, 사용자가 넓게 읽고 좁게 써야 하는 테이블에는 두 조건을 서로 다르게 설정해야 합니다.'
  - question: '행 수준 보안은 누가 우회할 수 있습니까?'
    answer: 'PostgreSQL에서는 슈퍼유저, BYPASSRLS 속성이 부여된 역할, 그리고 모두가 잊는 테이블 소유자입니다. 테이블 소유자는 FORCE ROW LEVEL SECURITY를 설정하지 않는 한 정책을 우회합니다. 이 우회 매트릭스를 감사하는 일은 RLS 배포의 일부입니다. 애플리케이션이 테이블 소유자로 접속한다면 완벽한 정책도 아무것도 보호하지 못합니다.'
  - question: '행 수준 보안은 성능을 저하시킵니까?'
    answer: '그럴 수 있습니다. 정책 조건식은 모든 쿼리에서 후보 행마다 평가됩니다. 완화책은 프로덕션 경험 전반에서 일관됩니다. 정책이 참조하는 열에 인덱스를 만들고, 조건식에서 조인을 없애고(대신 함수나 역할 조회를 사용), 요청 단위 함수를 감싸 행마다가 아니라 쿼리마다 한 번만 평가되게 합니다. 인덱스 없는 열에 걸린 정책은 보안 배지를 단 테이블 스캔일 뿐입니다.'
  - question: 'RLS와 애플리케이션 수준 필터링의 차이는 무엇입니까?'
    answer: '경계가 어디에 있느냐입니다. 애플리케이션 필터링은 코드에서 쿼리 범위를 제한합니다. 유연하지만 모든 코드 경로에 중복되고, 데이터베이스와 직접 통신하는 모든 것이 이를 우회합니다. RLS는 엔진에서 강제하므로 관리 도구와 다른 서비스를 포함한 모든 접근 경로를 포괄합니다. 합의된 답은 심층 방어입니다. 명확성을 위해 애플리케이션에서 범위를 제한하고, 안전을 위해 데이터베이스에서 강제합니다.'
  - question: 'RLS는 커넥션 풀링과 어떻게 함께 동작합니까?'
    answer: '신중하게 다뤄야 합니다. 풀링을 쓰는 애플리케이션은 하나의 데이터베이스 사용자로 접속하므로, 정책은 연결의 신원 대신 요청 단위 세션 변수를 기준으로 삼습니다. 이 변수는 연결을 꺼낸 뒤 설정하고, 정책이 읽고, 반환할 때 초기화합니다. 트랜잭션 모드 풀러에서는 이 설정이 트랜잭션 범위여야 하며, 그렇지 않으면 한 테넌트의 컨텍스트가 같은 연결의 다음 요청으로 새어 나갈 수 있습니다. 이 상호작용이 프로덕션에서 가장 흔한 RLS 버그입니다.'
  - question: '행 수준 보안 정책은 어떻게 테스트합니까?'
    answer: '각 역할로 가장해 SELECT, INSERT, UPDATE, DELETE 네 가지 작업을 모두 실행하고, 무엇이 보이는지와 무엇이 거부되는지를 함께 검증합니다. 잊기 쉬운 두 가지 경우도 추가합니다. 컨텍스트가 설정되지 않은 경우(테넌트 변수가 없으면 결과는 모든 행이 아니라 빈 결과여야 합니다)와 우회 매트릭스(소유자, 슈퍼유저, 복제 경로)입니다. RLS의 신뢰는 정책이 그럴듯해 보이는 데서가 아니라 공격적인 테스트에서 나옵니다.'
codeLanguages: [javascript, dart, swift, kotlin]
externalAuthorities:
  - name: 'PostgreSQL Row Security Policies'
    url: 'https://www.postgresql.org/docs/current/ddl-rowsecurity.html'
  - name: 'PostgreSQL CREATE POLICY reference'
    url: 'https://www.postgresql.org/docs/current/sql-createpolicy.html'
  - name: 'Row-Level Security and Multi-Tenancy on MongoDB (Back4app Engineering)'
    url: 'https://www.back4app.com/multi-tenant-mongodb-row-level-security'
  - name: 'Backend security documentation'
    url: 'https://docs.parseplatform.org/parse-server/guide/#security'
cta:
  title: 'MongoDB에 더하는 행 수준 보안'
  text: 'Back4app은 문서 데이터베이스에서 행 단위 접근 제어를 기본으로 강제합니다. 모든 객체가 ACL을 지니고, 역할이 사용자를 묶으며, 플랫폼이 모든 요청에서 둘 다 확인합니다. 모든 SDK, 모든 API, 대시보드까지 예외가 없습니다. 직접 작성할 정책도, 빠뜨릴 WHERE 절도 없습니다.'
  linkText: '무료로 시작하기'
  linkUrl: 'https://www.back4app.com/signup'
author: 'Back4app Engineering'
publishedDate: '2026-10-30'
translationKey: row-level-security
---

**행 수준 보안(RLS)은 모든 쿼리에 적용되는 정책으로 각 사용자가 보거나 변경할 수 있는 행을 걸러 내는 데이터베이스 메커니즘입니다.** 머릿속에 그릴 모델은 *강제되는, 보이지 않는 WHERE 절*입니다. 원래라면 모든 쿼리에서 기억해야 했을 조건이 테이블 자체의 속성이 되고, 애플리케이션 코드가 결코 보지 못하는 경로를 포함한 모든 접근 경로에서 엔진이 이를 적용합니다.

## 핵심 요약

| 질문 | 답변 |
| --- | --- |
| 정의 | 데이터베이스 엔진이 직접 강제하는 행 단위 접근 제어 |
| 머릿속 모델 | 잊어버릴 수 없는 WHERE 절 |
| 결정적인 사용 사례 | 침해가 건너뛸 수 없는 계층에서 이루어지는 멀티테넌트 격리와 사용자별 데이터 |
| 약관의 세부 조항 | 우회 역할, 기본 거부, 풀링 컨텍스트. 함정은 운영 측면에 있음 |
| 막아 주는 실패 | 애플리케이션 코드에서 필터 하나 누락 = 테넌트 간 데이터 침해 |

## SQL 10줄로 보는 RLS

```sql
CREATE TABLE invoices (
  tenant_id uuid NOT NULL,
  total     numeric(10,2)
);

ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;   -- 여기부터 기본 거부

CREATE POLICY tenant_isolation ON invoices
  USING      (tenant_id = current_setting('app.tenant')::uuid)   -- 읽을 수 있는 것
  WITH CHECK (tenant_id = current_setting('app.tenant')::uuid);  -- 쓸 수 있는 것

-- 이제 모든 쿼리가 WHERE tenant_id = <내 테넌트>로 끝나는 것처럼 동작합니다.
-- 필터를 잊은 쿼리는 전부가 아니라 아무것도 반환하지 않습니다.
```

같은 보장이 문서 데이터베이스에서는 *행 자체에 걸린 접근 제어*로 존재합니다. 각 객체가 자신의 ACL을 지니고, 플랫폼이 모든 요청에서 이를 강제합니다:

**JavaScript:**

```javascript
// JavaScript / Node.js — Back4app JS SDK
// Row-level security as data: this row is readable by its owner, period
const note = new Parse.Object('Note');
note.set('text', 'Q3 salary planning');
note.setACL(new Parse.ACL(Parse.User.current())); // owner-only, on the row itself
await note.save();

// Later, any query by any user — the row exists only for its owner
const notes = await new Parse.Query('Note').find(); // others never see it
```

**Flutter:**

```dart
// Flutter / Dart — Back4app Flutter SDK
// Row-level security as data: this row is readable by its owner, period
final user = await ParseUser.currentUser() as ParseUser;
final acl = ParseACL(owner: user); // owner-only, on the row itself

final note = ParseObject('Note')
  ..set('text', 'Q3 salary planning')
  ..setACL(acl);
await note.save(); // other users' queries never return this row
```

**Swift:**

```swift
// iOS / Swift — Back4app Swift SDK
// Row-level security as data: this row is readable by its owner, period
var note = Note()
note.text = "Q3 salary planning"
note.ACL = try ParseACL.defaultACL() // owner-only, on the row itself
note.save { result in
  if case .success = result {
    print("saved — other users' queries never return this row")
  }
}
```

**Kotlin:**

```kotlin
// Android / Kotlin — Back4app Android SDK
// Row-level security as data: this row is readable by its owner, period
val note = ParseObject("Note").apply {
  put("text", "Q3 salary planning")
  acl = ParseACL(ParseUser.getCurrentUser()) // owner-only, on the row itself
}
note.saveInBackground { e ->
  if (e == null) Log.d("Notes", "saved — invisible to every other user")
}
```

## 엔진이 쿼리를 처리하는 방식

```mermaid
flowchart LR
  accTitle: 행 수준 보안이 쿼리를 걸러 내는 방식
  accDescr: 서로 다른 두 사용자가 한 테이블에 같은 쿼리를 실행합니다. 엔진은 사용자 조건보다 먼저 보안 정책 조건식을 행마다 적용하므로, 각 사용자는 자신의 정책이 허용하는 행만 받습니다.
  Q["SELECT * FROM invoices<br/>(같은 쿼리, 모든 사용자)"] --> P["정책 조건식<br/>행마다, 가장 먼저 적용"]
  P --> A["테넌트 A는<br/>테넌트 A의 행만 봄"]
  P --> B["테넌트 B는<br/>테넌트 B의 행만 봄"]
```

## USING vs. WITH CHECK, 허용 정책 vs. 제한 정책

| 개념 | 관할 대상 | 경험칙 |
| --- | --- | --- |
| `USING` | 사용자에게 존재하는 것: 읽기, 그리고 갱신/삭제 대상이 될 수 있는 행 | "볼 수 있는가?" |
| `WITH CHECK` | 사용자가 쓸 수 있는 것: 삽입, 그리고 갱신 후의 값 | "이런 식으로 만들 수 있는가?" |
| 허용(permissive) 정책(기본값) | OR로 결합. 하나만 일치해도 허용 | 접근 부여 |
| 제한(restrictive) 정책 | 그 위에 AND로 결합. 모두 통과해야 함 | 필수 제한 부과 |

[PostgreSQL 레퍼런스](https://www.postgresql.org/docs/current/sql-createpolicy.html)에서 외워 둘 만한 결합 규칙은 두 가지입니다. `WITH CHECK`를 생략하면 쓰기에도 `USING`이 재사용되고, 제한 정책을 검토하기도 전에 적어도 하나의 허용 정책이 통과해야 합니다. 제한 정책만 있으면 아무도 들어오지 못한다는 뜻입니다.

## RLS를 지원하는 곳

| 엔진 계열 | 메커니즘 |
| --- | --- |
| PostgreSQL(9.5+)과 파생 제품 | `CREATE POLICY`. 참조 구현 |
| SQL Server(2016+) | 인라인 조건식 함수 위의 보안 정책(필터 + 차단) |
| 분산 SQL(CockroachDB, YugabyteDB) | PostgreSQL 호환 정책 |
| 클라우드 데이터 웨어하우스 | 벤더마다 다른 행 접근 정책 |
| 문서 데이터베이스 | 정책 기반이 아님. 플랫폼 계층이 강제하는 객체별 ACL |

이 용어집이 주목하는 것은 마지막 행입니다. 문서 저장소에서 행 수준 경계는 보통 엔진 안의 조건식이 아니라 *행에 담긴 데이터*(ACL)입니다. 보장은 같고 메커니즘만 다르며, 자세한 내용은 [Back4app 엔지니어링 글](https://www.back4app.com/multi-tenant-mongodb-row-level-security)에서 다룹니다.

## 함정, 마침내 한곳에

- **기본 거부의 함정.** 정책 없이 RLS를 활성화하면 소유자를 제외한 모두가 차단됩니다. "RLS가 프로덕션을 망가뜨렸다"는 이야기의 절반이 이 경우입니다.
- **우회 매트릭스.** 슈퍼유저, `BYPASSRLS` 역할, 그리고 *테이블 소유자*는 정책을 건너뜁니다. 소유자가 곧 애플리케이션의 접속 사용자라면 `FORCE ROW LEVEL SECURITY`를 설정하고, 누가 어떤 권한을 가졌는지 감사해야 합니다.
- **뷰는 기본적으로 소유자 권한으로 평가됩니다.** RLS 테이블 위의 뷰는 호출자 권한(PostgreSQL의 `security_invoker`)으로 만들지 않는 한 RLS를 조용히 우회할 수 있습니다.
- **제약 조건은 존재를 누설합니다.** 고유 제약 조건이나 외래 키 위반은 보이지 않는 행이 존재한다는 사실을 드러낼 수 있습니다. 문서화된 은닉 채널이므로, 보호 대상 값에 거는 고유성 제약은 이를 감안해 다뤄야 합니다.
- **오류는 값을 누설할 수 있습니다.** 특정 데이터에서만 실패하도록 조작한 식(숨겨진 값이 일치할 때 일어나는 0으로 나누기)은 오류 메시지를 통해 데이터를 빼냅니다. SQL Server 문서가 설명하는 사이드 채널 유형입니다.
- **덤프와 풀에는 모드가 있습니다.** 백업 도구는 모든 데이터를 내보내기 위해 행 보안을 꺼야 할 수 있고, 트랜잭션 모드 풀러에는 트랜잭션 범위의 컨텍스트(`SET LOCAL`)가 필요합니다. 그렇지 않으면 테넌트 데이터가 요청 사이로 번집니다.

## 성능: 정책은 핫 패스 위의 코드입니다

현장 경험의 대부분은 세 가지 규칙으로 정리됩니다. **정책이 참조하는 모든 열에 인덱스를 만들 것**(조건식은 쿼리 자체의 필터보다 먼저 실행되므로 인덱스가 없으면 스캔이 됩니다), **조건식에서 조인을 없앨 것**(조회는 함수나 역할 확인으로 넘깁니다), 그리고 **요청 단위 함수를 스칼라 서브쿼리로 감싸 행마다가 아니라 쿼리마다 한 번만 평가되게 할 것**입니다. 잘 설계하면 RLS의 비용은 한 자릿수 퍼센트에 그치고, 잘못 설계하면 보호한 모든 테이블에서 원인 모를 속도 저하가 됩니다.

## 일반적인 사용 사례

- **멀티테넌트 SaaS.** 대표적인 사례입니다. 애플리케이션 아래에서 테넌트 격리를 강제하므로, 잊어버린 필터가 침해로 이어지지 않습니다.
- **사용자별 데이터.** 메시지, 문서, 건강 기록처럼 사용자가 자신의 행만 보면 되는 데이터입니다. 예외는 없습니다.
- **부서와 지역 경계.** 영업팀은 자기 지역만 보고, 감사인은 전체를 읽기 전용으로 봅니다. 정책 계층화가 작동하는 모습입니다.
- **규정 준수 체계.** 접근 제어를 데이터 계층에서 입증할 수 있으며, 감사인이 선호하는 곳도 바로 그 계층입니다.
- **공유 분석 접근.** 분석가가 프로덕션 복제본에 직접 쿼리하면서 자신의 역할이 허용하는 것만 봅니다. 대시보드가 아니라 데이터베이스가 강제하기 *때문에* 안전합니다.

## 행 수준에서 강제해야 할까? 결정 매트릭스

| RLS/ACL로 강제해야 하는 경우… | 애플리케이션 필터링으로 충분할 수 있는 경우… |
| --- | --- |
| 여러 테넌트나 사용자가 테이블을 공유함 | 데이터베이스의 호출자가 신뢰할 수 있는 단 하나뿐임 |
| 앱 외의 무언가가 데이터베이스에 접근함 | BI 도구도, 관리용 SQL도, 두 번째 서비스도 없음 |
| 필터 누락이 버그가 아니라 침해를 뜻함 | 범위 제한이 보안이 아니라 편의를 위한 것임 |
| 규정 준수상 데이터 계층의 증거가 필요함 | 데이터가 민감하지 않음 |
| 경계를 한곳에서 한 번만 테스트하고 싶음 | 모든 쿼리를 영원히 감사하는 일이 즐거움 |

두 열을 솔직하게 종합하면 이렇습니다. 가독성을 위해 범위 제한은 애플리케이션에 두되, 그와 별개로 데이터베이스에서도 강제해야 합니다. 심층 방어가 바로 핵심입니다.

## 한계와 트레이드오프

- **정책은 설계상 보이지 않습니다.** 그래서 "내 행은 어디로 갔는가?"를 디버깅하는 일은 엄연한 기술이 됩니다. 먼저 세션 컨텍스트를 로그로 남겨야 합니다.
- **복잡한 인가 규칙은 조건식의 범위를 넘어섭니다.** 워크플로 상태나 엔티티 간 로직이 필요한 규칙은 애플리케이션의 인가(권한 부여) 계층에 두고, RLS는 최후의 방어선으로 삼아야 합니다.
- **운영 부담은 실재합니다.** 우회 매트릭스, 덤프 모드, 풀링 컨텍스트는 일회성 설정이 아니라 계속 유지해야 하는 운영 지식입니다.
- **행마다 평가하는 비용은 일종의 세금입니다.** 잘 설계하면 작고, 그렇지 않으면 한없이 커집니다.
- **RLS는 열이 아니라 행을 보호합니다.** 필드를 숨기는 것은 열 수준 보안의 영역이며, 셀 단위 효과를 얻으려면 둘을 결합해야 합니다.

## Back4app에서의 행 수준 보안

Back4app은 관리형 데이터베이스, 자동 생성되는 REST 및 GraphQL API, 인증, 파일 스토리지, Cloud Code 서버리스 함수를 결합한 오픈소스 Backend as a Service(BaaS) 플랫폼입니다. Back4app의 행 수준 모델은 위 탭에서 본 "데이터로서의 ACL" 메커니즘입니다. 모든 객체가 자신의 접근 목록을 지니고, 역할이 테넌트와 팀을 표현하며, [플랫폼이 모든 요청에서 둘 다 강제합니다](https://docs.parseplatform.org/parse-server/guide/#security). SDK, REST, GraphQL, 대시보드가 모두 포함되며, 클래스 수준 권한이 그 위의 제한 계층 역할을 합니다. 강제되는 WHERE 절이라는 보장을 문서 데이터베이스에서 제공하는 셈이며, 위의 함정 목록은 사용자에게 떠넘겨지는 대신 플랫폼이 흡수합니다.
