관련 티켓 업데이트의 일관성 유지

CloudflareBeginner
지금 연습하기

소개

해결 메모가 저장되지 않았다면 티켓을 종료해서는 안 됩니다. D1 prepared batch 를 사용해 두 쓰기 작업을 함께 처리한 다음, 요청 사이에 Sessions API 북마크를 전달하여 이후 읽기에서 앞서 커밋된 작업을 확인합니다.

이 실습에서는 원자적 롤백과 순차 세션 일관성을 구분합니다. 독립적인 데이터베이스와 Worker 하나를 사용하며, read replica 를 활성화하거나 복제 경쟁 상태를 재현하지 않습니다.

본인의 학습 계정과 새 VM 을 사용합니다. 먼저 설정 과정에서 Node.js 22.22.0 을 준비한 다음, /home/labex/project/ticket-database에서 프로젝트 로컬 Wrangler 4.131.1 과 평가에 필요한 종속 항목을 npm install로 설치합니다. 직접 지정한 종속 항목 버전은 고정되어 있으며, 설치 과정에서 자체 lockfile 이 생성됩니다. 설정 과정에서는 클라우드 로그인이나 평가 대상 데이터베이스 작업을 실행하지 않습니다. 개인 컴퓨터에서는 프로젝트에서 npm install --save-dev wrangler@4.131.1을 실행해 동일한 Wrangler 버전을 설치합니다.

이 실습에서는 D1 Free allowances 범위 내의 작은 테스트용 레코드를 사용합니다. 기존 계정 사용량도 해당 허용량에 포함됩니다. 구매한 도메인은 필요하지 않습니다. 리소스 삭제와 로그아웃을 모두 확인할 때까지 이 VM 을 유지합니다.

이 VM 을 인증하고 계정 선택

이 단계에서는 새 터미널을 본인의 학습 계정에 연결합니다. Dashboard 로그인만으로는 VM 이 인증되지 않습니다. D1 권한은 데이터베이스 생성, SQL 변경 및 삭제를 허용하고, Workers 권한은 배포를 허용하며, KV 권한은 Wrangler 정리 작업의 리소스 목록을 지원합니다. 인증하기 전에 Background Access 를 포함한 실제 동의 화면을 확인합니다.

준비된 프로젝트를 열고 고정된 CLI 버전을 확인합니다.

cd /home/labex/project/ticket-database
npx wrangler --version

4.131.1이 출력되어야 합니다. 디바이스 인증을 시작합니다. --device는 브라우저에 입력할 코드를 표시하고, --browser=false는 브라우저 선택을 직접 하도록 합니다.

npx wrangler login --device --browser=false --scopes account:read user:read d1:write workers_scripts:write workers_kv:write

브라우저에서 표시된 URL 을 열고 현재 코드를 입력한 다음, 학습 계정과 권한을 확인하고 인증을 승인합니다. 터미널에서 성공 메시지가 표시될 때까지 기다립니다. 프로젝트 파일에 비밀번호나 토큰을 절대 붙여 넣지 않습니다.

npx wrangler whoami --json

loggedIn: true인지 확인한 다음, 계정이 하나만 표시되더라도 계정의 nameid를 확인합니다. 사용할 계정의 ID 를 복사해 아래 구성에 입력합니다. 다음 셸 변수는 6 개의 무작위 바이트 (16 진수 12 자) 를 사용하여 다른 학습자와 이름이 겹치지 않도록 합니다. here-document 는 JSON 줄 사이의 JSON 을 작성하며, 그 안에서 $RUN이 확장됩니다.

$schema 앞의 백슬래시는 이 JSON 키를 그대로 유지하며, $RUN 은 이번 실행의 고유 이름으로 확장됩니다.

RUN=labex-c04-d05-$(openssl rand -hex 6)
cat > wrangler.jsonc <<JSON
{
  "\$schema": "./node_modules/wrangler/config-schema.json",
  "name": "$RUN",
  "account_id": "YOUR_ACCOUNT_ID",
  "main": "src/index.js",
  "compatibility_date": "2026-09-15",
  "workers_dev": true,
  "preview_urls": false
}
JSON

블록을 실행하기 전에 YOUR_ACCOUNT_ID를 본인의 계정 ID 로 바꿉니다. RUN이 계속 유지되도록 이 터미널을 열어 둡니다. name은 이번 실행을 식별하고, account_id는 클라우드 작업에 사용할 계정을 선택합니다. 이 파일은 일반 JSON 이며 유효한 JSONC 이기도 합니다. 이 파일을 작성하는 것만으로는 Worker 가 배포되지 않습니다.

티켓과 해결 메모 준비

이 단계에서는 서로 관련된 두 테이블을 준비합니다. 티켓을 종료할 때는 해결 메모도 저장해야 합니다. 한 쓰기 작업만 성공하면 담당자는 설명이 없는 종료된 티켓을 보게 될 수 있습니다. 설정 과정에서 스키마와 HTTP 라우터를 제공하며, 관련 SQL 작업은 직접 구현합니다.

임시 클라우드 데이터베이스를 생성합니다. --binding DB는 애플리케이션 코드에서 사용할 짧은 이름을 지정하고, --update-config는 실제 이름과 UUID 를 wrangler.jsonc에 기록하며, --use-remote=false는 개발 작업을 로컬에서 수행하도록 합니다.

npx wrangler d1 create "$RUN-db" --binding DB --update-config --use-remote=false

생성된 이름과 ID 를 확인한 다음, 저장된 바인딩을 살펴봅니다.

cat wrangler.jsonc

DB 항목에는 이번 실행에서 생성한 데이터베이스의 이름이 있어야 합니다. 바인딩은 코드와 리소스를 연결하도록 구성된 연결 정보입니다. UUID 는 클라우드 데이터베이스를 식별하고, --local은 이 VM 에서 별도의 SQLite 데이터베이스를 사용합니다. SQL 명령에는 항상 --local 또는 --remote 중 하나를 포함합니다.

cat schema.sql
npx wrangler d1 execute DB --local --file schema.sql
npx wrangler d1 execute DB --remote --file schema.sql

티켓 1 은 열려 있고 해결 메모리가 없습니다. 티켓 2 는 종료되어 있으며 해결 이벤트 1 을 사용합니다. 이미 사용 중인 이벤트 ID 를 이용하면 통제된 실패 상황을 만들 수 있습니다. 이벤트 1 을 다시 삽입하면 primary key 제약 조건을 위반합니다.

쓰기를 원자적으로 처리하고 읽기를 순차적으로 계속

이 단계에서는 서로 다른 두 가지 일관성 문제를 해결합니다. **원자성 (atomicity)**은 관련된 두 쓰기 작업이 모두 성공하거나 모두 실패해야 한다는 뜻입니다. D1 의 batch()는 prepared statement 를 트랜잭션으로 실행하므로, 하나라도 실패하면 전체 배치를 롤백합니다. await를 사용해 두 쓰기 작업을 따로 실행하면 이러한 보장이 제공되지 않습니다.

**세션 (session)**은 일련의 쿼리가 관찰한 데이터베이스 상태를 추적합니다. 제공된 라우터는 env.DB.withSession(...)을 호출하며, 클라이언트에 북마크가 없으면 first-primary에서 시작합니다. 라우터는 getBookmark()의 결과를 x-d1-bookmark 헤더로 돌려보냅니다. 이후 요청은 이 북마크를 보내 최소한 해당 데이터베이스 상태부터 계속 읽을 수 있습니다. 이는 순차 일관성이며, HTTP 요청 사이의 모든 작업을 하나의 트랜잭션으로 처리하는 것은 아닙니다.

라우터가 전달한 세션을 사용해 두 함수를 구현합니다.

cat > src/store.js <<'JS'
export async function closeTicket(session, id, eventId, note) {
  await session.batch([
    session.prepare("UPDATE tickets SET status = 'closed' WHERE id = ?").bind(id),
    session.prepare('INSERT INTO resolutions(event_id, ticket_id, note) VALUES (?, ?, ?)').bind(eventId, id, note)
  ]);
}
export async function readTicket(session, id) {
  const ticket = await session.prepare('SELECT id, subject, status FROM tickets WHERE id = ?').bind(id).first();
  if (!ticket) return null;
  const { results } = await session.prepare('SELECT event_id, note FROM resolutions WHERE ticket_id = ? ORDER BY event_id').bind(id).all();
  return { ...ticket, resolutions: results };
}
JS

src/index.js를 읽고 withSession, 수신한 북마크 헤더, 반환되는 북마크의 위치를 확인합니다. 해당 요청의 모든 데이터베이스 작업은 이 세션을 사용합니다. 북마크는 불투명한 위치 값이므로, 파싱하거나 임의로 만들지 말고 받은 값을 그대로 다시 전달합니다.

cat src/index.js
npx wrangler dev --ip 0.0.0.0 > dev.log 2>&1 &
cat dev.log

로컬에서 수신 대기 중이라는 메시지가 표시될 때까지 기다립니다. 로컬 시뮬레이션으로 배치 롤백은 테스트할 수 있지만, 실제 원격 복제나 클라우드 북마크는 확인할 수 없습니다.

성공적으로 종료하기 전에 롤백 확인

이 단계에서는 이미 사용 중인 이벤트 ID 를 의도적으로 제출합니다. 첫 번째 배치 문은 티켓 1 을 종료하려고 시도하지만, 두 번째 문에서 실패합니다. 실패 후 결과를 읽습니다.

curl -i http://localhost:8787/tickets/1/close -H 'Content-Type: application/json' -d '{"event_id":1,"note":"Must roll back"}'
curl -i http://localhost:8787/tickets/1

event_conflict를 포함한 409 응답이 표시되고, 이어서 티켓 1 이 여전히 open 상태이며 resolutions 배열이 비어 있어야 합니다. 409 응답만으로는 충분하지 않습니다. 후속 읽기를 통해 일부 업데이트가 남지 않았음을 확인해야 합니다.

이제 사용되지 않은 이벤트 ID 2 를 사용합니다.

curl -i http://localhost:8787/tickets/1/close -H 'Content-Type: application/json' -d '{"event_id":2,"note":"Access restored"}'
curl -i http://localhost:8787/tickets/1

HTTP 200 응답이 표시되고, 티켓 1 의 상태가 closed로 바뀌며, 메모 Access restored가 포함된 해결 이벤트 2 가 표시되어야 합니다. 이제 두 레코드의 상태가 일치합니다. 복제 지연을 만들기 위해 로컬 데이터를 초기화하거나 원격 데이터베이스를 변경하지 않습니다.

북마크를 사용해 원격 세션 계속하기

이 단계에서는 동일한 배치를 D1 에 실행하고, 요청 사이에 실제 북마크를 전달합니다. 원격 fixture 는 아직 초기 상태입니다.

npx wrangler deploy

실제 배포 URL 을 복사합니다. 먼저 실패하는 배치를 다시 실행하고 롤백을 확인합니다.

URL='YOUR_DEPLOYED_HTTPS_URL'
curl -i "$URL/tickets/1/close" -H 'Content-Type: application/json' -d '{"event_id":1,"note":"Must roll back"}'
curl -i "$URL/tickets/1"

409 응답이 나온 다음, 해결 메모리가 없는 열린 티켓이 표시되어야 합니다. 배포가 아직 전파 중이면 최대 1 분 동안 읽기 요청을 다시 시도합니다. 플랫폼 오류 페이지를 애플리케이션의 JSON 계약으로 잘못 판단하지 않습니다.

성공하는 종료 요청을 제출합니다.

curl -i "$URL/tickets/1/close" -H 'Content-Type: application/json' -d '{"event_id":2,"note":"Access restored"}'

종료된 티켓과 해당 메모리가 표시되어야 합니다. 응답 헤더의 비어 있지 않은 x-d1-bookmark 값을 추가 공백 없이 복사해 다음 변수에 넣습니다.

BOOKMARK='YOUR_RESPONSE_BOOKMARK'
curl -i "$URL/tickets/1" -H "x-d1-bookmark: $BOOKMARK"

후속 읽기에서는 종료된 티켓과 해결 이벤트 2 가 확인되어야 합니다. 북마크는 읽기가 얼마나 오래된 상태까지 허용되는지 제한하는 값이며, 인증 토큰이 아닙니다. 이 작업 흐름에서는 오래된 읽기를 요구하거나 read replication 을 활성화하지 않아도 됩니다. 여기서 확인하는 것은 세션 계약이지, replica 경쟁 상태가 발생했다는 주장이 아닙니다.

Dashboard 에서 정확한 Worker 를 열고 해당 Worker 의 DB 바인딩이 이번 실행의 데이터베이스를 가리키는지 확인합니다. 정리 작업을 시작하기 전에 기능 검증을 완료합니다.

Worker 와 D1 의 바인딩

이 예시는 Worker 의 DB 바인딩이 D1 데이터베이스를 가리키는 모습을 보여 줍니다. 생성된 리소스 접두사는 이 예시 실행을 식별하므로 실제 이름은 다릅니다. 스크린샷은 바인딩만 확인합니다. 위의 HTTP 검사는 원자적 롤백, 성공한 업데이트, 북마크를 사용한 연속 읽기를 검증합니다.

임시 리소스 삭제

이 단계에서는 VM 이 아직 인증된 상태에서 이 실습의 리소스만 삭제합니다. 먼저 모든 기능 확인을 완료합니다. 삭제 검증이 끝날 때까지 구성 파일을 유지합니다.

npx wrangler delete

이번 실행의 구성에 있는 Worker 이름만 표시되는지 확인합니다.

npx wrangler d1 delete DB

프롬프트를 확인하고 이번 실행의 데이터베이스만 삭제 대상으로 표시되는지 확인합니다. 그런 다음 데이터베이스 목록을 조회합니다.

npx wrangler d1 list --json

앞서 기록한 데이터베이스 이름과 UUID 가 성공적인 응답에 없어야 합니다. 다른 리소스는 남아 있을 수 있습니다. 인증 또는 네트워크 오류가 발생하면 삭제 여부를 판단할 수 없습니다. 액세스 문제를 해결한 다음 계속하기 전에 목록 조회를 다시 실행합니다. 아직 로그인된 상태에서 이 단계의 검증을 수행합니다.

로컬 개발 작업도 중지합니다. 작업 목록을 확인하고 본인이 시작한 wrangler dev 작업만 종료합니다. 작업 번호가 다르면 %1을 해당 번호로 바꿉니다.

jobs
kill %1

이 VM 의 인증 종료

이 단계에서는 독립적인 삭제 확인이 성공한 후에만 인증을 종료합니다. 로그아웃하면 이 VM 에 저장된 Wrangler 인증 정보가 삭제됩니다. VM 을 닫는 것만으로는 클라우드 정리가 완료되지 않습니다.

npx wrangler logout
npx wrangler whoami --json

loggedIn: false가 표시되어야 합니다. 인증되지 않은 상태에서 실행한 이 쿼리는 0 이 아닌 종료 코드를 반환할 수 있습니다. 구조화된 응답에 로그아웃 상태가 명시적으로 표시된 경우에만 정상입니다. 검증을 완료한 다음 실습 환경을 종료합니다.

요약

관련 티켓 업데이트의 일관성을 유지하는 방법을 실습했습니다. 데이터베이스 결과를 직접 확인하고, 선택한 계정과 로컬 상태를 명시적으로 관리했으며, 로그아웃하기 전에 임시 리소스를 삭제했습니다.