C++ 프로그램에서 Cloudflare Worker로 R2에 GET, POST 하기

작업 배경

처음에는 AWS SDK for C++를 사용해서 R2의 S3 호환 API에 접근하려고 했다. 이를 위해 vcpkgaws-sdk-cpp[s3]를 설치하고, r2Config.iniAccessKeyId, SecretAccessKey, BucketName, Endpoint 같은 정보를 저장한 뒤 C++ 코드에서 읽어오는 방식을 구성했다.

하지만 이 방식은 public repository와 배포 환경에서 보안 문제가 있었다.
게임 클라이언트에 R2 Access Key와 Secret Key가 포함되면, 실행 파일이나 설정 파일을 통해 키가 노출될 수 있고, 누군가가 R2 Bucket에 직접 접근할 가능성이 생긴다.

그래서 최종적으로 구조를 변경했다.

기존 구조:
C++ 클라이언트
→ R2 직접 접근

변경 후 구조:
C++ 클라이언트
→ Cloudflare Worker
→ R2 Bucket

이 구조에서는 C++ 클라이언트가 R2 Secret Key를 알 필요가 없다.
클라이언트는 Worker API만 호출하고, Worker가 R2 Bucket을 읽고 쓰는 역할을 담당한다.


최종 구조

최종 구조는 다음과 같다.

C++ 게임 클라이언트
    │
    ├─ GET /leaderboard
    │   → 리더보드 조회
    │
    └─ POST /record
        → 클리어 기록 등록

Cloudflare Worker
    │
    ├─ 요청 검증
    ├─ IP 기반 POST 제한
    ├─ 기록 정렬 및 포맷 정리
    │
    └─ R2 Bucket
        └─ text-rpg-leaderboard.txt

R2에는 리더보드 파일을 다음 형식으로 저장한다.

1. 00:03:25.142, 12, Knight
2. 00:04:01.530, 10, Mage

각 항목은 다음 의미를 가진다.

index. clearTime, level, name

3. Cloudflare Worker 설정

Worker 프로젝트를 생성한 뒤 wrangler.jsonc에 R2 Bucket binding을 추가했다.

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "text-rpg-r2-worker",
  "main": "src/index.ts",
  "compatibility_date": "2026-05-29",

  "r2_buckets": [
    {
      "binding": "LEADERBOARD_BUCKET",
      "bucket_name": "실제_R2_버킷_이름",
      "remote": true
    }
  ]
}

remote: true를 추가하면 wrangler dev로 로컬에서 테스트할 때도 실제 Cloudflare R2 Bucket을 바라본다.

처음에는 로컬 테스트에서 R2 Dashboard에는 파일이 존재하는데 Worker에서는 계속 기본값만 반환하는 문제가 있었다. 원인은 wrangler dev가 기본적으로 로컬 R2 리소스를 보고 있었기 때문이다. remote: true를 추가한 뒤 실제 R2 파일을 정상적으로 읽을 수 있었다.


Worker API

Worker에는 크게 두 가지 API를 만들었다.

GET /leaderboard

리더보드 파일을 읽어온다.

GET /leaderboard

이 API는 조회용이므로 인증 없이 호출할 수 있게 했다.

R2에 파일이 없으면 빈 리더보드 파일을 생성한다.

const object = await env.LEADERBOARD_BUCKET.get(LEADERBOARD_KEY);

if (object === null) {
  const defaultText = "";

  await env.LEADERBOARD_BUCKET.put(LEADERBOARD_KEY, defaultText, {
    httpMetadata: {
      contentType: "text/plain; charset=utf-8",
    },
  });

  return defaultText;
}

POST /record

새 기록을 등록한다.

POST /record

요청 body는 JSON 형식이다.

{
  "time": "00:03:25.142",
  "level": 12,
  "name": "Knight"
}

Worker는 기존 리더보드 파일을 읽고, 새 기록을 추가한 뒤 정렬해서 다시 R2에 저장한다.

정렬 기준은 다음과 같이 잡았다.

1순위: 클리어 시간이 짧을수록 상위
2순위: 시간이 같으면 level이 높을수록 상위
3순위: 이름 사전순

5. API Token과 보안 처리

POST /record는 아무나 호출하면 안 되기 때문에 Authorization 헤더를 확인하도록 했다.

Authorization: Bearer API_TOKEN

Worker에서는 다음처럼 검사한다.

function checkAuth(request: Request, env: Env): boolean {
  const auth = request.headers.get("Authorization");

  if (!auth) {
    return false;
  }

  return auth === `Bearer ${env.API_TOKEN}`;
}

로컬 개발에서는 .dev.vars 파일에 토큰을 저장했다.

API_TOKEN=text-rpg-api-key-9898

배포 환경에서는 다음 명령으로 Worker secret을 등록했다.

npx wrangler secret put API_TOKEN

이 토큰은 C++ 클라이언트에도 들어가므로 완전한 비밀은 아니다.
하지만 R2 Access Key나 Secret Key가 노출되는 것보다는 훨씬 안전하다.

R2 직접 접근 방식에서는 키가 노출되면 Bucket 전체에 접근할 위험이 있었다.
Worker 방식에서는 노출 범위가 Worker가 허용한 API로 제한된다.


6. IP 기반 POST 제한

API_TOKEN이 클라이언트에 포함되는 이상, 누군가 토큰을 추출해서 POST 요청을 반복할 가능성이 있다. 이를 줄이기 위해 Cloudflare KV를 사용해서 IP 기반 rate limit을 추가했다.

같은 IP 기준:
5분에 3회까지만 POST 허용

KV Namespace를 만들고 wrangler.jsonc에 binding을 추가했다.

"kv_namespaces": [
  {
    "binding": "RATE_LIMIT_KV",
    "id": "KV_NAMESPACE_ID"
  }
]

Worker에서는 클라이언트 IP를 가져와서 KV에 요청 횟수를 저장한다.

const key = `rate-limit:${actionName}:${ip}`;
const currentValue = await env.RATE_LIMIT_KV.get(key);
const currentCount = currentValue ? Number(currentValue) : 0;

if (currentCount >= RATE_LIMIT_MAX_REQUESTS) {
  return {
    allowed: false,
    ip,
    remaining: 0,
  };
}

await env.RATE_LIMIT_KV.put(key, String(nextCount), {
  expirationTtl: RATE_LIMIT_WINDOW_SECONDS,
});

제한을 초과하면 429 Too Many Requests를 반환한다.

{
  "error": "Too Many Requests",
  "message": "Too many rank submissions. Please try again later."
}

이 방식은 완벽한 보안은 아니지만, 연습 프로젝트나 포트폴리오 수준에서는 충분히 합리적인 방어선이다.


7. C++ 클라이언트 수정

기존 C++ 구조에서는 R2Connectorr2Config.ini를 읽고 AWS SDK의 S3Client를 직접 생성했다.

R2Config.ini
→ AccessKeyId
→ SecretAccessKey
→ BucketName
→ Endpoint
→ Aws::S3::S3Client

Worker 방식으로 변경하면서 이 구조를 제거했다.

이제 C++에서는 R2에 직접 접근하지 않는다.
대신 Worker URL에 HTTP 요청을 보낸다.

R2Connector::ReadLeaderboard()
→ HTTP GET /leaderboard

R2Connector::WriteLeaderboard()
→ HTTP POST /record

Windows 환경에서는 WinHTTP를 사용했다.

#include <Windows.h>
#include <winhttp.h>

#pragma comment(lib, "winhttp.lib")

이렇게 변경하면서 AWS SDK 의존성이 사라졌다. 따라서 더 이상 vcpkgaws-sdk-cpp[s3]를 설치할 필요도 없어졌다.


8. Git 관리

Worker 프로젝트에는 .dev.vars, .env, .wrangler, node_modules 같은 파일이 생긴다.

이 중 민감하거나 불필요한 파일은 Git에 올리지 않도록 .gitignore를 정리했다.

node_modules/
.wrangler/

.dev.vars
.dev.vars.*
!.dev.vars.example

.env
.env.*
!.env.example

dist/
*.tsbuildinfo
*.log

반대로 다음 파일들은 Worker 프로젝트를 유지한다면 커밋해도 되는 파일이다.

wrangler.jsonc
src/index.ts
package.json
package-lock.json
tsconfig.json
worker-configuration.d.ts

다만 최종적으로 게임 레포지토리에는 Worker 프로젝트를 포함하지 않고, 게임 코드만 남기는 방향으로 정리했다. Worker는 Cloudflare에 배포되면 로컬 폴더 위치와 무관하게 동작하므로, Worker 프로젝트는 별도 위치나 별도 레포에서 관리해도 된다.

배포 시 필요한 것

Worker 방식으로 변경한 뒤에는 게임 배포 폴더에 R2 관련 설정 파일을 넣을 필요가 없어졌다.

이제 배포물에는 다음만 포함하면 된다.

DIABL5.exe
게임 리소스 폴더
게임 실행에 필요한 DLL

더 이상 포함하지 않아도 되는 것들은 다음과 같다.

r2Config.ini
R2 Access Key
R2 Secret Key
AWS SDK DLL
text-rpg-r2-worker/
.dev.vars
.env
node_modules/
wrangler.jsonc

단, C++ 코드의 Worker URL은 로컬 테스트 주소가 아니라 배포된 Worker 주소여야 한다.

https://text-rpg-r2-worker.계정명.workers.dev

http://localhost:8787이 남아 있으면 배포된 exe에서는 리더보드 통신이 실패한다.


배운 점

이번 작업을 통해 R2를 클라이언트에서 직접 접근하는 것과 Worker를 중간에 두는 것의 차이를 명확히 이해했다.

처음에는 AWS SDK를 사용해서 C++에서 R2에 바로 연결하는 방식을 시도했다. 기능적으로는 가능했지만, 클라이언트 프로그램에 R2 Secret Key가 포함되는 문제가 있었다.

Worker를 사용하면서 구조가 더 명확해졌다.

클라이언트는 요청만 보낸다.
Worker가 인증, 검증, 제한, R2 접근을 담당한다.
R2 Secret Key는 클라이언트에 존재하지 않는다.

또한 단순히 파일을 저장하고 읽는 기능뿐 아니라, 실제 서비스 구조에서 필요한 요소들도 함께 고려했다.

비동기 로딩
로딩 UI
시간 포맷 변환
리더보드 파싱
POST 인증
IP 기반 rate limit
Git ignore 정리
배포 파일 구성

결과적으로 단순한 리더보드 기능을 구현하는 과정이었지만, 클라이언트-서버 분리, 보안, 배포, Git 관리까지 함께 정리할 수 있었다.


정리

최종적으로 구현한 구조는 다음과 같다.

C++ 클라이언트
    ├─ GET /leaderboard
    └─ POST /record

Cloudflare Worker
    ├─ API_TOKEN 검증
    ├─ IP별 POST 제한
    ├─ 기록 유효성 검사
    ├─ 리더보드 정렬
    └─ R2 Bucket 저장

Cloudflare R2
    └─ text-rpg-leaderboard.txt

이전 R2 직접 연결 방식보다 보안적으로 안전하고, 배포 구조도 단순해졌다.

연습 프로젝트 기준으로는 충분히 실용적인 구조이며, 나중에 더 발전시킨다면 계정 인증, 세션 검증, 기록 위변조 방지, 서명 기반 요청 검증 같은 요소를 추가할 수 있을 것이다.