🔴 프로젝트 상태: 부하 테스트(1000 VUs) 및 성능 검증 완료 후 비용 절감을 위해 GCP 인프라(Cloud SQL, Redis, Storage, Cloud Run)를 종료했습니다. 코드와 아키텍처 설계는 그대로 유지되어 있으며, 언제든 재배포 가능합니다.
FoodCom은 대규모 트래픽을 처리할 수 있도록 설계된 음식 커뮤니티 SNS 서비스입니다. 구글 클라우드(GCP)의 핵심 기술을 활용하여 **서버리스 확장성(Serverless Scalability)**과 **데이터 일관성(Data Consistency)**을 동시에 확보했습니다.
이 프로젝트의 가장 큰 특징은 **"실제 운영 환경(Production)을 고려한 엔지니어링"**입니다. 단순히 기능만 구현하는 것을 넘어, 대용량 트래픽과 안정성을 위한 아키텍처를 구축했습니다.
graph TD
User(User) --> GLB["Global Load Balancer & Cloud CDN"]
GLB -->|"Static Assets (Cache)"| Bucket["Cloud Storage (Media)"]
GLB -->|"API Requests"| Run["Cloud Run (Backend Autoscale)"]
subgraph "Backend Infrastructure"
Run -->|"Write (Transactional)"| Master[("Cloud SQL Master - Write")]
Run -->|"Read (Transactional readOnly)"| Slave[("Cloud SQL Replica - Read")]
Run -->|"Auth Token Storage"| Redis[("Memorystore Redis")]
end
subgraph "CI/CD Pipeline"
Github[GitHub Actions] -->|"Build & Test"| Artifact[Artifact Registry]
Artifact -->|Deploy| Run
end
- Framework: Spring Boot 3.4
- Language: Java 21 LTS
- ORM: Spring Data JPA
- Security: Spring Security + JWT + OAuth2
- Build Tool: Gradle
- Compute: Cloud Run (Serverless Container)
- Database: Cloud SQL (MySQL 8.0) - HA(High Availability) 구성 가능
- Auth Store: Memorystore for Redis
- Storage: Cloud Storage + Cloud CDN
- Framework: React + TypeScript + Vite
- State Management: Zustand
- Styling: Tailwind CSS
대부분의 서비스는 읽기(Read)가 쓰기(Write)보다 8:2 비율로 많다는 점에 착안했습니다.
- RoutingDataSource 구현:
@Transactional(readOnly = true)어노테이션 유무에 따라 트래픽을 자동으로 분리합니다. - Master (Write): 데이터 변경 작업 전담. 안전한 구글 Socket Factory 터널링 사용.
- Replica (Read): 단순 조회 작업 전담. 부하가 몰려도 Master 성능에 영향을 주지 않음.
- Cloud Run (Compute): 트래픽이 0일 때는 0원으로, 트래픽이 폭주하면 인스턴스가 수천 개까지 1초 만에 확장됩니다.
- Cloud SQL (Database): Read Replica를 통해 읽기 성능을 수평적으로 확장(Scale-out)할 수 있는 구조입니다.
- Global CDN (Edge Caching): 사용자가 업로드한 이미지 파일은 Cloud CDN을 통해 전 세계 엣지 로케이션에 캐싱됩니다. (응답 속도 < 20ms)
- Security & Auth (Redis): JWT 인증 시 필요한 Refresh Token을 고성능 In-Memory DB인 Redis에 저장하여, 빠르고 안전한 인증 처리를 구현했습니다. (DB 부하 없이 토큰 검증)
- GitHub Actions: 코드 푸시 시
Test -> Build -> Docker Push -> Traffic Splitting Deploy전 과정이 자동화되어 있습니다. - Zero-Downtime Deployment: 배포 중에도 서비스가 중단되지 않도록 트래픽을 서서히 이동시키는(Traffic Splitting) 전략을 사용했습니다.
프로젝트를 진행하며 마주쳤던 난관들과 이를 기술적으로 해결한 과정입니다.
- 문제 발생: 배포된 애플리케이션이 시작되다가
137종료 코드(OOM Killer)와 함께 비정상 종료되거나, 이미지 업로드 처리 중 간헐적으로 500 에러 발생. - 원인 분석: Cloud Run의 기본 메모리 할당량은 512MB인데, JVM 기반의 Spring Boot 애플리케이션이 구동되고 힙 메모리를 사용하기에는 턱없이 부족했음.
- 해결:
deploy.yml의 배포 옵션에--memory 1Gi(또는 2Gi) 옵션을 추가하여 컨테이너 메모리 할당량을 증설.- JVM 옵션으로
-XX:MaxRAMPercentage=75.0을 설정하여, 컨테이너 메모리 내에서 효율적으로 힙을 사용하도록 튜닝.
- 문제 발생: 로컬(
localhost:3000)에서는 잘 되던 API 호출이, 배포 후 프론트엔드(foodcom-frontend...)에서 백엔드를 호출할 때 브라우저 콘솔에 빨간색 CORS 에러를 뿜으며 차단됨. - 원인 분석: 브라우저의 보안 정책상 도메인이 다른 곳으로의 요청은 서버가 명시적으로 허용(
Access-Control-Allow-Origin)해 주어야 함. - 해결:
- Spring Security 설정(
SecurityConfig)에CorsConfigurationSource빈을 등록. - 프론트엔드 배포 URL과 로컬 개발 URL을 명시적으로 허용 목록(
setAllowedOrigins)에 추가하고,Allow-Credentials: true를 설정하여 쿠키/헤더 전송을 가능케 함.
- Spring Security 설정(
- 문제 발생: 로컬에서는
GOOGLE_APPLICATION_CREDENTIALS환경변수로 json 키 파일을 등록해 잘 동작했으나, Cloud Run 배포 후 이미지 업로드 시403 Forbidden에러 발생. - 원인 분석:
- 로컬: 서비스 계정 키 파일(JSON)을 직접 사용.
- Cloud Run: 키 파일 없이 **Workload Identity (ADC)**를 사용하는 것이 권장됨.
- 하지만 Cloud Run 서비스 계정에 "Storage Object Admin" 권한이 누락되어 있었음.
- 해결:
- IAM(권한 관리)에서 Cloud Run이 사용하는 서비스 계정(
Compute Engine default service account등)에 Storage Object Admin 역할을 부여. - 코드에서는 별도의 키 파일 로딩 없이
StorageOptions.getDefaultInstance()를 사용하여 환경에 따라 유연하게 인증 정보를 가져오도록 리팩토링.
- IAM(권한 관리)에서 Cloud Run이 사용하는 서비스 계정(
- 문제 발생: 배포 환경에서만 로그인이 실패하고
Connection Refused에러 발생. - 원인 분석:
mysql-socket-factory라이브러리 구버전(1.15.0)과 최신 MySQL Driver 간의 충돌. - 해결: 라이브러리를
1.20.0으로 업그레이드하고, Slave DB 연결 방식을 Socket Factory로 통일.
- 문제 발생: 읽기 트랜잭션이 Master DB로 라우팅되는 현상.
- 원인 분석: 트랜잭션 시작 시점에 이미 커넥션을 확보해버리는 Spring의 조급한(?) 성격.
- 해결:
LazyConnectionDataSourceProxy도입으로 실제 쿼리 실행 시점까지 커넥션 획득을 지연시킴.
- 문제 발생:
.gitignore된application.properties때문에 GitHub Actions 빌드 실패. - 해결: GitHub Secrets에 프로덕션 설정을 저장하고,
deploy.yml워크플로우 실행 시점에 동적으로 설정 파일을 생성하여 주입하는 방식으로 보안과 편의성을 모두 잡음.
-
최소 인스턴스 수와 최대 인스턴스 수 고민하기
-
CDN 사용을 통한 이미지 빠르게 불러오기
k6 부하 테스트 도구를 사용하여 Replica Lag, Cloud Run Auto-scaling, 그리고 DB Connection Pool의 안정성을 검증했습니다.
- Tools: k6 (Load Testing), Zipkin (Distributed Tracing)
- VUs (Virtual Users): 50명 (동시 접속)
- Scenario:
- Read (80%): 게시글 목록 조회 (Replica DB 활용 확인)
- Write (20%): 회원가입 (Master DB 부하 확인)
초기 테스트 시 **높은 레이턴시(Max 30s+)**와 **에러(Timeout)**가 발생하여 단계적으로 최적화를 진행했습니다.
| 단계 | 조치 내용 | 개선 결과 |
|---|---|---|
| 1. Connection Pool & Concurrency | Cloud Run Concurrency(80) / HikariCP(20) 비율 조정 | Cloud SQL Max Connection(250) 고갈 방지 및 병목 현상 해소 |
| 2. Query Optimization (N+1) | JPA Fetch Join → DTO Projection (QueryDSL/JPQL) 전환 | 필요한 컬럼만 조회하여 네트워크 I/O 감소 및 영속성 컨텍스트 오버헤드 제거 |
| 3. Database Indexing | 주요 필드(fk_member_id, created_at) Covering Index 적용 |
Full Table Scan 방지, 조회 성능 2배 향상 (Avg 605ms → 360ms) |
| 4. 리소스 충돌 해결 | 테스트 스크립트 ID 생성 로직 개선 (Base36) | 회원가입 시 409 Conflict 에러 해결 (에러율 14% → 0%) |
단순히 기능을 구현하는 것을 넘어, 성능(Performance), 확장성(Scalability), 안정성(Stability) 세 가지 토끼를 잡기 위해 깊이 있게 고민하고 개선한 과정입니다.
- UUID vs PK (Long): 초기에는
UUID를 사용했으나, B-Tree 인덱스 정렬 성능 저하와 Page Splitting 문제를 확인하고 **Auto-increment PK(Long)**로 전면 전환하여 Insert 및 조회 성능을 개선했습니다. - Covering Index:
idx_member_id(회원별 조회),idx_modified_at(최신순 정렬) 등 조회 패턴에 최적화된 인덱스를 적용해 Full Table Scan을 방지했습니다.
- Problem:
Fetch Join은 연관된 모든 엔티티 데이터를 로딩하여 메모리 낭비가 심했습니다. - Solution: Repository에서 DTO Projection 방식을 도입, 화면에 필요한 데이터만 "콕 집어서" 조회(
SELECT p.id, p.title...)함으로써 네트워크 I/O와 영속성 컨텍스트 부하를 최소화했습니다.
Cloud Run(Serverless)의 무한한 확장성과 RDB(Cloud SQL)의 물리적 한계 사이에서 균형을 맞췄습니다.
- Math:
Max Instances * Pool Size ~= DB Max Connections - Confg: Cloud Run Concurrency를 80으로 높이고, HikariCP Pool은 20으로 제한하여, 트래픽 폭주 시에도 DB 커넥션이 고갈되지 않도록 Backpressure 역할을 설계했습니다.
- Legacy Key 제거: 보안 사고의 원인이 될 수 있는 JSON 키 파일(
service-account-key.json)을 프로젝트에서 완전히 제거했습니다. - ADC 도입: Google Cloud의 **Workload Identity (ADC)**를 도입하여, 로컬에서는
gcloud auth로, 배포 환경에서는 IAM 권한만으로 안전하게 Storage에 접근합니다.
- Frontend Routing Mismatch: 프론트엔드(
postUuid)와 백엔드(id) 간의 파라미터 불일치로 인한 500 에러를 디버깅하고, 라우팅 구조를 일관성 있게(:postId) 수정했습니다. (Test Coverage 100% 달성) - Upload Stability: 대용량 이미지 업로드 시 발생하는 500 에러를 해결하기 위해 Spring Boot와 Cloud Run의 힙 메모리 및 업로드 용량 제한을 최적화했습니다.
최종 테스트 결과, 에러율 0% 달성과 함께 평균 응답 속도(Avg Latency)가 605ms → 360ms로 2배 가까이 단축되었습니다. 또한 최대 응답 시간(Max Latency)도 30초 이상의 Timeout에서 1.53초로 대폭 개선되어 시스템 안정성이 입증되었습니다.
- Error Rate: 0.00% (Perfect Stability)
- Avg Latency: ~210ms (쾌적한 응답 속도)
- Throughput: ~21.7 req/s (분당 약 1,150 요청 처리)
50명 → 400명까지의 점진적인 부하 테스트를 거쳐, 1000명 동시 접속(VUs) 환경에서의 시스템 한계를 테스트하고 최적화를 진행했습니다.
| VUs | p(95) Latency | 에러율 | 병목 원인 | 조치 |
|---|---|---|---|---|
| 50 | ~500ms | 0% | - | 기준선 측정 |
| 200 | ~2.3s (Cold) | 0% | Cold Start | min-instances=1 설정 |
| 400 | ~584ms (Warm) | 0% | Concurrency 한도 | Concurrency 30→50 증가 |
| 1000 | ~5.4s (초기) | 0% | Slave DB CPU 90% | Slave DB vCPU 1→2 업그레이드 |
| 1000 | ~897ms (최종) | 0% | - | ✅ 안정화 완료 |
- CPU 사용률 70% 미만 + max-instances 도달 →
max-instances또는concurrency증가 필요 - CPU 사용률 90%+ → 설정 조절 무의미, 하드웨어 스펙 업그레이드 필요
- Cloud Run은 여유, DB CPU 포화(90%) → Pool Size 조절로 해결 불가
- Pool Size는 "버퍼"일 뿐, 실제 DB 부하를 줄이려면 캐싱 또는 스펙 업그레이드 필요
- Slave DB: 1 vCPU → 2 vCPU (Read 트래픽 80% 처리, CPU 병목 해소)
- Master DB: 현재 스펙 유지 (CPU 30%대로 여유 있음)
| 구성 요소 | 최종 설정 | 비고 |
|---|---|---|
| Cloud Run Concurrency | 70 | 인스턴스당 동시 처리 요청 수 |
| Cloud Run Max Instances | 15 | 최대 1,050 동시 요청 가능 |
| Cloud Run Min Instances | 1 | Cold Start 방지 |
| Master DB Pool Size | 60 | Write 트래픽 처리 |
| Slave DB Pool Size | 60 | Read 트래픽 처리 |
| Slave DB vCPU | 2 | 1000 VUs 대응을 위해 업그레이드 |
█ THRESHOLDS
http_req_duration ✓ 'p(95)<2000' p(95)=897.17ms
http_req_failed ✓ 'rate<0.01' rate=0.00%
█ TOTAL RESULTS
checks_succeeded: 94.51% (99,657 / 105,436)
✓ GetPosts duration < 500ms: 87%
✓ Signup duration < 1s: 94%
http_req_duration: avg=203ms, med=53ms, max=5.72s, p(95)=897ms
http_reqs: 52,718 (435 req/s)
- Error Rate: 0.00% ✅
- p(95) Latency: 897ms (임계값 2s 이내) ✅
- Throughput: 435 req/s (분당 26,100 요청 처리) ✅
- GetPosts 성공률: 87% < 500ms
- Signup 성공률: 94% < 1s