시리즈: gcloud CLI 자동화 운영 플레이북 (총 11편) | 7회
데이터 서비스 CLI 실전 운영: Storage·BigQuery·Pub/Sub 비용과 안전 가드레일
GCP 데이터 서비스를 CLI로 다루다 보면 “한 번 돌린 쿼리가 수십만 원”이라는 끔찍한 경험을 하게 돼요. 이 글에서는 Storage, BigQuery, Pub/Sub를 CLI로 안전하게 반복 운영하는 템플릿과 비용 가드레일을 정리했어요.
Summary
- Cloud Storage CLI는
gcloud storage가 기본이고, 대량 병렬 전송만gsutil -m을 병행하면 돼 - BigQuery 비용 사고는 드라이런 +
--maximum_bytes_billed조합으로 막을 수 있어 - Pub/Sub 에뮬레이터를 쓰면 운영 환경 영향 없이 통합 테스트가 가능해
- 데이터 CLI 자동화의 핵심은 “반복 실행해도 안전한 템플릿”이야
이 글의 대상
- GCP 데이터 서비스를 CLI로 운영하면서 비용 관리가 걱정인 엔지니어
- BigQuery 쿼리 비용 폭탄을 사전에 차단하고 싶은 데이터 분석가
- CI/CD 파이프라인에서 Storage·Pub/Sub를 자동화하려는 DevOps 엔지니어
목차
- 데이터 CLI의 핵심 원칙: 반복 안전성
- Cloud Storage: gcloud storage vs gsutil 선택 기준
- BigQuery: 드라이런과 비용 가드레일
- Pub/Sub: 에뮬레이터 기반 통합 테스트
- 데이터 서비스 CLI 운영 체크리스트
- 실패 케이스와 트러블슈팅
1. 데이터 CLI의 핵심 원칙: 반복 안전성
데이터 작업에서 제일 중요한 건 “한 번 성공”이 아니라 “반복 실행해도 안전한가”야. 왜냐하면 데이터 서비스는 한 번 잘못 실행하면 비용과 보안 문제가 바로 따라오거든.
예를 들어볼게:
| 실수 유형 | 결과 | 복구 난이도 |
|---|---|---|
| BigQuery 풀스캔 쿼리 반복 | 수십만 원 과금 | 복구 불가 (이미 과금) |
| Storage 버킷 공개 설정 | 데이터 유출 | 즉시 차단 가능하지만 유출된 건 복구 불가 |
| Pub/Sub 메시지 중복 발행 | 하위 시스템 오동작 | 수동 정리 필요 |
그래서 데이터 CLI 자동화의 3대 원칙은 이거야:
# 원칙 1: 멱등성 - 여러 번 실행해도 같은 결과
# 원칙 2: 비용 상한 - 실행 전 비용 예측, 상한 설정
# 원칙 3: 드라이런 - 실제 실행 전 시뮬레이션
이 원칙을 각 서비스별로 어떻게 적용하는지 하나씩 살펴보자.
2. Cloud Storage: gcloud storage vs gsutil 선택 기준
두 CLI 도구의 관계
Google이 gsutil에서 gcloud storage로 전환 흐름을 제공하고 있어. 그런데 현실에서는 둘 다 쓰는 게 맞아. 이유는 간단해 — 각자 잘하는 게 다르거든.
| 기준 | gcloud storage |
gsutil |
|---|---|---|
| 공식 권장 | 신규 프로젝트 표준 | 레거시·기존 스크립트 |
| 병렬 전송 | 지원하지만 제한적 | gsutil -m이 실전에서 강력 |
| 설치 | gcloud SDK에 포함 | gcloud SDK에 포함 |
| JSON 출력 | --format=json 지원 |
별도 파싱 필요 |
| 자동화 친화도 | 높음 (–quiet, –format) | 보통 |
| rsync | gcloud storage rsync |
gsutil rsync |
실전 선택 기준
표준 규칙:
├─ 신규 자동화/스크립트 → gcloud storage
├─ 대량 병렬 업로드 (수만 파일) → gsutil -m
├─ 기존 레거시 스크립트 → gsutil (무리하게 마이그레이션 X)
└─ CI/CD 파이프라인 → gcloud storage (JSON 파싱 쉬움)
기본 작업 명령어 비교
버킷 생성:
# gcloud storage (권장)
gcloud storage buckets create gs://my-bucket \
--location=asia-northeast3 \
--uniform-bucket-level-access
# gsutil
gsutil mb -l asia-northeast3 -b on gs://my-bucket
파일 업로드:
# gcloud storage - 단일 파일
gcloud storage cp local-file.csv gs://my-bucket/data/
# gsutil - 대량 병렬 업로드
gsutil -m cp -r ./large-dataset/ gs://my-bucket/data/
동기화 (rsync):
# gcloud storage rsync
gcloud storage rsync ./local-dir gs://my-bucket/backup/ \
--delete-unmatched-destination-objects \
--recursive
# gsutil rsync
gsutil -m rsync -d -r ./local-dir gs://my-bucket/backup/
안전한 Storage 운영 템플릿
#!/bin/bash
# Storage 동기화 - 안전 버전
set -euo pipefail
BUCKET="gs://my-project-backup"
SOURCE="./data"
LOG_FILE="sync_$(date +%Y%m%d_%H%M%S).log"
# 1단계: 드라이런으로 변경 사항 확인
echo "=== 드라이런 시작 ==="
gcloud storage rsync "$SOURCE" "$BUCKET/daily/" \
--recursive \
--dry-run 2>&1 | tee "$LOG_FILE"
# 2단계: 변경 파일 수 확인
CHANGE_COUNT=$(grep -c "Would copy" "$LOG_FILE" || echo 0)
echo "변경 예정 파일: $CHANGE_COUNT"
# 3단계: 임계값 초과 시 중단
if [ "$CHANGE_COUNT" -gt 1000 ]; then
echo "경고: 변경 파일이 1000개 초과. 수동 확인 필요."
exit 1
fi
# 4단계: 실제 동기화
gcloud storage rsync "$SOURCE" "$BUCKET/daily/" \
--recursive \
--quiet
echo "동기화 완료: $CHANGE_COUNT 파일"
핵심은 드라이런 → 임계값 확인 → 실행 3단계야. 이렇게 해야 “실수로 수만 개 파일을 날렸다”는 사고를 막을 수 있어.
3. BigQuery: 드라이런과 비용 가드레일
BigQuery 과금의 핵심
BigQuery는 스캔한 바이트 기준으로 과금돼. 쿼리 결과가 1행이든 100만 행이든, 스캔한 데이터 양에 따라 비용이 결정되는 거야. 이게 왜 위험하냐면:
SELECT * FROM huge_table
→ 테이블 전체 스캔 → 10TB → 약 $50 한 방
→ 이걸 크론잡으로 매시간 돌리면? → 하루 $1,200
→ 한 달이면? → $36,000 (약 4,800만 원)
그래서 BigQuery CLI에서는 반드시 비용 가드레일을 걸어야 해.
드라이런: 실행 전 비용 예측
# 드라이런으로 스캔량 확인
bq query --dry_run --use_legacy_sql=false \
'SELECT user_id, COUNT(*) FROM `project.dataset.events`
WHERE date >= "2026-01-01" GROUP BY user_id'
출력 예시:
Query successfully validated. Assuming the tables are not modified,
running this query will process 2.1 GB of data.
2.1GB면 약 $0.01 수준이니 안전하지. 하지만 파티션을 안 탄 쿼리라면 수십~수백 GB가 나올 수도 있어.
–maximum_bytes_billed: 비용 상한선
# 최대 스캔량을 10GB로 제한
bq query --use_legacy_sql=false \
--maximum_bytes_billed=10737418240 \
'SELECT * FROM `project.dataset.large_table` WHERE date = "2026-03-01"'
10GB를 초과하는 쿼리는 실행 자체가 차단돼. 실행 전에 막아주니까 비용 사고가 원천 차단되는 거야.
비용 가드레일 3단계 템플릿
#!/bin/bash
# BigQuery 안전 쿼리 실행 템플릿
set -euo pipefail
QUERY='SELECT user_id, event_type, COUNT(*) as cnt
FROM `my-project.analytics.events`
WHERE _PARTITIONDATE >= "2026-03-01"
GROUP BY 1, 2'
MAX_BYTES=$((10 * 1024 * 1024 * 1024)) # 10GB 상한
# 1단계: 드라이런
echo "=== 드라이런 ==="
DRY_RESULT=$(bq query --dry_run --use_legacy_sql=false "$QUERY" 2>&1)
echo "$DRY_RESULT"
# 2단계: 예상 바이트 추출 및 확인
ESTIMATED_BYTES=$(echo "$DRY_RESULT" | grep -oP '\d+' | tail -1)
ESTIMATED_GB=$(echo "scale=2; $ESTIMATED_BYTES / 1024 / 1024 / 1024" | bc)
echo "예상 스캔량: ${ESTIMATED_GB}GB"
if [ "$ESTIMATED_BYTES" -gt "$MAX_BYTES" ]; then
echo "경고: 예상 스캔량(${ESTIMATED_GB}GB)이 상한(10GB) 초과. 쿼리 최적화 필요."
exit 1
fi
# 3단계: 상한 설정하고 실행
echo "=== 쿼리 실행 ==="
bq query --use_legacy_sql=false \
--maximum_bytes_billed=$MAX_BYTES \
--format=json \
"$QUERY"
비용 절감 핵심 전략
| 전략 | 효과 | 적용 방법 |
|---|---|---|
| 파티션 테이블 사용 | 스캔 범위 축소 | WHERE _PARTITIONDATE = ... 필터 |
| 클러스터링 | 관련 데이터 그룹핑 | 자주 필터링하는 컬럼으로 클러스터 설정 |
| SELECT * 금지 | 불필요한 컬럼 스캔 방지 | 필요한 컬럼만 명시 |
| 드라이런 필수화 | 예상 비용 사전 확인 | CI/CD에서 드라이런 단계 추가 |
| 바이트 상한 설정 | 비용 폭탄 원천 차단 | 프로젝트/팀별 상한 정책 |
쿼리 결과를 테이블로 저장
# 결과를 새 테이블로 저장 (비용 절감 패턴)
bq query --use_legacy_sql=false \
--destination_table=my-project:analytics.daily_summary \
--replace \
--maximum_bytes_billed=5368709120 \
'SELECT date, COUNT(*) as events
FROM `my-project.analytics.events`
WHERE _PARTITIONDATE = CURRENT_DATE()
GROUP BY date'
자주 쓰는 쿼리 결과를 테이블로 저장하면, 매번 원본을 스캔하지 않아도 돼. 이것만으로도 비용이 크게 줄어들어.
4. Pub/Sub: 에뮬레이터 기반 통합 테스트
왜 에뮬레이터가 필요한가
Pub/Sub를 개발·테스트할 때 실제 GCP 환경을 쓰면 두 가지 문제가 생겨:
- 비용: 메시지 발행/구독마다 과금
- 운영 영향: 테스트 메시지가 프로덕션 구독자에게 전달될 위험
그래서 Google이 공식으로 Pub/Sub 에뮬레이터를 제공하는 거야.
에뮬레이터 설정
# 에뮬레이터 설치 (gcloud 컴포넌트)
gcloud components install pubsub-emulator
# 에뮬레이터 시작
gcloud beta emulators pubsub start --project=test-project
# 다른 터미널에서 환경 변수 설정
$(gcloud beta emulators pubsub env-init)
환경 변수가 설정되면, 이후 Pub/Sub 클라이언트 라이브러리가 자동으로 에뮬레이터를 바라봐. 코드 변경 없이 로컬 테스트가 되는 거지.
에뮬레이터 기반 테스트 워크플로
개발 워크플로:
├─ 1. 에뮬레이터 시작 (로컬)
├─ 2. 토픽/구독 생성 (에뮬레이터에)
├─ 3. 메시지 발행 테스트
├─ 4. 구독자 동작 검증
├─ 5. 스키마 검증
└─ 6. 프로덕션 배포 전 최종 확인
토픽·구독 관리 명령어
# 토픽 생성
gcloud pubsub topics create my-events
# 구독 생성 (pull 방식)
gcloud pubsub subscriptions create my-sub \
--topic=my-events \
--ack-deadline=60
# 메시지 발행
gcloud pubsub topics publish my-events \
--message='{"user_id": "123", "action": "click"}' \
--attribute=source=test
# 메시지 수신 (pull)
gcloud pubsub subscriptions pull my-sub \
--auto-ack \
--limit=10 \
--format=json
스키마 검증
프로덕션에 배포하기 전에 메시지 스키마를 검증하는 게 중요해:
# Avro 스키마 생성
gcloud pubsub schemas create my-event-schema \
--type=avro \
--definition-file=event_schema.avsc
# 스키마를 토픽에 연결
gcloud pubsub topics create validated-events \
--schema=my-event-schema \
--message-encoding=json
# 스키마에 맞지 않는 메시지는 발행 거부됨
gcloud pubsub topics publish validated-events \
--message='{"invalid": "data"}'
# → 오류 발생: 스키마 불일치
에뮬레이터 vs 실제 환경 차이점
| 항목 | 에뮬레이터 | 실제 GCP |
|---|---|---|
| IAM 인증 | 없음 | 필수 |
| 메시지 순서 보장 | 보장 안 됨 | ordering key로 가능 |
| Dead Letter Queue | 미지원 | 지원 |
| 메시지 보존 기간 | 메모리 기반 (재시작 시 초기화) | 7일 기본 |
| 스키마 검증 | 부분 지원 | 완전 지원 |
에뮬레이터에서 통과해도 프로덕션에서 문제가 될 수 있는 부분이 있어. 특히 ordering key, Dead Letter Queue, IAM 권한 관련 동작은 실제 환경에서 별도 테스트가 필요해.
5. 데이터 서비스 CLI 운영 체크리스트
| 서비스 | 체크 항목 | 명령어/설정 |
|---|---|---|
| Storage | 버킷 공개 접근 차단 | --uniform-bucket-level-access |
| Storage | 동기화 전 드라이런 | --dry-run 플래그 |
| Storage | 대량 전송 시 병렬화 | gsutil -m 사용 |
| BigQuery | 쿼리 전 드라이런 | bq query --dry_run |
| BigQuery | 바이트 상한 설정 | --maximum_bytes_billed |
| BigQuery | SELECT * 사용 금지 | 코드리뷰에서 차단 |
| BigQuery | 파티션 필터 필수 | require_partition_filter 옵션 |
| Pub/Sub | 개발은 에뮬레이터 | gcloud beta emulators pubsub |
| Pub/Sub | 스키마 검증 | gcloud pubsub schemas create |
| Pub/Sub | ack deadline 적절 설정 | --ack-deadline |
6. 실패 케이스와 트러블슈팅
케이스 1: BigQuery 쿼리 비용 폭탄
상황: 크론잡에서 파티션 필터 없이 SELECT * 쿼리를 매시간 실행
증상: 월말 청구서에 예상의 50배 비용
해결:
# 1. 즉시 크론잡 중단
# 2. 쿼리에 파티션 필터 추가
bq query --use_legacy_sql=false \
--maximum_bytes_billed=1073741824 \
'SELECT col1, col2 FROM `dataset.table`
WHERE _PARTITIONDATE = CURRENT_DATE()'
# 3. 테이블에 파티션 필터 필수 설정
bq update --require_partition_filter dataset.table
케이스 2: gsutil rsync로 파일 삭제 사고
상황: gsutil rsync -d 옵션으로 소스에 없는 파일이 대상에서 삭제됨
증상: 버킷의 중요 파일이 사라짐
해결:
# 1. 버전 관리가 켜져 있었다면 복구 가능
gcloud storage objects list gs://my-bucket/ \
--include-managed-folders \
--all-versions
# 2. 앞으로는 반드시 드라이런 먼저
gcloud storage rsync ./local gs://my-bucket/ \
--delete-unmatched-destination-objects \
--dry-run
# 3. 버킷 버전 관리 활성화 (예방)
gcloud storage buckets update gs://my-bucket \
--versioning
케이스 3: Pub/Sub 메시지 누적
상황: 구독자 애플리케이션이 다운되어 메시지가 계속 쌓임
증상: 구독 backlog이 수백만 건으로 증가
해결:
# 1. backlog 확인
gcloud pubsub subscriptions describe my-sub \
--format='value(messageRetentionDuration)'
# 2. 오래된 메시지 일괄 확인 (seek)
gcloud pubsub subscriptions seek my-sub \
--time=$(date -u -d '-1 hour' +%Y-%m-%dT%H:%M:%SZ)
# 3. Dead Letter Topic 설정 (재발 방지)
gcloud pubsub subscriptions update my-sub \
--dead-letter-topic=projects/my-project/topics/dead-letters \
--max-delivery-attempts=5
핵심 정리
1. Cloud Storage CLI는 gcloud storage가 기본, 대량 병렬 전송만 gsutil -m을 쓰면 돼
2. BigQuery는 드라이런 → 바이트 상한 → 파티션 필터, 이 3단계가 비용 폭탄 방지의 전부야
3. Pub/Sub 에뮬레이터를 쓰면 비용 없이 로컬에서 통합 테스트가 가능해
4. 데이터 CLI 자동화는 "멱등성 + 비용 상한 + 드라이런" 3원칙을 반드시 지켜야 해
FAQ
Q. gcloud storage와 gsutil 중 뭘 먼저 배워야 돼?
A. gcloud storage를 먼저 배우는 걸 추천해. Google의 공식 전환 방향이기도 하고, --format=json 같은 자동화 친화 옵션이 잘 돼 있거든. gsutil은 대량 전송(-m 옵션)이 필요할 때만 추가로 익히면 충분해.
Q. BigQuery 드라이런은 정확한 비용을 보여주는 거야?
A. 드라이런은 예상 스캔량(바이트)을 보여주는 거지, 정확한 금액을 알려주는 건 아니야. 하지만 on-demand 과금 기준($6.25/TB)을 곱하면 꽤 정확한 추정이 가능해. 단, 캐시된 결과가 있으면 실제 과금이 줄어들 수 있어.
Q. –maximum_bytes_billed를 프로젝트 전체에 기본 적용할 수 있어?
A. bq query 명령어 수준에서는 매번 지정해야 해. 프로젝트 차원에서 강제하려면 BigQuery의 custom quota(프로젝트별 일일 쿼리 바이트 제한)를 설정하는 게 더 효과적이야. 콘솔에서 IAM & Admin > Quotas에서 설정할 수 있어.
Q. Pub/Sub 에뮬레이터에서 테스트 통과하면 프로덕션도 괜찮아?
A. 대부분은 괜찮지만, 에뮬레이터에서 지원하지 않는 기능이 있어. ordering key 동작, Dead Letter Queue, IAM 권한 검증은 에뮬레이터에서 완벽하게 재현이 안 돼. 프로덕션 배포 전에 스테이징 환경에서 이 부분만 별도 테스트하는 게 좋아.
Q. gsutil -m은 얼마나 빨라?
A. 파일 수와 크기에 따라 다르지만, 수천 개 이상의 작은 파일을 전송할 때 gsutil -m은 단일 스레드 대비 5~10배 정도 빨라질 수 있어. 내부적으로 멀티스레드/멀티프로세스로 동작하거든. 대신 네트워크 대역폭이 병목이면 효과가 줄어들어.
Q. BigQuery에서 SELECT *를 쓰면 왜 위험해?
A. BigQuery는 컬럼 스토리지라서, 필요한 컬럼만 지정하면 해당 컬럼만 스캔해. SELECT *를 쓰면 모든 컬럼을 스캔하게 돼서 비용이 수십 배로 뛸 수 있어. 특히 STRING이나 RECORD 타입 컬럼이 많은 테이블에서는 차이가 엄청나.
Q. Cloud Storage 버킷이 실수로 공개된 걸 어떻게 확인해?
A. gcloud storage buckets describe gs://버킷명 --format=json으로 IAM 정책을 확인할 수 있어. allUsers나 allAuthenticatedUsers가 포함돼 있으면 공개 상태야. 예방하려면 조직 정책에서 constraints/storage.publicAccessPrevention을 활성화하는 게 제일 확실해.
Q. Pub/Sub 메시지 순서가 보장이 안 된다고?
A. 기본적으로 Pub/Sub는 메시지 순서를 보장하지 않아. 순서가 중요한 경우에는 ordering key를 설정해야 해. 같은 ordering key를 가진 메시지끼리는 발행 순서대로 전달되지. 단, ordering key를 쓰면 처리량이 줄어들 수 있으니 꼭 필요한 경우에만 쓰는 게 좋아.
Q. BigQuery 쿼리 결과를 CSV로 바로 내보낼 수 있어?
A. bq query --format=csv로 결과를 CSV 형태로 출력할 수 있어. 대량 데이터라면 bq extract로 GCS 버킷에 내보내는 게 더 효율적이야. bq extract --destination_format=CSV 'project:dataset.table' gs://bucket/output_*.csv 형태로 쓰면 돼.
참고 자료 (References)
데이터 출처
| 출처 | 설명 | 링크 |
|---|---|---|
| Google Cloud Storage 문서 | gsutil에서 gcloud storage 전환 가이드 | 전환 가이드 |
| gsutil 개요 | gsutil 명령줄 도구 공식 문서 | gsutil 개요 |
| BigQuery 가격 정책 | 스캔 바이트 기반 과금 체계 | BigQuery 가격 |
| BigQuery 파티션 테이블 | 파티션으로 스캔 바이트 절감 | 파티션 테이블 |
| Pub/Sub 에뮬레이터 | 로컬 개발용 에뮬레이터 가이드 | Pub/Sub emulator |
| Pub/Sub 스키마 기반 발행 | 메시지 스키마 검증 가이드 | 스키마 기반 퍼블리시 |
핵심 인용
“데이터 작업은 한 번 성공보다 반복 실행해도 안전한가가 중요하다. 비용과 보안이 즉시 따라오기 때문이다.” — Google Cloud CLI 실무 가이드 리서치
다음 편 예고
[8편] 보안·감사·거버넌스: 키리스 운영, KMS, SCC로 CLI 보안 완성하기
- CLI 운영 최대 리스크인 로컬 자격증명/키 파일 관리법
- KMS Data Access 로그 활성화와 감사 체계 구축
- SCC 탐지 데이터를 Pub/Sub·BigQuery로 내보내는 파이프라인 설계
