생성형 웹 애플리케이션의 테넌트별 전용 데이터베이스 격리 전략
소프트웨어 아키텍처에서 멀티테넌시(Multi-tenancy)를 설계할 때 데이터 격리는 늘 까다로운 저울질의 대상이었습니다. 하나의 거대한 관계형 데이터베이스 인스턴스 안에 모든 고객의 데이터를 모아 넣고 테넌트 식별자 컬럼으로 구분하는 방식은 오랫동안 표준처럼 여겨졌습니다. 인프라 운영 비용을 아끼고 연결 풀을 효율적으로 관리하려면 다른 대안을 떠올리기 어려웠기 때문입니다. 데이터베이스를 테넌트마다 새로 띄우는 방식은 엔터프라이즈 환경의 값비싼 전용 인프라에서나 검토되던 사치에 가까웠습니다.
하지만 사용자의 자연어 프롬프트 한 줄로 프론트엔드 코드부터 백엔드 API까지 통째로 찍어내는 생성형 애플리케이션 파이프라인이 등장하면서 이 전통적인 룰이 흔들리고 있습니다. 코드를 실시간으로 합성해 배포하는 에이전트가 늘어나자, 공유 데이터베이스의 테이블 스키마를 동적으로 수정하거나 복잡한 행 수준 보안(Row Level Security, RLS) 정책을 프롬프트 엔지니어링으로 주입하는 일 자체가 거대한 기술 부채이자 보안 취약점으로 떠올랐습니다.
GeekNews에 따르면, iMessage 기반의 개인 비서 서비스 Poke는 사용자가 문자로 요청한 웹사이트를 즉시 생성해 Vercel에 배포하고, 생성된 사이트마다 별도의 Turso 데이터베이스를 자동으로 프로비저닝하여 연결하는 방식을 채택했습니다. 이는 단순한 기능 조합이 아니라, AI가 주도하는 소프트웨어 생태계에서 데이터 인프라의 단위가 어떻게 재편되고 있는지를 보여주는 상징적인 사례입니다. 왜 현대의 애플리케이션 아키텍트는 다시금 '사이트당 전용 데이터베이스'라는 급진적인 격리 모델을 선택하고 있는지 들여다볼 필요가 있습니다.
거대 공유 인스턴스에서 테넌트별 분리 인스턴스로의 패러다임 전환
공유 데이터베이스 모델은 근본적으로 애플리케이션 레이어의 무결성에 기댈 수밖에 없는 구조입니다. 모든 쿼리에 테넌트 고유 ID를 누락 없이 바인딩해야 하고, ORM 설정이나 SQL 인터셉터가 단 한 번이라도 오작동하면 옆 테넌트의 개인정보가 그대로 노출되는 참사가 벌어집니다. 이를 방어하기 위해 데이터베이스 엔진 수준의 행 수준 보안(RLS) 정책을 도입하지만, 복잡한 조인이 얽힌 쿼리에서 RLS는 종종 쿼리 플래너를 혼란에 빠뜨려 성능 저하를 일으킵니다.
AI가 애플리케이션 코드를 자율적으로 작성하는 환경에서는 이 위험이 몇 배로 증폭됩니다. 거대 언어 모델(LLM)이 생성한 백엔드 코드가 테넌트 격리 조건을 빠뜨리지 않고 완벽하게 작성하리라는 보장은 어디에도 없습니다. 데이터베이스 유저 권한을 쪼개고 스키마를 논리적으로 분리하더라도, 같은 인스턴스 안에 머무르는 한 연결 풀 고갈이나 특정 테넌트의 악의적 슬로우 쿼리로 인한 리소스 고갈(Noisy Neighbor) 문제를 완벽히 차단하기 어렵습니다.
웹사이트나 테넌트마다 물리적·논리적으로 독립된 데이터베이스를 배정하면 이 모든 복잡성이 일거에 해소됩니다. 애플리케이션 코드는 테넌트 ID 조건을 의식할 필요 없이 일반적인 단일 사용자 시스템처럼 작성될 수 있습니다. 생성된 사이트가 침해당하거나 잘못된 쿼리를 날리더라도 그 영향은 해당 사이트의 전용 데이터베이스 파일 하나에 갇힙니다. 보안 격리의 책임을 불안정한 코드 생성 프롬프트가 아니라 인프라의 물리적 경계로 되돌리는 셈입니다.
서버리스 임베디드 엔진이 무너뜨린 프로비저닝 장벽
테넌트마다 데이터베이스를 따로 주는 발상이 과거에 외면받았던 이유는 명확합니다. 전통적인 RDBMS 인스턴스를 하나 띄우려면 수백 메가바이트의 메모리가 상시 점유되고, 가상 머신이나 컨테이너를 프로비저닝하는 데 수십 초에서 수분의 지연 시간이 발생했기 때문입니다. 수천, 수만 개의 사이트가 실시간으로 생겨나는 서비스에서 인스턴스 단위 격리는 비용과 속도 양면에서 불가능에 가까웠습니다.
이 병목을 허문 주역이 바로 SQLite 호환 클라우드 플랫폼의 진화입니다. Turso처럼 libSQL을 기반으로 동작하는 분산 데이터베이스는 SQLite 본연의 가벼움을 그대로 유지하면서도 클라우드 환경에 맞는 네트워크 인터페이스와 복제 계층을 덧붙였습니다. SQLite는 디스크 상의 단일 파일로 동작하므로 인스턴스를 생성한다는 것은 사실상 빈 데이터베이스 파일 하나를 할당하고 메타데이터를 등록하는 것에 불과합니다.
typescript
import { createClient } from "@libsql/client";
// AI가 사이트 배포 파이프라인에서 호출하는 전용 DB 프로비저닝 로직 예시
async function provisionIsolatedSite(siteId: string) {
// 플랫폼 API를 통해 테넌트 전용 원격 libSQL DB 인스턴스를 즉각 생성
const dbResponse = await fetch(`https://api.turso.tech/v1/organizations/my-org/databases`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TURSO_API_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: `site-${siteId}`,
group: "default",
}),
});
const dbData = await dbResponse.json();
const dbUrl = `libsql://${dbData.database.Hostname}`;
// 생성된 전용 DB에 초기 스키마 마이그레이션 실행
const client = createClient({
url: dbUrl,
authToken: dbData.database.jwt,
});
await client.execute(`
CREATE TABLE IF NOT EXISTS site_content (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
body TEXT NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
return { dbUrl, authToken: dbData.database.jwt };
}
이러한 아키텍처에서는 새로운 테넌트용 데이터베이스를 프로비저닝하는 작업이 밀리초 단위로 끝납니다. Vercel이나 Cloudflare Workers 같은 엣지 런타임에서 웹사이트가 빌드되고 배포되는 속도에 데이터베이스 준비 속도가 정확히 맞아떨어지는 것입니다. 유휴 상태(Idle)에 머무는 데이터베이스는 메모리를 상시 점유하지 않고 스토리지 계층으로 내려가 잠들기 때문에, 수만 개의 데이터베이스를 유지하더라도 활성 연결이 발생할 때만 비용이 청구되는 서버리스 경제성이 성립합니다.
분산 파일 기반 아키텍처가 치러야 하는 대가
그렇다고 테넌트별 데이터베이스 분리 모델이 모든 백엔드 아키텍처의 만병통치약은 아닙니다. 구조가 단순해진 애플리케이션 코드의 이면에는 인프라 레벨에서 감당해야 할 무거운 트레이드오프가 도사리고 있습니다.
가장 먼저 맞닥뜨리는 장벽은 전체 테넌트를 아우르는 분석 쿼리와 교차 데이터 조회(Cross-database Query)의 불가능성입니다. 단일 공유 데이터베이스에서는 GROUP BY 한 번으로 끝날 전체 사용자 통계 집계가, 수천 개로 쪼개진 데이터베이스 환경에서는 불가능합니다. 모든 데이터베이스를 순회하며 데이터를 긁어모으는 별도의 ETL 파이프라인을 구축하거나, 데이터 변경 이벤트를 중앙 데이터 웨어하우스로 쏴주는 이벤트 스트리밍 시스템을 따로 운영해야 합니다.
스키마 마이그레이션의 운영 복잡도 역시 기하급수적으로 증가합니다. 테이블 구조를 변경해야 할 때, 단일 데이터베이스라면 한 번의 DDL 트랜잭션으로 끝납니다. 하지만 테넌트별로 데이터베이스가 쪼개져 있다면 수만 번의 마이그레이션 API 호출을 비동기로 조율해야 합니다. 네트워크 오류로 일부 데이터베이스에서만 마이그레이션이 실패했을 때 발생하는 버전 파편화와 롤백 문제는 운영팀에게 상당한 부담을 안겨줍니다.
동시 쓰기 처리량의 한계도 분명합니다. SQLite 계층은 단일 파일 락(Lock)을 기반으로 동작하기 때문에 쓰기 작업이 고도로 집중되는 워크로드에서는 성능 병목이 발생할 수 있습니다. 각 사이트가 개인용 블로그나 소규모 커뮤니티 수준을 넘어 초당 수백 건의 트랜잭션을 쏟아내는 서비스로 성장한다면, 결국 전통적인 고성능 분산 데이터베이스로의 재이전이라는 아키텍처 전환 비용을 치러야 합니다.
데이터 격리 전략을 선택하는 아키텍트의 기준
단일 공유 데이터베이스와 테넌트별 전용 데이터베이스 중 무엇을 택할 것인가는 서비스가 다루는 비즈니스의 생명주기와 데이터 결합도에 달려 있습니다.
테넌트 간의 데이터 공유가 원천적으로 필요 없고, 각 테넌트의 라이프사이클이 철저히 독립적인 서비스라면 전용 데이터베이스 모델이 압도적인 생산성과 안정성을 제공합니다. Poke의 사례처럼 AI가 사용자의 요구에 맞춰 일회성 혹은 독립적인 웹 애플리케이션을 찍어내는 도메인, 화이트라벨 SaaS, 노코드 웹 빌더 플랫폼 등이 여기에 완벽히 부합합니다. 테넌트가 서비스를 탈퇴할 때도 복잡한 삭제 쿼리 대신 해당 데이터베이스 파일 자체를 날려버리면 그만입니다.
반면 테넌트 사이의 소셜 상호작용이 핵심이거나, 플랫폼 전체 사용자의 통합 랭킹, 글로벌 검색, 실시간 교차 정산이 필수적인 도메인에서는 이 모델을 피해야 합니다. 분리된 인스턴스를 하나로 묶기 위해 애플리케이션 레이어에서 분산 트랜잭션을 흉내 내는 순간 시스템은 걷잡을 수 없이 망가집니다.
기술의 진보는 격리의 단위를 지속적으로 낮춰왔습니다. 물리 서버에서 가상 머신으로, 컨테이너에서 서버리스 함수로 격리 단위가 쪼개졌듯, 이제 데이터 계층 역시 거대한 모놀리식 클러스터에서 초경량 분산 데이터베이스 단위로 분해되고 있습니다. 여러분이 설계 중인 시스템이 AI 에이전트의 동적 코드 생성이나 극단적인 테넌트 독립성을 요구하고 있다면, 데이터베이스를 하나 더 띄우는 비용이 이제는 거의 '0'에 가까워졌다는 사실을 아키텍처 계산기에 반영해야 할 때입니다.
댓글 0