Citus 분산 클러스터로의 애플리케이션 이전 가이드
기존에 구축된 시스템을 Citus 분산 아키텍처로 이동할 때, 단순한 설정 변경만으로는 충분하지 않습니다. 성능과 안정성을 보장하기 위해 데이터 모델 (스키마) 과 조회 문 (Query) 에 상당한 조정이 필요합니다. Citus 는 PostgreSQL 의 기능을 확장한 것이지만, 모든 작업 부하를 자동으로 처리해주는 마법 같은 도구는 아닙니다. 고가용성 클러스터를 운영하려면 데이터 배치 방식, 도구 선택, 그리고 SQL 사용 방식을 신중하게 설계해야 합니다.
마이그레이션의 첫 관문은 다수의 노드에서 효율적으로 작동하도록 기존 스키마를 최적화하는 것입니다.
- 분산 전략 수립: 샤딩 키 선정, 테이블 유형 분류
- 소스 테이블 준비: 분산 키 컬럼 추가, 기존 데이터 값 채움 (Backfill)
이후에는 변경된 스키마를 반영하여 애플리케이션 코드와 쿼리를 업데이트해야 합니다.
- 애플리케이션 적응: 개발 환경 구축, 쿼리 필터링 추가, 보안 연결 활성화, 간접 참조 트래픽 점검
개발 환경에서 검증이 완료되면, 최종 단계는 생산 데이터를 Citus 클러스터로 전송하고 서비스를 전환하는 것입니다. 이 과정은 서비스 중단 시간을 최소화하는 기술적 접근이 필수적입니다.
- 생산 데이터 이전: 소규모 DB 용량 대응, 대규모 DB 용량 대응
1. 분산 전략 결정하기
샤딩 키 선정
Citus 로 전환할 때 가장 우선시해야 할 작업은 적절한 분산 키 (Distribution Key) 를 결정하고 테이블의 배치 구조를 계획하는 것입니다. 멀티테넌트 아키텍처의 경우 일반적으로 테넌트를 식별하는 고유 ID 가 이 역할을 담당합니다. 이를 통상적으로 '테넌트 ID'라고 부릅니다. 하지만 사용 사례에 따라 다를 수 있으므로 해당 단계에서 철저한 검토가 필요합니다.
관련 지식을 얻고자 한다면 다음 주제들을 참고하십시오:
- 애플리케이션의 성격 파악
- 분산 할 대상 컬럼 선정
환경 분석을 통해 최적의 키를 선택할 수 있도록 지원하는 컨설팅을 받을 수도 있습니다. 이때 보통 스키마 구조, 대용량 테이블, 장시간 실행되는 문제 발생 쿼리 등을 함께 검토하게 됩니다.
테이블 유형 구분
분산 키가 정해지면, 각 테이블을 어떻게 처리할지 스키마를 기준으로 분류해야 합니다. 이 과정을 관리하기 위해 스프레드시트를 활용하는 것이 권장되며, 표준 템플릿이 마련되어 있을 수 있습니다.
테이블은 대개 다음과 같이 네 가지 범주로 나뉩니다.
- 즉각 분산 가능한 표: 이미 분산 키를 포함하고 있어 바로 샤딩 적용이 가능한 테이블입니다.
- 데이터 보완이 필요한 표: 논리적으로는 분산 키에 따라 배치할 수 있으나, 실제 열이 누락된 경우입니다. 추후 열을 추가해야 합니다.
- 참조 표 (Reference Table): 크기가 작으며 분산 키를 포함하지 않고 여러 테넌트가 공유하거나 분산 표와 조인하는 테이블입니다. (예: 국가 코드, 상품 카테고리) 모든 노드에 복제가 유지됩니다.
- 로컬 표 (Local Table): 다른 테이블과 연결되지 않고 분산 키가 없는 유틸리티나 관리자 정보 테이블입니다. 코디네이터 노드에만 보관됩니다.
여기서는 쇼핑몰 형태의 멀티테넌트 사례를 가정하겠습니다. 여기서 '숍 (Shop)'이 테넌트에 해당하며, 식별자는 shop_id입니다. 분산 후 동일한 숍에 대한 행들이 하나의 노드에 집중되도록 해야 합니다.
2. 마이그레이션을 위한 소스 테이블 준비
필요한 데이터베이스 수정 범위가 확정되면, 기존 구조를 변경하는 작업을 수행합니다. 먼저 분산 키가 누락된 테이블에 해당 열을 추가해야 합니다.
분산 키 컬럼 추가
가상의 예시에서 shops와 products 테이블에는 이미 shop_id가 존재합니다. 반면 정규화된 order_details 테이블에는 이 정보가 빠져있습니다. 만약 shop_id 기반으로 분산하려는 경우 해당 열을 반드시 포함시켜야 합니다.
-- order_details 에 분산 열 추가
ALTER TABLE order_details
ADD COLUMN client_shop_uuid UUID;
모든 관련 테이블의 분산 열 타입이 일치하는지 (예: int 와 bigint 혼용 금지) 검증해야 하며, 이는 정확한 데이터 라우팅을 위해 필수적입니다.
추가된 열에 데이터 보충 (Backfill)
스키마 변경 후에는 누락된 값을 채워 넣어야 합니다. 위 예시라면 order_details의 client_shop_uuid에 올바른 값을 가져와야 합니다.
주문 테이블과 조인하여 값을 가져오는 방식으로 업데이트합니다:
UPDATE order_details od
SET client_shop_uuid = o.shop_uuid
FROM sales_orders o
JOIN order_details od ON od.order_uuid = o.order_uuid
WHERE od.client_shop_uuid IS NULL;
단일 트랜잭션으로 전체 таблиц 을 업데이트하면 부하가 과도해질 수 있습니다. 대신 일정 단위로 나누어 배치 처리를 하는 함수를 정의하고 주기적으로 호출하는 방법이 안전합니다.
-- 일괄 처리 함수 정의 (매번 5,000 행씩)
CREATE OR REPLACE FUNCTION refill_client_data()
RETURNS VOID LANGUAGE plpgsql AS $$
DECLARE
affected_rows INT;
BEGIN
WITH locked_batch AS (
SELECT uuid, order_ref
FROM order_details
WHERE client_shop_uuid IS NULL
LIMIT 5000
FOR UPDATE SKIP LOCKED
)
UPDATE order_details d
SET client_shop_uuid = s.shop_uuid
FROM locked_batch lb
JOIN sales_orders s ON lb.order_ref = s.id
WHERE d.uuid = lb.uuid;
GET DIAGNOSTICS affected_rows = ROW_COUNT;
END;
$$;
-- 15 분마다 스케줄링 등록
SELECT cron.schedule('*/15 * * * *', 'SELECT refill_client_data()');
전체 데이터 처리가 끝나면 스케줄러 작업을 중지합니다.
-- 등록된 작업 ID(예시: 42) 를 취소
SELECT cron.unschedule(42);
3. 애플리케이션 Citus 적용 준비
개발용 Citus 클러스터 구성
코드를 Citus 에 맞게 수정하려면 테스트 환경을 먼저 준비해야 합니다. 단일 노드 클러스터를 설치한 뒤, 기존 프로덕션 DB 에서 스키마 만 추출하여 이곳에 리플레이 합니다.
# 원본 DB 에서 스키마 추출
pg_dump --format=plain --no-owner --schema-only \
-f target_schema.sql \
--host=db_host --dbname=source_db
# 테스트 DB 에 스키마 복원
psql --host=test_host --dbname=citus_dev \
-f target_schema.sql
분산하려는 모든 표에 샤딩 키가 포함되어 있어야 합니다.
기본키 및 외래키 수정
Citus 는 분산 열을 포함하지 않는 고유 제약조건 (Unique/Primary Key) 을 강제할 수 없습니다. 따라서 기본키와 외래키를 수정하여 분산 열을 포함시키는 것이 필요합니다.
BEGIN;
-- 기존 단일 열 주키 삭제 (연쇄 삭제를 방지하기 위해 주의)
ALTER TABLE product_catalog DROP CONSTRAINT catalog_pkey CASCADE;
ALTER TABLE sales_orders DROP CONSTRAINT orders_pkey CASCADE;
-- 복합 기본키로 재설정 (테넌트 키 + 원본 키)
ALTER TABLE product_catalog ADD PRIMARY KEY (client_shop_uuid, sku_id);
ALTER TABLE sales_orders ADD PRIMARY KEY (client_shop_uuid, order_num);
-- 외래키도 분산 열 포함하도록 재생성
ALTER TABLE order_details
ADD CONSTRAINT fk_details_product
FOREIGN KEY (client_shop_uuid, sku_id) REFERENCES product_catalog(client_shop_uuid, sku_id);
COMMIT;
이러한 변경 사항이 모두 반영된 뒤에는 애플리케이션의 데이터 입력 로직도 반드시 수정해야 합니다.
쿼리에 분산 키 포함시키기
적절한 테이블에 샤딩 키가 추가되었으면, 애플리케이션 쿼리에서도 이를 명시해야 합니다. 개발 환경에서 Citus 백엔드로 테스트를 진행하며, 불필요한 교차 샤딩 쿼리를 줄입니다.
- 쓰기 요청 시 테넌트 식별자를 포함하도록 코드 수정.
- 테스트 실행 시 로그를 분석하여 누락된 필터 확인.
- SSL 연결을 통한 안전한 통신 보장.
다중 샤딩 조회는 지원되지만, 멀티테넌트 환경에서는 대부분 단일 노드 조회가 되어야 효율적입니다. 즉, SELECT, UPDATE, DELETE 문의 조건절에 항상 테넌트 ID 가 들어가야 합니다.
대부분의 프레임워크는 이를 도와주는 헬퍼 라이브러리를 제공합니다.
- Ruby on Rails (Multi-tenant Gem)
- Django (Middleware approach)
- .NET (Custom DbContext filter)
ORM 을 직접 사용하지 않거나 자바 스크립트로 직접 쿼리를 작성한다면 아래 원칙을 준수합니다.
예를 들어 특정 주문 정보를 조회할 때, 테넌트 ID 가 포함된 조건을 추가하면 성능이 극적으로 개선됩니다.
-- 수정 전 (비효율적)
SELECT * FROM sales_orders WHERE order_num = 'X-999';
-- 수정 후 (효율적)
SELECT * FROM sales_orders
WHERE order_num = 'X-999'
AND client_shop_uuid = 'uuid-value-here';
INSERT 문에도 동일하게 테넌트 식별자가 필수이며, 생략될 경우 라우팅 오류가 발생합니다. 또한 조인 연산 시에도 두 테이블 모두 테넌트 기준으로 필터링되거나 조인 조건에 해당 열이 명시되어야 합니다.
-- 조인 시 테넌트 필터 적용
SELECT SUM(qty)
FROM order_lines l
JOIN product_catalog p ON l.sku_id = p.sku_id AND l.client_shop_uuid = p.client_shop_uuid
WHERE l.client_shop_uuid = 'target-uuid'
보안 연결 및 모니터링
클라이언트는 중계 공격 방지를 위해 SSL 암호화 연결을 사용해야 합니다. 또한 대규모 코드 베이스에서는 우발적인 쿼리가 발생하기 쉬우므로, 여러 샤드를 건드리는 쿼리를 탐지하는 설정을 개발 단계에 도입합니다.
-- 개발 환경: 다중 샤딩 쿼리 발생 시 에러 출력
ALTER DATABASE mydb SET citus.multi_task_query_log_level = 'error';
-- 본番 환경: 다중 샤딩 쿼리 발생 시 로그 남기기
ALTER DATABASE mydb SET citus.multi_task_query_log_level = 'log';
4. 생산 데이터 이전
스키마와 쿼리 수정이 완료되면 실제로 프로덕션 데이터를 이동할 차례입니다. 데이터 양과 허용 가능한 다운타임에 따라 두 가지 경로가 존재합니다.
소규모 데이터베이스 이전
짧은 유지보수 시간이 가능한 경우, 표준 덤프/복구 툴을 사용합니다.
- 개발용 스키마 파일 준비 (
pg_dump --schema-only) - Citus 클러스터에 스키마 생성
- 분산 테이블 선언 (
CREATE DISTRIBUTED TABLE) 실행. 외래키 문제는 순서 조정으로 해결. - 응용 프로그램을 유지보수 모드로 전환하여 쓰기 정지.
- 프로덕션 데이터 덤핑 (
pg_dump --data-only) - Citus 에 데이터 복구 (
pg_restore) - 검증 후 서비스 재개.
대규모 데이터베이스 이전 (논리적 복제 활용)
대용량 환경에서는 중단 시간을 거의 없애기 위해 실시간 복제 (Online Replication) 기술을 사용합니다. 이는 Postgres 의 논리적 복제 (Logical Decoding) 기능을 기반으로 하여, 트랜잭션 발생 시 Citus 클러스터로 스트리밍합니다.
이 절차를 시작하기 전 다음 준비물이 필요합니다.
스키마 동기화
대상 Citus 환경과 소스의 스키마 구조가 완벽히 일치해야 합니다. 특히 이동하려는 모든 테이블은 기본키를 가지고 있어야 하며, Citus 의 경우 이 기본키에 분산 열이 포함되어야 한다는 점을 유의하십시오.
논리적 복제 활성화
관리형 클라우드 (RDS 등) 의 경우 파라미터 그룹 설정을 변경해야 합니다.
rds.logical_replication = 1
자체 호스팅 인스턴스의 경우 postgresql.conf 에 다음을 추가하고 서버를 재부팅합니다.
wal_level = logical
max_replication_slots = 5
max_wal_senders = 5
네트워크 접근 제어 개방
목표 Citus 클러스터의 코디네이터 IP 가 소스 데이터베이스에 접근할 수 있도록 방화벽 규칙을 조정하거나 VPC Peering 을 설정합니다. TCP 5432 포트가 열려 있어야 합니다.
복제 시작 및 전환
지원 팀과 협의하여 초기 덤프를 실행하고 복제 슬롯을 엽니다. 복제 중에는 원본 DB 의 디스크 사용량을 주시해야 하며, 복제 지연이나 충돌로 인해 복제 슬롯이 무한히 커지지 않도록 관리해야 합니다.
복제가 동기화 상태로 쫓아오면, 서비스 전환 전에 목표 DB 의 시퀀스 번호 (Sequence Value) 를 수동으로 맞추어야 합니다. 이 단계가 끝나면 애플리케이션 연결 주소를 새로운 Citus 클러스터로 변경하고, 원본 DB 에 대한 모든 읽기/쓰기를 차단한 뒤 복제 슬롯을 제거합니다.