본문 바로가기

Programmer/AI

AI 에이전트를 위한 PostgreSQL 베스트 프랙티스: Supabase Agent Skills 완벽 가이드

반응형

> 이 글은 [supabase/agent-skills](https://github.com/supabase/agent-skills)를 기반으로 정리했습니다.


들어가며

AI 코딩 에이전트가 데이터베이스 스키마를 설계하고 쿼리를 작성하는 시대가 되었습니다. Claude Code, Cursor, GitHub Copilot 같은 도구들이 SQL 코드를 생성하고, MCP(Model Context Protocol)를 통해 데이터베이스와 직접 상호작용하기도 합니다.

하지만 AI 에이전트가 생성한 코드가 항상 최적은 아닙니다. 인덱스 없이 대용량 테이블을 조회하거나, RLS(Row Level Security) 정책을 비효율적으로 구성하거나, 커넥션 풀링을 무시한 채 직접 연결을 남발하는 경우가 흔합니다.

Supabase는 이 문제를 해결하기 위해 Agent Skills 포맷을 따르는 postgres-best-practices 스킬을 공개했습니다. Agent Skills는 AI 에이전트가 특정 도메인의 베스트 프랙티스를 학습할 수 있도록 구조화된 지식을 제공하는 표준 포맷입니다.

이 글의 대상 독자

이 글은 두 부류의 독자를 위해 작성되었습니다.

  • PostgreSQL 사용자: AWS RDS, Google Cloud SQL, 자체 호스팅 PostgreSQL 등을 사용하는 모든 개발자
  • Supabase 사용자: Supabase 플랫폼을 사용하는 개발자

각 섹션에 🐘 (PostgreSQL 공통) 또는 ⚡ (Supabase 특화) 표시를 해두었습니다.


Agent Skills란 무엇인가

Agent Skills는 AI 에이전트가 발견하고 활용할 수 있는 지침, 스크립트, 리소스의 표준 포맷입니다. Claude Code, Cursor, GitHub Copilot 등의 에이전트가 특정 작업을 수행할 때 더 정확하고 효율적으로 동작하도록 돕습니다.

Supabase는 이 포맷을 따르는 postgres-best-practices 스킬을 공개했습니다. 8개 카테고리에 걸친 PostgreSQL 성능 최적화 규칙들을 담고 있으며, 영향도에 따라 우선순위가 매겨져 있습니다.

설치는 간단합니다.

npx skills add supabase/agent-skills

Claude Code를 사용한다면 플러그인으로도 설치할 수 있습니다.

/plugin marketplace add supabase/agent-skills
/plugin install postgres-best-practices@supabase-agent-skills

영향도 레벨 이해하기 🐘

모든 최적화가 동일한 가치를 갖지는 않습니다. Agent Skills는 각 규칙의 영향도를 명확히 구분합니다. 이 분류 체계는 PostgreSQL 전반에 적용됩니다.

레벨 성능 개선 폭 예시

CRITICAL 10-100배 누락된 인덱스, 커넥션 고갈
HIGH 5-20배 잘못된 인덱스 타입, 부적절한 파티셔닝
MEDIUM-HIGH 2-5배 N+1 쿼리, RLS 최적화 미흡
MEDIUM 1.5-3배 중복 인덱스, 오래된 통계 정보
LOW-MEDIUM 1.2-2배 VACUUM 튜닝, 설정 조정
LOW 점진적 고급 패턴, 엣지 케이스

Part 1: PostgreSQL 공통 베스트 프랙티스 🐘

이 섹션의 내용은 모든 PostgreSQL 환경에서 적용됩니다. AWS RDS, Google Cloud SQL, Azure Database for PostgreSQL, 자체 호스팅 환경 모두 해당됩니다.


1. 쿼리 성능 최적화 (Critical) 🐘

인덱스 기본 전략

쿼리 성능 문제의 대부분은 인덱스 부재에서 시작됩니다. 100,000건의 테이블에서 인덱스 없이 특정 사용자의 데이터를 조회하면, PostgreSQL은 모든 행을 순차적으로 스캔합니다.

-- ❌ 나쁜 예: 인덱스 없는 조회
SELECT * FROM orders WHERE user_id = 'abc123';

-- ✅ 좋은 예: 인덱스 생성 후 조회
CREATE INDEX idx_orders_user_id ON orders(user_id);
SELECT * FROM orders WHERE user_id = 'abc123';

대용량 테이블에서 이 차이는 100배 이상의 성능 개선으로 이어집니다.

인덱스 타입 선택

B-Tree는 동등 비교(=)와 범위 쿼리(>, <, BETWEEN)에 최적화되어 있습니다. 하지만 배열 연산에는 GIN이 적합합니다.

-- 동등/범위 비교: B-Tree (기본값)
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_orders_created_at ON orders(created_at);

-- 배열 포함 여부 확인: GIN
CREATE INDEX idx_tags ON posts USING GIN (tags);

-- 전문 검색: GIN with tsvector
CREATE INDEX idx_content_search ON articles USING GIN (to_tsvector('english', content));

EXPLAIN ANALYZE 활용

쿼리 실행 계획을 확인하여 문제를 진단합니다.

EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 'abc123';

결과에서 확인할 사항:

  • Seq Scan: 대용량 테이블에서 나타나면 인덱스 필요
  • Index Scan / Index Only Scan: 인덱스가 사용되고 있음
  • actual time: 실제 실행 시간
  • rows: 처리된 행 수

2. 스키마 설계 (High) 🐘

기본 키 필수

모든 테이블에는 기본 키가 있어야 합니다. 기본 키가 없으면 PostgreSQL이 행을 고유하게 식별할 수 없어 복제, 업데이트, 삭제 작업에서 문제가 발생합니다.

-- ✅ 기본 키 포함
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email TEXT NOT NULL UNIQUE,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

외래 키 인덱스

외래 키 컬럼에는 인덱스를 추가하는 것이 표준 관행입니다. JOIN 성능이 크게 향상됩니다.

-- 외래 키 정의
ALTER TABLE orders ADD CONSTRAINT fk_orders_customer 
    FOREIGN KEY (customer_id) REFERENCES customers(id);

-- ✅ 외래 키 컬럼에 인덱스 추가
CREATE INDEX idx_orders_customer_id ON orders(customer_id);

PostgreSQL은 외래 키에 자동으로 인덱스를 생성하지 않습니다. 명시적으로 추가해야 합니다.


3. Row Level Security (RLS) 🐘

RLS는 PostgreSQL 9.5부터 지원되는 기본 기능입니다. 테이블 수준에서 행 단위 접근 제어를 구현합니다.

RLS 기본 사용법

-- RLS 활성화
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- 정책 생성: 자신의 게시물만 조회 가능
CREATE POLICY "Users see own posts" ON posts
    FOR SELECT
    USING (user_id = current_user_id());  -- current_user_id()는 앱에서 정의

RLS 성능 최적화

RLS 정책은 모든 SELECT, UPDATE 작업에서 각 행마다 평가됩니다. 함수를 직접 호출하면 행 수만큼 반복 실행됩니다.

-- ❌ 나쁜 예: 매 행마다 함수 호출
CREATE POLICY "own_data" ON posts
    FOR SELECT USING (get_current_user_id() = user_id);

-- ✅ 좋은 예: subquery로 캐싱
CREATE POLICY "own_data" ON posts
    FOR SELECT USING ((SELECT get_current_user_id()) = user_id);

SELECT 서브쿼리로 감싸면 PostgreSQL 옵티마이저가 initPlan을 실행하여 결과를 캐싱합니다. 100,000건 테이블에서 100배 이상의 성능 차이가 발생할 수 있습니다.

RLS 컬럼 인덱싱

RLS 정책에서 사용하는 컬럼에는 반드시 인덱스를 추가합니다.

-- RLS 정책이 user_id를 사용한다면
CREATE INDEX idx_posts_user_id ON posts(user_id);

팀 기반 접근 제어

쿼리 방향이 성능에 큰 영향을 미칩니다.

-- ❌ 느린 방향: 각 게시물마다 팀 멤버십 조회
get_current_user_id() IN (
    SELECT user_id FROM team_members 
    WHERE team_members.team_id = posts.team_id
)

-- ✅ 빠른 방향: 사용자의 팀 목록을 한 번만 조회
team_id IN (
    SELECT team_id FROM team_members 
    WHERE user_id = get_current_user_id()
)

4. 동시성과 락킹 (Medium-High) 🐘

논블로킹 인덱스 생성

일반 CREATE INDEX는 테이블에 쓰기 락을 겁니다. 프로덕션 환경에서는 CONCURRENTLY 옵션을 사용합니다.

-- ❌ 테이블 락 발생
CREATE INDEX idx_users_email ON users(email);

-- ✅ 논블로킹 (시간은 더 걸림)
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);

대규모 배치 처리

대량의 UPDATE/DELETE는 청크 단위로 나눕니다.

-- ❌ 한 번에 처리: 긴 락, 트랜잭션 로그 폭증
DELETE FROM logs WHERE created_at < '2024-01-01';

-- ✅ 청크 단위 처리
DO $$
DECLARE
    deleted_count INT;
BEGIN
    LOOP
        DELETE FROM logs 
        WHERE id IN (
            SELECT id FROM logs 
            WHERE created_at < '2024-01-01' 
            LIMIT 10000
        );
        GET DIAGNOSTICS deleted_count = ROW_COUNT;
        EXIT WHEN deleted_count = 0;
        COMMIT;
    END LOOP;
END $$;

5. 커넥션 관리 (Critical) 🐘

PostgreSQL 커넥션은 리소스를 많이 소모합니다. 커넥션당 약 10MB의 메모리를 사용하며, 최대 커넥션 수에 제한이 있습니다.

커넥션 풀링 필수

서버리스 환경(AWS Lambda, Vercel Functions 등)에서는 커넥션 풀러가 필수입니다.

일반적인 옵션:

  • PgBouncer: 가장 널리 사용되는 PostgreSQL 커넥션 풀러
  • pgpool-II: 로드 밸런싱, 복제 기능 포함
  • 애플리케이션 레벨: Prisma, SQLAlchemy 등의 내장 풀링
# PgBouncer 설정 예시 (pgbouncer.ini)
[databases]
mydb = host=localhost dbname=mydb

[pgbouncer]
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20

풀 모드 선택

모드 설명 사용 사례

Session 세션 동안 커넥션 유지 장기 연결, Prepared Statement 사용
Transaction 트랜잭션 동안만 커넥션 사용 서버리스, 짧은 쿼리
Statement 문장 단위로 커넥션 할당 AUTOCOMMIT만 사용하는 경우

서버리스 환경에서는 Transaction 모드를 권장합니다.


6. 모니터링과 진단 (Low-Medium) 🐘

느린 쿼리 로깅

-- postgresql.conf 또는 ALTER SYSTEM
ALTER SYSTEM SET log_min_duration_statement = 1000;  -- 1초 이상 쿼리 로깅
SELECT pg_reload_conf();

pg_stat_statements 활용

-- 확장 활성화
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

-- 가장 시간이 오래 걸린 쿼리 Top 10
SELECT 
    substring(query, 1, 100) as query,
    calls,
    round(total_exec_time::numeric, 2) as total_ms,
    round(mean_exec_time::numeric, 2) as mean_ms
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

커넥션 상태 확인

-- 현재 활성 커넥션
SELECT 
    state,
    count(*) 
FROM pg_stat_activity 
GROUP BY state;

-- 오래 실행 중인 쿼리
SELECT 
    pid,
    now() - pg_stat_activity.query_start AS duration,
    query
FROM pg_stat_activity
WHERE state = 'active'
  AND now() - pg_stat_activity.query_start > interval '5 minutes';

Part 2: Supabase 특화 베스트 프랙티스 ⚡

이 섹션의 내용은 Supabase 플랫폼 사용자에게만 해당됩니다.


1. Supabase 인증 함수와 RLS ⚡

Supabase는 RLS에서 사용할 수 있는 인증 헬퍼 함수를 제공합니다.

auth.uid()와 auth.jwt()

-- 현재 로그인한 사용자 ID
SELECT auth.uid();

-- JWT 전체 내용
SELECT auth.jwt();

-- JWT에서 특정 클레임 추출
SELECT auth.jwt() -> 'app_metadata' ->> 'role';

Supabase RLS 최적화

-- ❌ 나쁜 예
CREATE POLICY "Users see own data" ON posts
    FOR SELECT USING (auth.uid() = user_id);

-- ✅ 좋은 예: subquery 캐싱
CREATE POLICY "Users see own data" ON posts
    FOR SELECT USING ((SELECT auth.uid()) = user_id);

역할 명시적 지정

anon 역할을 제외하려면 명시적으로 authenticated를 지정합니다.

-- ❌ 모든 역할에 적용
CREATE POLICY "Read posts" ON posts FOR SELECT USING (true);

-- ✅ authenticated 역할만
CREATE POLICY "Read posts" ON posts 
    FOR SELECT TO authenticated USING (true);

정책 분리

Supabase에서는 FOR ALL 대신 개별 정책을 권장합니다.

-- ❌ 피해야 할 패턴
CREATE POLICY "all_access" ON posts FOR ALL USING (...);

-- ✅ 권장 패턴
CREATE POLICY "select_own" ON posts FOR SELECT USING ((SELECT auth.uid()) = user_id);
CREATE POLICY "insert_own" ON posts FOR INSERT WITH CHECK ((SELECT auth.uid()) = user_id);
CREATE POLICY "update_own" ON posts FOR UPDATE USING ((SELECT auth.uid()) = user_id);
CREATE POLICY "delete_own" ON posts FOR DELETE USING ((SELECT auth.uid()) = user_id);

2. Supabase 커넥션 풀링 ⚡

Supabase는 세 가지 연결 방식을 제공합니다.

연결 방식 비교

방식 포트 사용 사례 플랜

Direct Connection 5432 서버 환경, 마이그레이션 모든 플랜
Shared Pooler (Supavisor) 6543 서버리스, IPv6 모든 플랜
Dedicated Pooler (PgBouncer) 6543 고성능 서버리스 유료 플랜만

연결 문자열 예시

# 직접 연결
postgresql://postgres:[PASSWORD]@db.[PROJECT].supabase.co:5432/postgres

# Shared Pooler (Supavisor) - 트랜잭션 모드
postgresql://postgres.[PROJECT]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres

# Dedicated Pooler (PgBouncer) - 유료 플랜
postgresql://postgres:[PASSWORD]@db.[PROJECT].supabase.co:6543/postgres

Prisma와 함께 사용

# 런타임: 풀링 연결 사용
DATABASE_URL="postgresql://postgres.[PROJECT]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres?pgbouncer=true"

# 마이그레이션: 직접 연결 사용
DIRECT_URL="postgresql://postgres:[PASSWORD]@db.[PROJECT].supabase.co:5432/postgres"

?pgbouncer=true를 추가하면 Prisma가 Prepared Statement를 비활성화합니다 (트랜잭션 모드에서 필요).


3. MCP 서버 보안 ⚡

AI 에이전트가 MCP를 통해 Supabase 데이터베이스에 직접 접근할 때의 보안 고려사항입니다.

핵심 원칙

  1. 프로덕션 연결 금지: 개발 프로젝트에만 연결
  2. 고객에게 제공 금지: 내부 개발 도구로만 사용
  3. 읽기 전용 모드 권장: 실수로 인한 데이터 손실 방지
  4. 프로젝트 범위 제한: 특정 프로젝트만 접근 허용

안전한 MCP URL 설정

https://mcp.supabase.com/mcp?read_only=true&project_ref=[YOUR_PROJECT]

MCP 클라이언트 설정 (예: Claude Code)

{
  "mcpServers": {
    "supabase": {
      "type": "http",
      "url": "https://mcp.supabase.com/mcp?read_only=true&project_ref=YOUR_PROJECT"
    }
  }
}

4. Supabase Dashboard 도구 활용 ⚡

Performance Advisor

대시보드의 Database > Performance Advisor에서 자동으로 감지하는 항목:

  • 누락된 인덱스
  • 사용되지 않는 인덱스
  • 중복 인덱스
  • 테이블 bloat

Security Advisor

Database > Security Advisor에서 확인:

  • RLS가 비활성화된 테이블
  • RLS가 활성화되었지만 정책이 없는 테이블
  • 노출된 auth 스키마
  • 위험한 security definer 함수

실전 적용 체크리스트

PostgreSQL 공통 체크리스트 🐘

새로운 테이블을 만들 때:

  • [ ] 기본 키가 있는가?
  • [ ] 외래 키 컬럼에 인덱스가 있는가?
  • [ ] WHERE 절에 자주 사용되는 컬럼에 인덱스가 있는가?
  • [ ] RLS를 사용한다면 정책 컬럼에 인덱스가 있는가?
  • [ ] RLS 정책에서 함수를 (SELECT func()) 패턴으로 캐싱하는가?
  • [ ] 서버리스 환경에서 커넥션 풀링을 사용하는가?

Supabase 추가 체크리스트 ⚡

  • [ ] auth.uid() 대신 (SELECT auth.uid())를 사용하는가?
  • [ ] 정책에 TO authenticated를 명시했는가?
  • [ ] FOR ALL 대신 개별 정책(SELECT/INSERT/UPDATE/DELETE)을 사용하는가?
  • [ ] 서버리스 환경에서 Pooler 연결(포트 6543)을 사용하는가?
  • [ ] Prisma 사용 시 ?pgbouncer=true를 추가했는가?

마치며

이 글에서 다룬 PostgreSQL 베스트 프랙티스의 핵심을 요약하면:

모든 PostgreSQL 사용자에게 중요한 것:

  • 적절한 인덱스 설계 (10-100배 성능 차이)
  • RLS 정책의 함수 캐싱 패턴
  • 커넥션 풀링 (특히 서버리스 환경)
  • EXPLAIN ANALYZE를 통한 쿼리 분석

Supabase 사용자에게 추가로 중요한 것:

  • auth.uid()/auth.jwt() 최적화
  • Supavisor/PgBouncer 연결 방식 선택
  • MCP 서버 보안 설정

Agent Skills를 설치하면 AI 에이전트가 이런 사항들을 자동으로 고려합니다. Supabase를 사용하지 않더라도 PostgreSQL 공통 부분은 충분히 가치 있는 내용입니다.


참고 자료

PostgreSQL 공통 🐘

Supabase 특화 ⚡

반응형