Citus 기반 분산 PostgreSQL 환경에서 다중 테넌트 시스템 구현

SaaS 애플리케이션 구축 시 데이터 모델에 테넌시 개념이 포함되는 경우가 많습니다. 일반적으로 대부분의 정보는 테넌트/고객/계정과 연관되며, 데이터베이스 테이블은 이 관계를 구현합니다.

단일 PostgreSQL 인스턴스 내에서 테넌트 데이터를 격리 상태로 통합 저장하면 세 가지 이점이 있습니다: 애플리케이션 개선 사항이 전체 클라이언트에 적용되며, 하드웨어 사용 효율이 향상되고, 다중 테넌트 관리가 단순화됩니다.

기존 관계형 데이터베이스는 대규모 다중 테넌트 애플리케이션의 데이터 볼륨을 처리하기 어렵습니다. Citus는 단일 PostgreSQL 데이터베이스처럼 동작하도록 설계되었으며, 실제로는 수평 확장 가능한 머신 클러스터로 구성됩니다.

광고 분석 시스템 사례

온라인 광고 성과 추적 및 분석 대시보드 애플리케이션 백엔드를 구현합니다. 사용자 데이터 요청이 단일 회사에 집중되는 특성상 다중 테넌트에 적합합니다.

최적화된 스키마 설계

CREATE TABLE organizations (
  org_id bigserial PRIMARY KEY,
  org_name text NOT NULL,
  logo_url text,
  created_time timestamp NOT NULL,
  updated_time timestamp NOT NULL
);

CREATE TABLE marketing_campaigns (
  campaign_id bigserial,
  org_id bigint REFERENCES organizations (org_id),
  campaign_name text NOT NULL,
  pricing_strategy text NOT NULL,
  status text NOT NULL,
  monthly_budget bigint,
  blocked_sites text[],
  created_time timestamp NOT NULL,
  updated_time timestamp NOT NULL,
  PRIMARY KEY (org_id, campaign_id)
);

CREATE TABLE advertisements (
  ad_id bigserial,
  org_id bigint,
  campaign_id bigint,
  ad_name text NOT NULL,
  image_url text,
  destination_url text,
  impression_count bigint DEFAULT 0,
  click_count bigint DEFAULT 0,
  created_time timestamp NOT NULL,
  updated_time timestamp NOT NULL,
  PRIMARY KEY (org_id, ad_id),
  FOREIGN KEY (org_id, campaign_id)
    REFERENCES marketing_campaigns (org_id, campaign_id)
);

CREATE TABLE ad_clicks (
  click_id bigserial,
  org_id bigint,
  ad_id bigint,
  click_time timestamp NOT NULL,
  source_url text NOT NULL,
  cost_per_click numeric(20,10),
  client_ip inet NOT NULL,
  user_info jsonb NOT NULL,
  PRIMARY KEY (org_id, click_id),
  FOREIGN KEY (org_id, ad_id)
    REFERENCES advertisements (org_id, ad_id)
);

CREATE TABLE ad_impressions (
  view_id bigserial,
  org_id bigint,
  ad_id bigint,
  view_time timestamp NOT NULL,
  source_url text NOT NULL,
  cost_per_view numeric(20,10),
  client_ip inet NOT NULL,
  user_info jsonb NOT NULL,
  PRIMARY KEY (org_id, view_id),
  FOREIGN KEY (org_id, ad_id)
    REFERENCES advertisements (org_id, ad_id)
);

분산 아키텍처 구성

테넌트 단위 쿼리 최적화를 위해 org_id를 분산 키로 지정:

SELECT create_distributed_table('organizations', 'org_id');
SELECT create_distributed_table('marketing_campaigns', 'org_id');
SELECT create_distributed_table('advertisements', 'org_id');
SELECT create_distributed_table('ad_clicks', 'org_id');
SELECT create_distributed_table('ad_impressions', 'org_id');

애플리케이션 통합

org_id 필터를 포함하는 쿼리는 Citus에서 자동 최적화:

-- 테넌트별 캠페인 조회
SELECT campaign_name, pricing_strategy, status, monthly_budget
FROM marketing_campaigns
WHERE org_id = 7
ORDER BY monthly_budget DESC
LIMIT 10;

-- 트랜잭션 처리 예시
BEGIN;
UPDATE marketing_campaigns
SET monthly_budget = monthly_budget + 1500
WHERE org_id = 7 AND campaign_id = 45;

UPDATE marketing_campaigns
SET monthly_budget = monthly_budget - 1500
WHERE org_id = 7 AND campaign_id = 46;
COMMIT;

공유 데이터 관리

모든 테넌트가 사용하는 IP 지리정보 테이블:

CREATE TABLE ip_geolocation (
  ip_range cidr PRIMARY KEY,
  coordinates point NOT NULL
);

SELECT create_reference_table('ip_geolocation');

동적 스키마 변경

ALTER TABLE로 컬럼 추가 시 전체 클러스터에 자동 전파:

ALTER TABLE advertisements
  ADD COLUMN ad_description text;

테넌트별 데이터 확장

JSONB 필드를 활용한 유연한 데이터 저장:

SELECT
  user_info->>'device_type' AS device,
  count(*) AS total_clicks
FROM ad_clicks
WHERE org_id = 7
GROUP BY user_info->>'device_type';

클러스터 확장

새 노드 추가 후 데이터 재분배:

SELECT rebalance_table_shards('organizations');

대규모 테넌트 처리

특정 테넌트 전용 샤드 생성 및 이동:

SELECT isolate_tenant_to_new_shard('organizations', 7, 'CASCADE');

SELECT citus_move_shard_placement(
  102040,
  'original_host', 5432,
  'new_host', 5432);

태그: PostgreSQL citus Database-Sharding Multi-Tenant SaaS

6월 16일 00:01에 게시됨