애플리케이션이 성장하면서 데이터의 양도 많아지지만, 동시에 보안과 접근 제어의 중요성도 커집니다. 특히 사용자마다 보여줘야 할 데이터가 다를 경우, 즉 로그인한 사용자에게 자신에게 해당하는 정보만 보여주고 싶을 때 어떻게 해야 할까요?
이런 상황에서 가장 효과적으로 활용할 수 있는 개념이 바로 Row-Level Security(RLS)입니다. 이름 그대로 데이터베이스 테이블의 ‘행’ 단위에서 접근을 제어하는 기능입니다. 단순히 전체 테이블을 보는 권한이 있는지 여부가 아니라, 그 안의 각 행(row)에 대해 ‘이 사용자가 이 데이터를 봐도 되는가’를 평가하는 방식이죠.
RLS는 특히 멀티 유저 환경, 예를 들어 게시판, 채팅, CRM, 다중 테넌시 구조의 서비스에서 사용자별 데이터 분리가 핵심일 때 필수적인 도구입니다. 이번 글에서는 RLS의 개념이 왜 필요한지, 기존 권한 시스템과 어떤 차이가 있는지, 그리고 이를 어떻게 설계하고 활용할 수 있는지를 하나씩 정리해보겠습니다.
RLS는 왜 필요한가?
보통 애플리케이션에서는 인증(auth)과 권한 부여(permission)를 프론트엔드 또는 서버 애플리케이션 단에서 처리합니다. 예를 들어 로그인한 사용자의 user_id를 세션이나 토큰으로 관리하고, API 요청 시 서버에서 이 값을 기반으로 SELECT * FROM posts WHERE user_id = ? 식의 쿼리를 날리는 방식이 일반적이죠.
하지만 이 방식에는 몇 가지 한계가 있습니다.
가장 큰 문제는 데이터베이스 자체는 사용자의 권한을 전혀 인식하지 못한다는 점입니다. 즉, 잘못된 쿼리나 API 버그로 인해 원래 의도하지 않은 데이터가 노출될 가능성이 항상 존재한다는 것이죠. 게다가 API 레벨에서 모든 권한 로직을 직접 구현하고 유지하려면, 시스템이 복잡해질수록 점점 더 많은 중복과 실수를 유발하게 됩니다.
반면 RLS는 이러한 접근 제어 로직을 데이터베이스 레벨로 끌어올립니다. PostgreSQL을 비롯한 몇몇 데이터베이스 시스템에서는 특정 테이블에 대해 ‘어떤 조건을 만족해야 이 행을 읽을 수 있는가’를 정의하는 정책을 설정할 수 있습니다. 이렇게 하면 쿼리를 보내는 주체(예: Supabase, Hasura, 직접 쿼리 등)가 무엇이든 상관없이, 데이터베이스가 알아서 필터링을 적용해 줍니다.
예를 들어, todos라는 테이블이 있고, 여기에 사용자 ID가 저장되어 있다면, RLS 정책을 통해 “user_id가 현재 로그인한 사용자의 ID와 일치하는 데이터만 조회 가능하다”는 규칙을 적용할 수 있습니다. 이 규칙은 SQL 수준에서 강제되기 때문에, 개발자가 실수로 WHERE 절을 빼먹어도 잘못된 데이터가 노출되지 않게 되는 거죠.
정리하자면, RLS는 보안성과 일관성을 동시에 높여주는 기능입니다. 복잡한 조건을 코드가 아닌 데이터베이스 레이어에서 정의함으로써, 데이터 접근 제어의 책임을 명확하고 안전하게 위임할 수 있게 되는 것입니다.
RLS의 실제 적용 예시와 설정 방법
RLS(Row-Level Security)는 PostgreSQL 기반 데이터베이스에서 공식적으로 지원하는 기능이며, Supabase는 이 기능을 매우 직관적인 인터페이스로 활용할 수 있게 해줍니다. 예제를 통해 살펴보면 이해가 훨씬 쉬워요.
예를 들어, todos라는 테이블이 있고, 이 테이블에 사용자가 작성한 할 일 목록이 저장된다고 가정해봅시다. 각 행에는 해당 데이터를 소유한 사용자의 ID가 함께 저장되어 있겠죠. 이를 바탕으로 “로그인한 사용자만 자신의 할 일만 조회하고 수정할 수 있도록” RLS를 설정할 수 있습니다.
먼저 Supabase 대시보드에서 todos 테이블을 만들고, 그 안에 user_id, task, is_done 같은 필드를 생성합니다. 이후 이 테이블에 RLS를 활성화하면, 기본적으로 모든 접근이 차단됩니다. 이제부터는 정책(policy)을 직접 설정해줘야 하죠.
Supabase에서는 정책을 아주 세부적으로 정의할 수 있습니다. 예를 들어 다음과 같은 로직을 설정할 수 있어요:
- 로그인한 사용자의 ID가 todos.user_id와 같을 경우에만 SELECT 허용
- 로그인한 사용자의 ID가 같을 경우에만 INSERT, UPDATE, DELETE 허용
이렇게 하면 백엔드 API든 Supabase 클라이언트 라이브러리든 관계없이, 인증된 사용자만 자신의 데이터를 안전하게 다룰 수 있게 됩니다. 아래는 Supabase에서 todos 테이블에 RLS를 활성화하고, 사용자별 접근 정책을 설정하는 전체 흐름입니다.
-- 1. todos 테이블 예시
create table todos (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users(id),
task text,
is_done boolean default false
);
-- 2. RLS 활성화
alter table todos enable row level security;
-- 3. 사용자별 조회 권한 (로그인한 유저만 자신의 할 일 조회 가능)
create policy "Allow user to read own todos"
on todos
for select
using (auth.uid() = user_id);
-- 4. 사용자별 삽입 권한
create policy "Allow user to insert own todos"
on todos
for insert
with check (auth.uid() = user_id);
-- 5. 사용자별 수정 권한
create policy "Allow user to update own todos"
on todos
for update
using (auth.uid() = user_id);
-- 6. 사용자별 삭제 권한
create policy "Allow user to delete own todos"
on todos
for delete
using (auth.uid() = user_id);
이 정책들은 Supabase의 인증 시스템과 통합되어 작동합니다. auth.uid()는 현재 로그인한 사용자의 ID를 반환하는 내장 함수이며, 이를 이용해 각 데이터 행의 user_id와 비교하여 접근 여부를 결정합니다.
이렇게 설정하고 나면, 클라이언트 단에서 굳이 user_id 조건을 직접 붙이지 않아도 됩니다. 실수로 전체 데이터를 요청하더라도, 데이터베이스에서 사용자 본인의 데이터만 필터링해주는 것이죠. 즉, 프론트엔드와 백엔드에서 ‘실수해도 안전한’ 시스템을 만드는 것, 그것이 RLS의 강력한 장점입니다.
RLS를 도입할 때의 유의점과 적용 팁
RLS는 분명 강력한 기능이지만, 도입 과정에서 몇 가지 주의할 점도 있습니다. 우선 RLS를 활성화하면 명시적으로 허용된 정책이 없는 한, 기본적으로 모든 행에 대한 접근이 차단된다는 것을 반드시 인지해야 합니다. 이는 보안을 강화하는 차원에서는 매우 좋은 일이지만, 테스트 과정에서 “왜 데이터가 안 나올까?” 싶은 경우 대부분은 정책 설정이 빠져 있거나 조건이 잘못된 경우가 많습니다.
또 하나 주의할 점은 관리자(혹은 시스템 계정)의 접근 예외 처리입니다. 예를 들어, 운영툴에서 전체 데이터를 조회하거나 특정 유저의 데이터를 대신 관리할 수 있어야 하는 경우, RLS가 걸려 있으면 그마저도 막힐 수 있어요. 이런 상황을 대비해, Supabase에서는 service_role 키를 사용하는 백엔드 요청에서는 RLS를 무시할 수 있도록 되어 있습니다. 민감한 작업에는 반드시 이 역할 키를 사용해야 하며, 클라이언트 앱에서는 절대 노출되지 않도록 주의해야 합니다.
RLS 정책은 되도록이면 단순하고 명확하게 유지하는 것이 좋습니다. 너무 많은 조건이 겹치면 디버깅이 어려워지고, 실제로 어떤 데이터가 노출되는지 파악하기 힘들 수 있기 때문이죠. 처음부터 모든 기능을 RLS로 제한하기보다는, 사용자 데이터와 같이 명확히 구분되어야 하는 테이블부터 단계적으로 적용해보는 것이 현실적인 접근입니다.
또한, RLS는 프론트엔드뿐만 아니라 BI 도구, 외부 API, ETL 파이프라인 등 데이터베이스를 참조하는 모든 경로에 영향을 줄 수 있다는 점도 염두에 두어야 해요. 그렇기 때문에 적용 전에는 반드시 테스트 환경에서 충분히 시뮬레이션을 해보고, 예외 상황을 고려한 정책을 함께 설계해야 안정적인 운영이 가능합니다.
마치며...
Row-Level Security는 단순히 보안 기능이 아닙니다. 그것은 개발자의 실수를 줄이고, 데이터 권한 체계를 더 명확하고 견고하게 만드는 아키텍처적인 선택입니다. 우리는 흔히 보안은 ‘위에서 관리하는 것’이라고 생각하지만, RLS를 도입하면 보안이 데이터베이스 안에서 자연스럽게 작동하게 됩니다.
Supabase처럼 RLS를 쉽게 설정할 수 있는 플랫폼이 보편화되면서, 과거에는 DBA나 백엔드 아키텍트의 몫이었던 데이터 보호 체계가 이제는 프론트엔드 개발자, 스타트업 팀 단위에서도 실현 가능한 현실이 되었습니다.
마지막으로 강조하고 싶은 건, RLS는 “당신이 데이터를 얼마나 신중하게 다루는지를 보여주는 방식”이라는 점입니다. 사용자가 자신의 데이터가 보호되고 있다는 신뢰를 갖게 하는 것, 그것이야말로 서비스가 오랫동안 신뢰받고 유지될 수 있는 가장 단단한 기반이 아닐까요?
이 글이 RLS의 개념을 이해하고, 실제 서비스에 적용할 때 도움이 되는 기준점이 되었기를 바랍니다. 데이터가 중요한 만큼, 데이터의 보안도 개발의 중심에 놓일 수 있기를 바라며 마무리합니다.
'IT' 카테고리의 다른 글
iOS 앱 개발에 꼭 필요한 Xcode (4) | 2025.05.19 |
---|---|
백엔드 할 줄 모르면 Supabase Edge Function (4) | 2025.05.16 |
Flutter에서 Supabase로 Google 로그인 구현하기 (2) | 2025.05.14 |
플러터, 크래시리틱스로 유지보수하기 (0) | 2025.05.12 |
플러터, 유저에게 알림 보내기 (2) | 2025.05.08 |