Supabase와 직접 구성한 PostgreSQL: 사내 업무 시스템 선택 기준
사내 업무 시스템을 만들 때 데이터베이스를 고르는 질문은 대개 이렇게 나옵니다. 「그냥 PostgreSQL 쓰면 되지 않나요? Supabase 는 뭐가 다른가요?」
맞는 질문입니다. Supabase 의 데이터베이스는 PostgreSQL 이고, 특별한 방언이나 독자 엔진이 아닙니다. 그래서 「DB 를 고르는 문제」로 보면 둘은 같습니다. 실제로 갈리는 것은 그 옆에 딸려 오는 것들입니다.
PostgreSQL 대 Supabase 로 비교하는 시각
이 비교를 「PostgreSQL vs Supabase」로 두면 답이 나오지 않습니다. 둘 다 PostgreSQL 이기 때문입니다.
비교할 대상은 이렇게 놓아야 합니다.
A: 관리형 PostgreSQL + 인증 서비스 + 파일 저장소 + 백업 + 모니터링 + 커넥션 풀러
B: Supabase (위 항목이 한 프로젝트 안에 묶여 있음)A 를 고르면 각 칸을 직접 고르고 잇습니다. 자유도가 높은 대신, 그 연결을 만들고 유지하는 일이 남습니다. 1인 개발사라면 이 「연결을 유지하는 일」의 비용이 생각보다 큽니다.
가장 크게 갈리는 지점: 인증과 행 수준 보안의 연결
기능 목록을 나열하는 것보다, 실제로 차이가 드러나는 한 곳을 보는 편이 빠릅니다.
멀티테넌트 업무 시스템에서 가장 위험한 실수는 조회 조건 하나를 빠뜨리는 것입니다.
-- 의도
select * from orders where tenant_id = $1 and id = $2;
-- 실수
select * from orders where id = $1;앱 코드에만 의존하면 이 한 줄이 곧 다른 회사 데이터 노출입니다. 그래서 PostgreSQL 의 행 수준 보안(RLS)을 켜서 데이터베이스가 한 번 더 거르게 합니다. 여기까지는 어떤 PostgreSQL 이든 같습니다.
차이는 「지금 요청한 사용자가 누구인지」를 데이터베이스가 어떻게 아느냐입니다. Supabase 의 RLS 문서는 자기네 Auth 를 쓰면 auth.uid() 로 요청한 사용자의 id 를 바로 쓸 수 있다고 안내합니다.
create policy "본인 프로필만 조회"
on profiles for select
to authenticated
using ((select auth.uid()) = user_id);직접 구성하는 쪽에서는 이 연결을 스스로 만들어야 합니다. 세션에서 사용자 id 를 꺼내 요청마다 데이터베이스 세션 변수로 넘기고, 정책이 그 값을 읽게 하는 구조입니다. 만들 수 있고, 만들면 동작합니다. 다만 트랜잭션마다 그 값을 세팅하는 코드가 빠지면 정책이 조용히 무력해진다는 점이 위험합니다.
정리하면 RLS 는 어디서나 쓸 수 있지만, Supabase 는 「로그인 → 토큰 → 정책」의 배선을 기본으로 깔아 둡니다. 직접 구성하면 그 배선이 우리 코드의 일부가 됩니다.
딸려 오는 것들의 목록
인증 말고도 프로젝트 하나에 함께 오는 것들이 있습니다. 문서에서 확인되는 범위만 적으면 이렇습니다.
| 항목 | Supabase 에서 |
|---|---|
| MFA | Auth 기능으로 제공. 인증 앱(TOTP)과 전화 방식 문서가 따로 있다 |
| 파일 저장소 | Storage 는 기본적으로 정책 없이는 업로드를 허용하지 않고, storage.objects 테이블의 RLS 정책으로 접근을 연다 |
| 백업 | Pro 플랜은 최근 7일, Team 14일, Enterprise 30일치 일일 백업. 더 촘촘하면 PITR 을 켠다. 무료 플랜은 CLI 로 직접 내보내라고 안내한다 |
| 커넥션 | 서버리스·엣지 함수에는 공유 풀러의 transaction 모드를 권한다. 짧은 연결이 많이 열리기 때문이다 |
마지막 줄이 Vercel 같은 서버리스 환경에서 특히 중요합니다. 함수 인스턴스가 여러 개 뜨면 연결이 그만큼 열립니다. 직접 구성하는 쪽이라면 풀러를 따로 두거나 서버리스용 드라이버를 골라야 합니다. 어느 쪽이든 「연결 방식」은 반드시 결정해야 하는 항목이고, 미루면 운영에서 드러납니다.
그래서 언제 Supabase 를 고르나
제가 보기에 기준은 팀 규모와 보안 책임의 총량입니다.
Supabase 쪽이 맞는 경우
- 1인이나 소수가 만들고 운영한다
- 인증, 권한, 파일 접근 통제를 처음부터 다 만들 시간이 없다
- 멀티테넌트라 데이터 격리를 코드 밖에서도 한 번 더 걸러야 한다
- 서버리스 배포(Vercel 등)라 커넥션 관리가 필요하다
직접 구성이 맞는 경우
- 이미 쓰는 인증 체계(사내 SSO, 기존 회원 DB)가 있어 Auth 를 따로 두면 오히려 이중이 된다
- 데이터베이스가 특정 리전·망 안에 있어야 하는 요구가 있다
- PostgreSQL 설정을 깊게 손봐야 하거나, 관리형에서 막히는 확장을 써야 한다
- 이미 운영 인력과 백업·모니터링 체계가 있다
성능 때문에 고르는 문제는 아니라고 봅니다. 같은 PostgreSQL 이고, 사내 업무 시스템 규모의 부하는 어느 쪽에서도 문제가 되지 않습니다. 고르는 이유는 혼자 운영하면서 빠뜨릴 수 있는 항목의 수를 줄이는 것입니다.
종속의 정도
「Supabase 에 묶이는 것 아닌가」라는 걱정은 층을 나눠 보면 정도가 달라집니다.
| 층 | 옮기기 |
|---|---|
| 데이터 (PostgreSQL) | 표준 PostgreSQL 이라 덤프와 복원으로 옮길 수 있다 |
| RLS 정책 | SQL 이므로 함께 옮겨간다. 다만 auth.uid() 같은 헬퍼는 대체가 필요하다 |
| Auth (사용자 계정·해시) | 옮길 수 있지만 세션·소셜 로그인 설정은 다시 만든다 |
| Storage | 파일은 옮기고, 접근 정책은 새 저장소의 방식으로 다시 쓴다 |
| Realtime·Edge Function 등 | 쓰고 있었다면 대체 구성을 새로 만든다 |
핵심이 PostgreSQL 이라는 점이 여기서 값을 합니다. 데이터 자체는 표준 도구로 나갈 수 있습니다. 대신 플랫폼 기능을 깊게 쓸수록 이전 비용이 커집니다. 이전 가능성을 중요하게 본다면 RLS 정책에서 플랫폼 헬퍼 함수 사용을 한 군데로 모아 두는 정도의 준비는 해 둘 만합니다.
고르기 전에 답해 둘 것
| 질문 | 확인 |
|---|---|
| 인증을 직접 만들 것인가 | 만들면 MFA·세션·재설정까지 포함해서 본다 |
| 멀티테넌트인가 | 그렇다면 RLS 를 어느 쪽이든 켠다 |
| 배포가 서버리스인가 | 커넥션 방식(풀러·전용 드라이버)을 먼저 정한다 |
| 백업 보관 기간은 얼마나 필요한가 | 플랜별 보관 일수와 PITR 필요 여부를 확인한다 |
| 파일 접근 통제가 필요한가 | 저장소의 권한 모델을 먼저 본다 |
이 다섯 개를 답하고 나면 Supabase 든 직접 구성이든 결정이 거의 정해진다고 봅니다. 답을 미룬 채 「일단 PostgreSQL 하나 띄우고 시작」하면, 나중에 다섯 개를 운영 중에 하나씩 만나게 됩니다.
여기까지가 확실한 부분
auth.uid() 로 RLS 정책을 쓰는 방식, Storage 가 정책 없이는 업로드를 허용하지 않는다는 점, 플랜별 일일 백업 보관 기간과 PITR 안내, 서버리스 환경에 transaction 모드 공유 풀러를 권한다는 점, MFA 기능의 존재는 Supabase 문서 기준입니다. 직접 구성할 때 세션 변수로 사용자 id 를 넘기는 방식은 PostgreSQL 에서 흔히 쓰는 패턴이고, 구체적인 구현은 쓰는 드라이버와 ORM 에 따라 다릅니다. 요금과 플랜별 한도는 바뀌므로 결정 시점에 각 서비스의 가격 페이지를 다시 확인해야 합니다.
함께 읽기
- Supabase 풀러 계정 이름: 운영이 두 번 죽은 이유환경변수의 정본을 볼트로 옮기는 작업이었습니다. 값을 한곳에 모으고, 거기서 배포처로 밀고, 어긋나면 대조로 잡는 구조입니다. 마지막 단계가 운영이었습니다.
- PlanetScale + Vercel: DATABASE_URL과 DIRECT_URL을 나눠야 하는 이유접속 문자열을 정리한다며 시크릿 이름 하나를 통일했습니다. 그날 밤 운영 사이트 로그인이 죽었고, 백업은 아무 소리 없이 깨졌습니다.
- Supabase에서 PlanetScale로: 이관이 드러낸 커넥션 풀 기본값운영 데이터베이스를 Supabase에서 PlanetScale Postgres로 옮겼습니다. 옮기고 나서 사이트가 조금 굼떠진 것 같았습니다. 클라우드를 건너뛰게 됐으니 그 대가겠거니 생각했는데, 재보니 아니었습니다. 원인은 이관과 상관없이 원래 있던 커넥션 풀 설정이었고, 그것도 제 첫 측정으로는 보이지 않았습니다.
- PostgreSQL autovacuum: 지운 행이 디스크에서 사라지지 않는 이유테이블에서 100만 행을 지웠습니다. DELETE는 성공했고 SELECT count()는 0을 돌려줍니다.
- Neon vs Supabase: 리전, egress, 리얼타임 비교Vercel 서울 리전에서 도는 Next.js 앱에 붙일 관리형 Postgres 를 고르는 중이었습니다. 처음에는 두 서비스의 가격표를 나란히 놓고 비교하면 끝날 일이라고 생각했습니다. 실제로는 가격이 아니라, 나중에 되돌릴 수 없는 것들에서 갈렸습니다.