# 인증부터 조회·MERGE·CDC까지

TypeScript와 Python으로 BigQuery 연결 확인, 파라미터 조회, 비용 상한, 행 입력, MERGE, CDC 레코드 변환을 구현하고 정상 출력과 오류 대응을 함께 확인합니다.

- 카테고리: BigQuery
- 소요 시간: 약 16분
- 난이도: 중간
- 업데이트: 2026.09.03
- 원문: /wiki/playbook/bigquery-checklist/bigquery-code-examples

## 목차

- [시작 전 기준](#before-start)
- [1단계 · 프로젝트 준비](#step-1)
- [2단계 · 연결 확인](#step-2)
- [3단계 · 안전한 조회](#step-3)
- [4단계 · 행 입력](#step-4)
- [5단계 · MERGE](#step-5)
- [6단계 · CDC 레코드](#step-6)
- [오류 읽는 법](#errors)
- [자주 묻는 질문](#faq)

인증이 끝난 다음 무엇을 실행해야 하는지 바로 찾을 수 있도록, 연결 확인부터 조회·입력·MERGE·CDC까지 하나의 주문 테이블로 이어지는 예제를 모았습니다. TypeScript를 기본으로 두고 같은 동작의 Python과 GoogleSQL을 함께 제공합니다.

> **이 문서 핵심**
>
> - **대상.** 애플리케이션이나 자동화 작업에서 BigQuery를 호출해야 하는 사람.
>
> - **준비.** Node.js 22 이상 또는 Python 3.11 이상, 프로젝트 ID, 인증, IAM 권한.
>
> - **완료 후.** 연결 확인 → 비용 제한 조회 → 입력 → 멱등 MERGE의 전체 흐름을 실행합니다.
>
> - **샘플 기준.** 서울 region의 `developer_guide.orders` 테이블을 사용합니다.

## 코드보다 먼저 맞춰야 할 네 가지

| 항목 | 이 예제의 값 | 확인하지 않으면 생기는 일 |
| --- | --- | --- |
| **프로젝트** | `hurdlers-bq-dev-guide-260902` | 다른 프로젝트에 Job이 생성되거나 권한 오류 발생 |
| **region** | `asia-northeast3` | Dataset not found in location 오류 발생 |
| **Job 권한** | 프로젝트의 BigQuery Job User | 쿼리 Job 생성 실패 |
| **데이터 권한** | 조회는 Data Viewer, 쓰기는 Data Editor | 테이블 조회 또는 변경 거부 |

> **정보**
>
> 인증 방식은 코드에 고정하지 않습니다. 사용자 ADC, 서비스 계정 가장, OAuth, Workload Identity Federation, 키 파일 중 실행 환경에 맞는 방식을 [BigQuery 인증 설정](/wiki/playbook/bigquery-checklist/how-to-set-up-bigquery-authentication)에서 선택하세요. 아래 클라이언트 코드는 모두 ADC 탐색 규칙을 그대로 사용합니다.

## 1단계. 예제 프로젝트와 환경 변수 준비

사용하는 언어의 패키지를 설치하고 `.env`에 대상 자원을 적습니다. 자격 증명 값이 아니라 프로젝트·region·데이터셋·테이블 이름만 들어갑니다.

**.env**
```dotenv
GOOGLE_CLOUD_PROJECT=hurdlers-bq-dev-guide-260902
BIGQUERY_LOCATION=asia-northeast3
BIGQUERY_DATASET=developer_guide
BIGQUERY_TABLE=orders
```

테이블이 없다면 먼저 [데이터셋·테이블 설계](/wiki/playbook/bigquery-checklist/bigquery-dataset-and-table-design-guide)의 DDL을 실행합니다. 예제의 `order_date` 파티션과 `require_partition_filter` 설정은 이후 쿼리의 날짜 조건을 필수로 만듭니다.

## 2단계. 가장 작은 호출로 연결 확인

실제 업무 쿼리부터 실행하지 말고, 데이터셋 메타데이터와 `SELECT 1`로 인증·IAM·region을 먼저 분리해 확인합니다.

**정상 출력 예시**
```text
{ projectId: "hurdlers-bq-dev-guide-260902", datasetId: "developer_guide", location: "asia-northeast3", queryOk: true }
```

메타데이터 조회만 실패하면 데이터셋 권한을, `SELECT 1`만 실패하면 프로젝트의 Job User 권한을 먼저 확인합니다. 두 검사를 분리해 두면 인증 오류를 SQL 오류로 오해하지 않습니다.

## 3단계. 파라미터와 비용 상한을 적용해 조회

사용자 입력을 SQL 문자열에 이어 붙이지 않고 named parameter로 전달합니다. 날짜 파티션 조건과`maximum bytes billed`를 함께 지정해 조회 범위와 비용 상한을 코드에서 고정합니다.

**정상 출력의 마지막 줄**
```text
jobId=3f7c...c91, rows=4
```

> **팁**
>
> 실제 실행 전에 스캔량만 알고 싶다면 같은 Query Job 설정에 `dryRun: true`를 사용합니다. 쿼리 비용 제어 흐름은 [BigQuery 쿼리·비용 제어](/wiki/playbook/bigquery-checklist/bigquery-query-and-cost-control-guide)에서 이어서 확인할 수 있습니다.

## 4단계. 소량의 행을 즉시 입력

요청 직후 조회해야 하는 소량 이벤트에는 JSON 행 입력을 사용할 수 있습니다. 같은 요청을 재시도할 수 있으므로 행마다 안정적인 `insertId`를 함께 보냅니다.

**정상 출력**
```text
inserted=1, order_id=ORD-1005
```

> **주의**
>
> `insertId`는 재시도 중복을 줄이는 보조 장치이며 영구적인 유일성 제약이 아닙니다. 파일이나 수십만 행을 적재할 때는 행별 입력을 반복하지 말고 Load Job을 사용하세요. 입력 API의 언어별 옵션은 Google Cloud의 [테이블 행 입력 예제](https://cloud.google.com/bigquery/docs/samples/bigquery-table-insert-rows)를 기준으로 확인합니다.

## 5단계. MERGE로 갱신과 추가를 한 번에 처리

동일한 `order_id`가 있으면 최신 값으로 갱신하고, 없으면 새 행을 추가합니다. 소스에 같은 키가 여러 번 있으면 먼저 한 건으로 줄이고, 파티션 조건을 `ON`에 포함합니다.

**src/merge-order.sql**
```sql
MERGE `hurdlers-bq-dev-guide-260902.developer_guide.orders` AS target
USING (
  SELECT
    'ORD-1005' AS order_id,
    'CUS-204' AS customer_id,
    'REFUNDED' AS order_status,
    NUMERIC '49000' AS order_amount,
    TIMESTAMP '2026-09-03T10:30:00Z' AS updated_at,
    DATE '2026-09-03' AS order_date
) AS source
ON target.order_id = source.order_id
  AND target.order_date = source.order_date
WHEN MATCHED AND source.updated_at >= target.updated_at THEN
  UPDATE SET
    customer_id = source.customer_id,
    order_status = source.order_status,
    order_amount = source.order_amount,
    updated_at = source.updated_at
WHEN NOT MATCHED THEN
  INSERT (order_id, customer_id, order_status, order_amount, updated_at, order_date)
  VALUES (source.order_id, source.customer_id, source.order_status,
    source.order_amount, source.updated_at, source.order_date);
```

**정상 출력**
```text
jobId=8a2d...f04, affectedRows=1
```

> **정보**
>
> BigQuery 일반 DML은 `INSERT`·`UPDATE`·`DELETE`·`MERGE`를 모두 지원합니다. **UPSERT와 DELETE만 받는 것은 Storage Write API의 CDC 변경 타입**입니다. 배치 갱신과 실시간 CDC를 같은 개념으로 섞지 않습니다. MERGE의 절별 동작과 제한은 [GoogleSQL MERGE 문법](https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax#merge_statement)을 기준으로 합니다.

## 6단계. CDC 입력용 UPSERT·DELETE 레코드 만들기

CDC는 기본 키가 선언된 테이블의 default stream에 protobuf 형식으로 전송합니다. 아래 코드는 앱의 변경 이벤트를 BigQuery CDC 레코드로 바꾸는 경계이며, 결과를 Storage Write API writer에 전달합니다.

| 필드 | 규칙 |
| --- | --- |
| `_CHANGE_TYPE` | `UPSERT` 또는 `DELETE`만 허용 |
| `_CHANGE_SEQUENCE_NUMBER` | 최대 4개 구간의 16진수 문자열. 동일 키의 최신 변경을 결정 |
| UPSERT 본문 | 부분 PATCH가 아니라 스키마에 맞는 전체 행을 전송 |
| DELETE 본문 | 대상 행을 식별할 기본 키 포함 |

> **주의**
>
> 이 레코드를 일반 JSON insert API에 보내면 CDC가 되지 않습니다. CDC writer의 default stream, protobuf 스키마, 재연결·재시도·적용 지연까지 함께 설계해야 합니다. 전체 운영 기준은 [BigQuery 쓰기·MERGE·CDC](/wiki/playbook/bigquery-checklist/bigquery-data-write-and-cdc-guide)와 Google Cloud의 [CDC 입력 문서](https://cloud.google.com/bigquery/docs/change-data-capture)를 확인하세요.

## 오류 메시지를 어디서부터 읽을까

| 메시지에 보이는 단어 | 먼저 확인할 것 | 대응 |
| --- | --- | --- |
| `Could not load the default credentials` | ADC | 선택한 인증 방식이 현재 프로세스에 노출됐는지 확인 |
| `bigquery.jobs.create denied` | 프로젝트 IAM | 쿼리 Job이 생성되는 프로젝트에 Job User 부여 |
| `Access Denied: Table` | 데이터셋 IAM | 조회는 Data Viewer, 쓰기는 Data Editor 범위 확인 |
| `not found in location` | region | 코드의 location과 참조 데이터셋 위치를 일치 |
| `Query exceeded limit for bytes billed` | 스캔량 | 파티션 필터·컬럼 선택을 개선한 뒤 상한 재검토 |
| `must match at most one source row` | MERGE 소스 중복 | 키별 최신 한 건만 남기고 다시 실행 |

## 자주 묻는 질문

### TypeScript와 Python 중 무엇을 써야 하나요?

현재 서비스와 배포 환경에서 이미 사용하는 언어를 고릅니다. 두 클라이언트 모두 ADC와 Query Job을 사용하므로 권한·region·비용 제어 원칙은 같습니다.

### 예제의 프로젝트 ID를 그대로 실행해도 되나요?

아닙니다. 예제 값은 문서용 샘플입니다. 자신의 프로젝트·데이터셋·테이블로 `.env`를 바꾸고, SQL의 정규화된 테이블 이름도 함께 확인합니다.

### MERGE만 쓰면 중복이 완전히 막히나요?

소스가 키별 한 행이고 모든 쓰기가 같은 키 정책을 지킬 때 멱등 결과를 만들 수 있습니다. BigQuery의 `PRIMARY KEY NOT ENFORCED` 자체는 중복을 차단하지 않습니다.

### CDC 예제는 왜 writer 호출까지 복사하지 않나요?

Storage Write API writer는 언어별 protobuf descriptor, 연결 수명, 재시도와 offset 정책까지 함께 정해야 합니다. 짧은 범용 코드로 숨기면 운영 오류를 만들 수 있어, 여기서는 앱 이벤트를 CDC 레코드로 변환하는 경계와 필수 규칙을 보여줍니다.

## Navigation

- [전체 플레이북 Markdown sitemap](/wiki/playbook/sitemap.md)

### BigQuery

- [BigQuery](/wiki/playbook/bigquery-checklist)
- [BigQuery 온보딩](/wiki/playbook/bigquery-checklist/bigquery-onboarding-guide)
- [BigQuery 인증 설정](/wiki/playbook/bigquery-checklist/how-to-set-up-bigquery-authentication)
- [BigQuery 데이터셋·테이블 설계](/wiki/playbook/bigquery-checklist/bigquery-dataset-and-table-design-guide)
- [BigQuery 쿼리·비용 제어](/wiki/playbook/bigquery-checklist/bigquery-query-and-cost-control-guide)
- [BigQuery 쓰기·MERGE·CDC](/wiki/playbook/bigquery-checklist/bigquery-data-write-and-cdc-guide)
- [BigQuery 코드 예제 모음](/wiki/playbook/bigquery-checklist/bigquery-code-examples) (현재 문서)
- [BigQuery로 들어오는 GA4 데이터](/wiki/playbook/bigquery-checklist/what-is-ga4-data-in-bigquery)
- [BigQuery Studio 인터페이스 이해하기](/wiki/playbook/bigquery-checklist/what-is-bigquery-studio-interface)
- [BigQuery 예상 비용](/wiki/playbook/bigquery-checklist/how-to-estimate-bigquery-costs)
- [무료 버전(샌드박스) 해제해야 하는 이유](/wiki/playbook/bigquery-checklist/why-upgrade-from-bigquery-sandbox)
- [GA4 BigQuery 데이터를 왜 평탄화해야 하나](/wiki/playbook/bigquery-checklist/why-flatten-ga4-bigquery-data)
- [Log Router란?](/wiki/playbook/bigquery-checklist/what-is-log-router)
- [Pub/Sub이란?](/wiki/playbook/bigquery-checklist/what-is-pubsub)
- [Cloud Functions이란?](/wiki/playbook/bigquery-checklist/what-is-cloud-function-and-run)
- [GA4 export 시점에 예약 쿼리 자동 실행하기](/wiki/playbook/bigquery-checklist/how-to-trigger-scheduled-query-on-ga4-export)
- [테이블 정의서는 왜 필요한가](/wiki/playbook/bigquery-checklist/why-table-definition-doc)
- [Event_Flat 테이블 이해하기](/wiki/playbook/bigquery-checklist/what-is-event-flat-table)
- [Item_Performance 테이블 이해하기](/wiki/playbook/bigquery-checklist/what-is-item-performance-table)
