AI 에이전트 시대의 Postgres 베스트 프랙티스! Supabase가 공개한 'Agent Skills'를 활용해 봅시다.

2026-02-13
24분 만에 읽기
업데이트: 2026-02-19
hf_20260213_001608_5722dca2-e989-4d47-9098-66b99d02c9b1.webp

목차

본 글은 Supabase 공식 블로그와 supabase/agent-skills의 공개 문서를 참고해 핵심 내용을 일본어로 재구성한 해설 기사입니다. 명령어 예시 및 SQL 예시는 이해하기 쉽도록 재편집한 샘플을 일부 포함하고 있습니다.


AI 코딩 에이전트가 코드를 작성해 주는 시대가 되어, Cursor나 Claude Code, GitHub Copilot 등 AI가 데이터베이스 쿼리나 스키마 설계까지 도와주는 것은 정말 고마운 일입니다.

하지만 한 가지 큰 과제가 있습니다. AI 에이전트는 코드를 작성하는 것 자체는 잘하지만, 대상 시스템을 깊이 이해한 상태에서 '올바른' 코드를 작성한다고는 할 수 없습니다. 특히 Postgres와 같은 데이터베이스에서는 작동은 하지만 성능이 나쁜 쿼리나 보안에 구멍이 있는 설계가 만들어지기 쉽다고 알려져 있습니다.

그래서 Supabase가 공개한 것이 'Postgres Best Practices for AI Agents'라는 프로젝트입니다. 이 글에서는 그 내용을 소개하면서, Postgres를 더 안전하고 빠르게 사용하기 위한 구체적인 방법을 살펴보겠습니다.

Supabase의 'Agent Skills'란?

Supabase가 공개한 Agent Skills는 AI 코딩 에이전트에게 Postgres의 베스트 프랙티스를 가르치기 위한 지식 베이스(Knowledge Base)입니다.

작동 방식으로는 AI 에이전트가 참조할 수 있는 형식으로 최적화된 문서들을 제공합니다. 31개의 참조 파일이 8개의 카테고리로 분류되어 있어, 에이전트가 쿼리를 작성하거나 스키마를 설계할 때 이 규칙들을 참조하여 올바른 코드를 생성할 수 있도록 되어 있습니다(참조 파일 수는 향후 업데이트에 따라 증감할 수 있습니다).

설치도 간단하여 다음 명령어 하나로 도입할 수 있습니다.

npx skills add https://github.com/supabase/agent-skills --skill supabase-postgres-best-practices

그럼 공개된 가이드라인 중에서 실무 영향도가 큰 항목을 우선순위대로 살펴보겠습니다.

8가지 카테고리와 우선순위

Agent Skills에서는 규칙이 다음 8가지 카테고리로 분류되어 있습니다. 영향도가 큰 순서대로 나열되어 있습니다.

우선순위카테고리영향도
1쿼리 성능CRITICAL
2커넥션 관리CRITICAL
3보안/RLSCRITICAL
4스키마 설계HIGH
5동시성/잠금(Lock)MEDIUM-HIGH
6데이터 액세스 패턴MEDIUM
7모니터링/진단LOW-MEDIUM
8고급 기능LOW

상위 3개는 'CRITICAL'로 분류되어 있으며, 이를 방치하면 심각한 성능 저하나 보안 위험으로 이어질 수 있습니다. 각각의 구체적인 내용을 자세히 알아보겠습니다.

CRITICAL: 쿼리 성능 최적화

인덱스 누락은 최대의 적

※ 본 기사 내의 SQL은 원본 자료의 개념을 설명하기 위해 간략화 및 재구성한 샘플입니다.

Postgres에서 가장 많은 성능 문제는 인덱스 누락입니다. WHERE 절이나 JOIN에서 사용되는 컬럼에 인덱스가 없으면 테이블 전체를 탐색하는 '풀 테이블 스캔(Full Table Scan)'이 발생합니다. 데이터 양이 늘어날수록 영향은 심각해지며, 100~1000배의 속도 저하로 이어지기도 합니다.

먼저 전형적인 안티 패턴을 간략화한 예시로 확인해 보겠습니다.

-- 안티 패턴: 인덱스 없이 필터링
select * from orders where customer_id = 123;
-- → 테이블 전체를 스캔해 버림

대책은 간단합니다. 필터 조건에 사용하는 컬럼에 인덱스를 생성하는 것입니다.

-- 베스트 프랙티스: WHERE 절에서 사용하는 컬럼에 인덱스 생성
create index orders_customer_id_idx on orders (customer_id);

JOIN의 경우도 마찬가지로 외래 키(Foreign Key) 쪽에 인덱스를 생성합니다.

-- JOIN에서 사용하는 컬럼에도 인덱스 생성
create index orders_customer_id_idx on orders (customer_id);

select c.name, o.total
from customers c
join orders o on o.customer_id = c.id;

부분 인덱스(Partial Index)로 효율 향상

테이블의 전체 행이 아니라 특정 조건에 맞는 행만을 대상으로 한 인덱스를 '부분 인덱스(Partial Index)'라고 부릅니다. 인덱스 크기가 5~20분의 1로 줄어들며, 쓰기 및 쿼리 속도 모두 향상시킬 수 있습니다.

-- 안티 패턴: 전체 행을 대상으로 한 인덱스
create index users_email_idx on users (email);

-- 베스트 프랙티스: 활성 사용자만 대상으로 한 부분 인덱스
create index users_email_active_idx on users (email)
  where deleted_at is null;

논리적 삭제(soft delete)된 레코드나 특정 상태의 데이터가 대량으로 존재하는 테이블에서는 부분 인덱스가 특히 유효합니다. 쿼리 측의 WHERE 절과 인덱스의 조건을 일치시키는 것을 잊지 말아야 합니다.

CRITICAL: 커넥션 관리

Postgres의 연결 비용은 높다

Postgres의 연결(Connection)은 하나당 1~3MB의 메모리를 소비합니다. 500명의 동시 사용자가 있다면 그것만으로 500개의 연결이 필요하게 되어 데이터베이스가 다운될 수 있습니다.

이 문제에 대해 일반적인 해결책은 **커넥션 풀링(Connection Pooling)**입니다. 애플리케이션과 데이터베이스 사이에 풀러(PgBouncer 등)를 두고, 소수의 실제 연결을 다수의 클라이언트가 공유하게 합니다.

풀 크기 계산식

최적의 풀 크기는 다음 식으로 산출할 수 있습니다.

풀 크기 = (CPU 코어 수 × 2) + 디스크 스핀들 수

예를 들어 4코어 시스템이라면 풀 크기는 약 10개의 연결로, 500명 이상의 동시 사용자를 처리할 수 있습니다.

트랜잭션 모드와 세션 모드

커넥션 풀러에는 두 가지 모드가 있습니다.

  • Transaction Mode(트랜잭션 모드): 트랜잭션 완료 후 연결을 풀에 반환합니다. 서버리스 함수나 Edge Functions와 같은 일시적인 클라이언트에 최적입니다.
  • Session Mode(세션 모드): 세션 전체에 걸쳐 연결을 유지합니다. Prepared Statements나 임시 테이블을 사용할 경우 필요합니다.

Supabase를 사용하고 있는 경우, PostgREST API를 많이 사용하는 프로젝트에서는 풀 크기를 전체의 40% 정도로 억제하고, 그 외의 경우는 80%까지 할당하는 것이 기준이 됩니다.

CRITICAL: 보안과 Row Level Security(RLS)

애플리케이션 계층만의 필터링은 위험

멀티 테넌트 환경에서 '앱 측의 조건 분기만으로 제어하는' 설계는 위험합니다. 버그나 우회 경로가 하나만 있어도 다른 테넌트의 데이터를 건드릴 가능성이 있습니다.

※ 여기서 말하는 '멀티 테넌트'는 하나의 앱을 여러 고객(회사·팀)이 공유하는 구성을 말합니다. 또한 '다른 테넌트'는 자신의 고객 이외의 데이터를 의미합니다.

**Row Level Security(RLS)**는 데이터베이스 레벨에서 접근 제어를 강제하는 메커니즘입니다. 행 단위로 접근 권한을 설정할 수 있기 때문에 애플리케이션 측에 버그가 있어도 데이터가 보호됩니다.

-- RLS 활성화
alter table orders enable row level security;

-- 테이블 소유자에게도 RLS 강제
alter table orders force row level security;

-- 사용자가 자신의 데이터에만 접근할 수 있는 정책 생성
create policy "Users can only see their own orders"
  on orders for select
  using (user_id = (select auth.uid()));

RLS 성능 최적화하기

RLS는 강력하지만 잘못 작성하면 성능에 큰 영향을 미칩니다. 특히 주의해야 할 점은 인증 함수 호출 비용입니다.

-- 안티 패턴: auth.uid()가 행마다 호출됨
create policy "bad_policy" on orders
  for select using (auth.uid() = user_id);

-- 베스트 프랙티스: select로 감싸서 캐싱되게 함
create policy "good_policy" on orders
  for select using ((select auth.uid()) = user_id);

이 작은 차이가 큰 테이블에서는 100배 이상의 속도 차이를 만들 수 있습니다. auth.uid()(select auth.uid())로 감싸는 것만으로 함수가 한 번만 실행되고 캐시된다고 합니다.

또한 RLS 정책에서 참조되는 컬럼(user_id 등)에는 인덱스를 추가해 두는 것도 중요합니다.

-- RLS에서 사용하는 컬럼에 인덱스 추가
create index orders_user_id_idx on orders (user_id);

Security Definer 함수로 RLS 재귀 방지하기

복잡한 인가 체크(팀 멤버십 확인 등)에서는 security definer 함수로 판정 로직을 분리하면 구현이 안정됩니다. RLS 정책 내에서 참조하는 테이블에도 RLS가 걸려 있으면, 정책 평가 중에 또 다른 정책 평가가 연쇄적으로 일어나 쿼리가 느려지거나 의도치 않은 동작이 발생할 수 있습니다. security definer 함수에 판정을 모아두면, 그러한 재귀적인 평가를 피하면서 정책 본체를 단순하게 유지할 수 있습니다.

-- 팀 멤버십을 확인하는 함수
create or replace function is_team_member(team_id bigint)
returns boolean
language sql
security definer
set search_path = ''
as $$
  select exists (
    select 1 from public.team_members
    where team_members.team_id = $1
    and team_members.user_id = (select auth.uid())
  );
$$;

다만 주의점도 있습니다. security definer 함수는 '작성자의 권한'으로 실행되므로 일반적인 RLS 체크를 일부 건너뛰고 작동합니다. 따라서 API에서 직접 접근할 수 있는 공개 스키마(Exposed schemas)에 두면, 의도치 않게 강력한 권한을 외부에 공개해 버릴 위험이 있습니다. 이런 종류의 함수는 외부 공개하지 않는 스키마에 배치하고, 필요한 처리만 안전하게 호출할 수 있는 형태로 두는 것이 기본입니다.

HIGH: 스키마 설계의 베스트 프랙티스

적절한 데이터 타입 선택하기

데이터 타입의 선택은 스토리지 효율과 쿼리 성능에 직결됩니다. 자주 하는 실수와 권장되는 타입을 정리했습니다.

용도안티 패턴권장이유
IDintbigintint는 21억 행에서 오버플로우 발생
텍스트varchar(255)textPostgres에서는 성능 차이 없음
일시timestamptimestamptz타임존 정보를 유지
불리언text('true'/'false')boolean1바이트 vs 가변 길이
금액textnumeric(10,2)정확한 계산 가능

특히 ID 컬럼에 int를 사용해 버리면 약 21억 레코드에서 오버플로우가 발생합니다. bigint를 사용하면 약 900경 건까지 대응할 수 있으므로 미래의 확장에도 안심할 수 있습니다.

MEDIUM-HIGH: 동시성과 잠금(Lock)

트랜잭션은 짧게 유지하기

장시간 실행되는 트랜잭션은 잠금(Lock)을 계속 유지하여 다른 쿼리를 차단해 버립니다. 특히 흔히 저지르는 실수가 트랜잭션 내에서 외부 API를 호출하는 패턴입니다.

-- 안티 패턴: 트랜잭션 내에서 API 호출
begin;
  select * from orders where id = 1 for update;  -- 잠금 획득
  -- 여기서 외부 API 호출 (2~5초 소요)
  update orders set status = 'paid' where id = 1;
commit;  -- 이 시간 동안 계속 잠금을 유지해 버림
-- 베스트 프랙티스: API 호출은 트랜잭션 밖에서 수행
-- 1. 먼저 외부 API를 호출 (트랜잭션 밖)
-- 2. 결과를 받은 후 트랜잭션 시작
begin;
  update orders set status = 'paid' where id = 1;
commit;  -- 잠금 유지는 밀리초 단위

이처럼 비 데이터베이스 처리를 트랜잭션 밖으로 빼는 것만으로 3~5배의 처리량(Throughput) 개선을 기대할 수 있습니다.

안전책으로 statement_timeout을 설정해 두는 것도 추천합니다.

-- 전역 설정
set statement_timeout = '30s';

-- 세션 단위 설정
set local statement_timeout = '5s';

MEDIUM: 데이터 액세스 패턴

N+1 문제 해결하기

N+1 문제는 1회의 쿼리로 취득한 결과에 대해 루프로 N회의 추가 쿼리를 실행해 버리는 패턴입니다. 데이터베이스 왕복 횟수가 10~100배가 됩니다.

-- 안티 패턴: 루프 내에서 1건씩 쿼리
-- 1회차: select * from users limit 100;
-- 2~101회차: select * from orders where user_id = ?; (100회 반복)
-- → 합계 101회의 쿼리

-- 베스트 프랙티스: 배치로 취득
select * from orders where user_id = any($1::bigint[]);
-- → 배열로 일괄 지정하여 1회의 쿼리로 완료

-- 또는 JOIN 사용
select u.id, u.name, o.*
from users u
left join orders o on o.user_id = u.id;

애플리케이션 측에서 ID를 모은 후 any()로 일괄 취득하거나, JOIN으로 한 번에 취득하는 것 중 하나를 선택합시다.

모니터링: EXPLAIN ANALYZE로 문제 특정하기

성능 문제가 발생했을 때 추측이 아닌 실제 데이터에 근거하여 판단하는 것이 중요합니다. EXPLAIN ANALYZE를 사용하면 쿼리의 실행 계획과 실제 처리 시간을 확인할 수 있습니다.

explain (analyze, buffers, format text)
select * from orders where customer_id = 123;

출력 결과에서 주목할 포인트는 다음과 같습니다.

  • Seq Scan(시퀀셜 스캔): 인덱스가 부족할 가능성
  • Rows Removed by Filter: 필터로 제외된 행 수가 많은 경우 인덱스 추가 검토
  • Buffer reads >> cache hits: 디스크 I/O가 다발하고 있어 메모리가 부족할 가능성
  • Nested Loop의 과도한 반복: JOIN 방식을 재검토할 필요 있음

AI 에이전트와 조합하여 사용하는 방법

지금까지의 내용을 실제 개발에 적용하려면 어떻게 사용하는 것이 효과적일까요? 실천하기 쉬운 방법을 몇 가지 소개합니다.

1. Agent Skills를 프로젝트에 도입하기

앞서 소개한 명령어로 스킬을 설치하면 프로젝트 디렉터리에 참조 파일이 배치됩니다.

npx skills add https://github.com/supabase/agent-skills --skill supabase-postgres-best-practices

AI 에이전트는 이 파일들을 읽어 쿼리 생성이나 리뷰 시에 베스트 프랙티스를 참조할 수 있게 됩니다.

2. Claude Code에서는 플러그인으로도 설치 가능

Claude Code를 사용하고 있는 경우 플러그인으로 직접 설치하는 방법도 제공됩니다.

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

플러그인으로 설치하면 Claude Code가 데이터베이스 관련 작업을 감지했을 때 자동으로 베스트 프랙티스를 참조해 줍니다. "이 Postgres 쿼리를 최적화해 줘"나 "테이블에 적절한 인덱스를 추가해 줘"라고 지시하는 것만으로 Agent Skills의 규칙에 기반한 제안을 얻을 수 있게 됩니다.

물론 앞서 언급한 npx skills add 명령어도 사용할 수 있습니다. 프로젝트 디렉터리에 파일이 배치되면 팀원 전원이 베스트 프랙티스를 공유하고 싶은 경우에 편리합니다.

3. 코드 리뷰 시 체크포인트로 활용하기

AI 에이전트에게 코드 리뷰를 의뢰할 때 다음 관점을 포함하도록 지시하면 좋습니다.

  • WHERE 절이나 JOIN 조건에서 사용하는 컬럼에 인덱스는 있는가
  • RLS 정책에서 auth.uid()(select auth.uid())로 감싸고 있는가
  • 트랜잭션 내에 외부 API 호출이 포함되어 있지 않은가
  • N+1 패턴이 되어 있지 않은가
  • 데이터 타입은 적절한가 (특히 ID 컬럼이 bigint로 되어 있는가)

하지만 매번 이렇게 세세하게 지시를 작성할 필요는 없습니다. supabase-postgres-best-practices 스킬이 도입되어 있다면 "이 변경 사항을 Postgres 베스트 프랙티스 관점에서 리뷰해 줘"와 같이 의뢰하는 것만으로 많은 경우 스킬 내용에 따라 리뷰해 줍니다. 한편, 리뷰 대상이 넓거나 다른 규칙 세트도 병용하고 있는 경우는 "supabase-postgres-best-practices 스킬을 사용하여 리뷰해 줘"라고 명시하는 편이 의도를 전달하기 쉽고 결과도 안정적입니다. 위의 체크리스트는 리뷰의 망라성을 더욱 높이고 싶을 때 추가 프롬프트로 사용하는 이미지입니다.

4. 우선순위에 따라 단계적으로 개선하기

이상적인 것은 영향 범위를 확인한 후 모든 카테고리를 한꺼번에 재검토하는 것입니다. 하지만 현실적으로 공수나 리스크의 제약이 있기 때문에, 어려운 경우 CRITICAL한 카테고리(쿼리 성능, 커넥션 관리, 보안)부터 우선적으로 재검토해 나가는 것이 효과적입니다.

정리

Supabase가 공개한 'Postgres Best Practices for AI Agents'의 내용을 소개했습니다. 핵심 내용을 정리해 봅시다.

  • AI 에이전트는 베스트 프랙티스를 지식으로 가지고 있어도 항상 그것을 적용해 준다고는 할 수 없다는 것이 과제
  • Agent Skills를 도입함으로써 에이전트에게 Postgres의 올바른 작성법을 가르칠 수 있음
  • 우선순위가 가장 높은 3가지 카테고리는 쿼리 성능, 커넥션 관리, 보안/RLS
  • 인덱스 추가, 커넥션 풀링 도입, RLS의 적절한 구현이 가장 영향력 큰 개선책
  • (select auth.uid())와 같은 작은 아이디어로 100배 이상의 속도 개선을 기대할 수 있는 경우도 있음

AI 에이전트와 함께 데이터베이스를 다룰 기회는 앞으로 점점 늘어날 것입니다. 이러한 베스트 프랙티스를 프로젝트에 포함해 두면 AI가 생성하는 코드의 품질을 높일 수 있습니다. 꼭 활용해 보시기 바랍니다.

참고 링크

이 기사 공유하기

관련 기사