Skip to content

feat: Cloudflare R2 → Google Cloud Storage(GCS)로 스토리지 교체 #78

@Sunja-An

Description

@Sunja-An

개요

현재 파일 스토리지로 사용 중인 Cloudflare R2를 제거하고, Google Cloud Storage(GCS) 버킷으로 완전히 교체한다. 기존 StoragePort 인터페이스를 유지하면서 인프라 어댑터만 교체하므로 서비스 레이어 변경은 최소화된다.

배경

현재 구조

StoragePort (interface)
    └── R2StorageAdapter  ← Cloudflare R2 (aws-sdk-go-v2 + 커스텀 엔드포인트)

목표 구조

StoragePort (interface)
    └── GCSStorageAdapter  ← Google Cloud Storage (cloud.google.com/go/storage)

구현 범위

In Scope

  • R2StorageAdapter 제거, GCSStorageAdapter 신규 구현
  • GCS 환경변수 추가 (R2 변수 제거)
  • provider.go 수정 (R2 → GCS 초기화)
  • env/.env.example 업데이트
  • cloud.google.com/go/storage 의존성 추가

Out of Scope

  • 기존 R2에 저장된 데이터 마이그레이션
  • Cloud CDN 연동
  • Presigned URL / Signed URL 발급 기능

환경변수

제거

R2_ACCOUNT_ID=
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_BUCKET_NAME=
R2_PUBLIC_URL=

추가

# Google Cloud Storage
GCS_BUCKET_NAME=                  # GCS 버킷 이름
GCS_PUBLIC_URL=                   # 공개 접근 Base URL
                                  # e.g. https://storage.googleapis.com/{bucket-name}
GCS_CREDENTIALS_JSON=             # 서비스 계정 JSON 키 전체 내용
                                  # 미설정 시 ADC(Application Default Credentials) 사용

GCS_CREDENTIALS_JSON이 없으면 GKE / Cloud Run 환경의 Workload Identity / ADC로 자동 폴백됩니다.

구현 명세

신규 파일: internal/storage/infra/gcs_storage_adapter.go

type GCSConfig struct {
    BucketName      string
    PublicURL       string
    CredentialsJSON string  // Service Account JSON (선택, 없으면 ADC 사용)
}

type GCSStorageAdapter struct {
    client     *storage.Client
    bucketName string
    publicURL  string
}

// StoragePort 인터페이스 구현
func (a *GCSStorageAdapter) Upload(ctx, key, body, size, contentType) error
func (a *GCSStorageAdapter) Delete(ctx, key) error
func (a *GCSStorageAdapter) GetPublicURL(key string) string

GCP 서비스 계정 최소 권한

roles/storage.objectCreator  →  오브젝트 생성(업로드)
roles/storage.objectViewer   →  오브젝트 조회
roles/storage.legacyBucketWriter →  오브젝트 삭제 포함

또는 커스텀 역할로 storage.objects.create, storage.objects.delete 만 부여.

업로드 경로 구조 (기존과 동일 유지)

{UploadType}/{id}/{uuid}{ext}

예시)
contest-banners/42/550e8400-e29b-41d4-a716-446655440000.jpg
user-profiles/7/f47ac10b-58cc-4372-a567-0e02b2c3d479.png
main-banners/3/6ba7b810-9dad-11d1-80b4-00c04fd430c8.webp

변경 파일 목록

파일 변경 유형 내용
internal/storage/infra/r2_storage_adapter.go 삭제 R2 어댑터 제거
internal/storage/infra/gcs_storage_adapter.go 신규 GCSStorageAdapter 구현
internal/storage/provider.go 수정 R2 → GCS 초기화로 교체
env/.env.example 수정 R2 변수 제거, GCS 변수 추가
go.mod / go.sum 수정 cloud.google.com/go/storage 의존성 추가

테스트 전략

Unit Test

  • GCSStorageAdapter 메서드를 StoragePort mock으로 검증
  • GCS_BUCKET_NAME 누락 시 초기화 실패 케이스

Integration Test

  • testcontainersfake-gcs-server 컨테이너로 실제 GCS API 호출 검증
    • Upload → 오브젝트 생성 확인
    • Delete → 오브젝트 삭제 확인
    • GetPublicURL → URL 포맷 확인

Edge Cases

  • GCS_CREDENTIALS_JSON 미설정 시 ADC로 폴백
  • 버킷 이름 누락 시 서버 시작 경고 로그 후 storage 엔드포인트 비활성화

완료 조건

  • R2StorageAdapter 및 R2 관련 환경변수 완전 제거
  • GCS로 파일 업로드/삭제 동작 확인
  • 기존 스토리지 엔드포인트(/api/v1/storage/contest-banner, /api/v1/storage/user-profile) 정상 동작
  • GCS 어댑터 초기화 실패 시 graceful 처리 (서버 전체 중단 없음)
  • env/.env.example에 GCS 환경변수 문서화 및 R2 변수 제거

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions