멀티 클라우드 프록시 구축기 - 홍콩 리전 우회와 SSE 호환성 삽질

멀티 클라우드 프록시 구축기 - 홍콩 리전 우회와 SSE 호환성 삽질

프로젝트 개요

프로젝트: LLM Worker (AI API 프록시)
개발 기간: 2025.08 (약 2주)
기술 스택: Cloudflare Workers, Railway, Bun, SSE
주요 작업: 19 commits


문제 상황

우리 서비스는 Cloudflare Workers에서 LLM API를 호출한다. 근데 특정 사용자들에게서 이상한 에러가 터졌다.

“Gemini API 호출이 안 돼요”

로그를 까보니 홍콩 리전에서 접속한 사용자들이었다.


홍콩 리전 문제

Google의 Gemini API는 홍콩에서 접근이 제한된다. Cloudflare Workers는 사용자와 가까운 엣지에서 실행되기 때문에, 홍콩 사용자의 요청은 홍콩 엣지에서 처리된다.

홍콩 사용자 → 홍콩 CF Edge → Gemini API ❌

해결책: 홍콩 요청만 다른 리전으로 우회시키자.


Railway 프록시 구축

49614db 2025-08-26 feat: railway 설정
57a5689 2025-08-27 feat: 홍콩 리전 감지 Railway 서버로 프록시

Railway에 같은 API 서버를 하나 더 띄웠다. Railway는 리전을 선택할 수 있으니까.

// 리전 감지
const cfRegion = request.cf?.colo; // Cloudflare 엣지 코드

const isHongKong = cfRegion === 'HKG';

if (isHongKong) {
  // Railway 서버로 프록시
  return proxyToRailway(request);
}

Cloudflare의 request.cf.colo로 현재 엣지 위치를 알 수 있다.


SSE 호환성 지옥

프록시는 됐는데 스트리밍 응답이 안 됐다.

c8a73a1 2025-08-27 fix: Railway와 Cloudflare 환경 호환성을 위한 조건부 SSE 헤더 설정
dd472f7 2025-08-27 fix: Railway SSE 헤더 명시적 설정
c35f0ba 2025-08-26 fix: /v2/models text/event-stream 헤더 명시

AI 응답은 Server-Sent Events(SSE)로 스트리밍된다. 문제는 Cloudflare와 Railway가 SSE를 다르게 처리한다는 것.

Cloudflare에서의 SSE

// Cloudflare Workers
return new Response(stream, {
  headers: {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
  }
});

Railway에서의 SSE

Railway는 프록시 뒤에서 돌아가기 때문에 추가 헤더가 필요했다.

// Railway 환경
const isRailway = process.env.RAILWAY_ENVIRONMENT;

const headers = {
  'Content-Type': 'text/event-stream',
  'Cache-Control': 'no-cache',
  'X-Accel-Buffering': 'no',  // nginx 버퍼링 비활성화
};

if (isRailway) {
  headers['Connection'] = 'keep-alive';
}

X-Accel-Buffering: no가 핵심이었다. 이게 없으면 nginx가 응답을 버퍼링해서 스트리밍이 안 된다.


환경변수 로딩 삽질

4502fae 2025-08-26 fix: env 로드하는방법 변경
d885e57 2025-08-26 fix: env 파일 cloudflare 종속성 제거

Cloudflare Workers와 Railway는 환경변수 로딩 방식이 다르다.

Cloudflare Workers:

// wrangler.toml에 정의하거나 대시보드에서 설정
export default {
  fetch(request, env) {
    const apiKey = env.API_KEY;
  }
}

Railway (Bun):

// .env 파일 또는 Railway 대시보드
const apiKey = process.env.API_KEY;
// 또는
const apiKey = Bun.env.API_KEY;

같은 코드베이스로 두 환경을 지원하려면:

function getEnv(key: string): string {
  // Cloudflare Workers
  if (typeof globalThis.env !== 'undefined') {
    return globalThis.env[key];
  }
  // Node.js / Bun
  return process.env[key] ?? '';
}

Gemini API 에러 수정

6476d95 2025-09-05 fix: Gemini API 이미지 처리 Function Calling 에러 수정

프록시는 잘 되는데 Gemini 특유의 에러가 있었다.

이미지 처리

Gemini는 이미지를 base64로 받는데, 포맷이 까다롭다:

// OpenAI 스타일
{
  type: 'image_url',
  image_url: { url: 'data:image/png;base64,...' }
}

// Gemini 스타일
{
  inlineData: {
    mimeType: 'image/png',
    data: '...' // base64, data: prefix 없이
  }
}

Function Calling

Gemini의 function calling 응답도 형식이 다르다:

// OpenAI
{
  tool_calls: [{
    function: { name: 'search', arguments: '{"query":"..."}' }
  }]
}

// Gemini
{
  functionCall: {
    name: 'search',
    args: { query: '...' }  // 이미 파싱된 객체
  }
}

이런 차이점들을 프록시 레이어에서 변환해줬다.


최종 아키텍처

사용자 요청

Cloudflare Workers (엣지)

[리전 체크]
    ├─ 홍콩 → Railway (US/EU) → AI API
    └─ 기타 → 직접 호출 → AI API

SSE 스트리밍 응답

배운 점

1. 엣지 컴퓨팅의 양날의 검

사용자와 가까워서 빠르지만, 지역 제한에 걸릴 수 있다. 멀티 클라우드 폴백은 필수.

2. SSE는 환경마다 다르다

같은 코드도 Cloudflare, Railway, Vercel에서 다르게 동작한다. 각 환경의 프록시/버퍼링 설정을 이해해야 한다.

3. API 변환 레이어의 가치

OpenAI 호환 API로 통일해두면 프론트엔드 코드 변경 없이 백엔드에서 여러 AI 프로바이더를 교체할 수 있다.

4. 로그로 디버깅

4d701b0 2025-08-27 refactor: 홍콩 프록시 로그 개선 테스트 코드 제거

프록시 서버는 문제가 생겨도 원인을 찾기 어렵다. 요청/응답을 상세히 로깅해야 한다.


마무리

“홍콩에서 안 돼요” 한 마디에서 시작한 작업이 멀티 클라우드 프록시 구축으로 끝났다.

클라우드 서비스를 쓰면 인프라 걱정이 없을 줄 알았는데, 오히려 여러 클라우드의 특성을 다 이해해야 했다. 그래도 덕분에 엣지 컴퓨팅과 SSE에 대해 깊이 알게 됐다.


“Works on my machine”은 클라우드 시대에도 유효하다. 내 리전에서는 되니까.

관련 포스트가 없어요.

profile
손상혁.
Currently Managed
Currently not managed