4단계 V2 서비스 확장 설계 - 100-hours-a-week/KTB4-3rd-wiki GitHub Wiki
4단계 컨테이너 기반 서비스 확장 및 운영 자동화 설계
V2 성장 가정에서 산정한 트래픽을 V1 구조(단일 EC2 + Docker Compose)에 부하테스트로 적용하고, 측정된 한계 지점을 근거로 V2 확장 구조를 설계한다. 각 항목은 근거 문서의 핵심만 요약하며, 상세 측정값과 비교는 링크된 문서에서 다룬다.
목차
- 근거 문서
- 1. 서비스 성장 상황 및 예상 Traffic 정의
- 2. 기존 EC2 + Docker Compose 구조의 한계 분석
- 3. Scale-Up / Scale-Out 비교 및 선택 근거
- 4. Load Balancing 전략
- 5. Auto Scaling 기준 및 정책
- 6. 장애 발생 시 가용성 유지 전략
- 7. 확장 구조에 맞게 변경된 CI/CD Pipeline
- 8. 확장 전후 Architecture 비교
- 9. 아키텍처 최종 선택 근거
- 10. 확장 이후 새롭게 발생하는 문제와 트레이드오프
근거 문서
| 구분 | 문서 | 이 문서에서 사용하는 핵심 |
|---|---|---|
| 성장 가정 | V2 서비스 확장 가정 및 성장 지표 | DAU 24배, 91·180·225.5 RPS, WebSocket 1,800개, V2 SLO |
| 사용자 여정 | V1의 핵심 사용자 여정(CUJ) 정의 | 참여자·모집자 CUJ |
| SLI/SLO | V1의 핵심 사용자 여정 별로 SLI/SLO 정의 | 세 CUJ의 API 경로, Good·Bad event, V1 초기 SLO |
| 부하테스트 설계 | V1의 핵심 사용자 여정 기반 부하테스트 시나리오 설계 | S1~S5 시나리오, CUJ E2E 측정 방식 |
| 부하 측정 | V1의 Nginx 컨테이너 부하 측정 | 진입점 연결 비용 집중, 호스트 단위 장애 전파, 진입점 대안 |
| 부하 측정 | V1의 Spring Boot 컨테이너 부하 측정 | REST·WebSocket JVM 공유, 서버 분리, 실행 환경 비교 |
| 부하 측정 | V1의 MySQL 부하 실측 | DB CPU 여유, HikariCP 공용 풀 점유, row lock |
| V2 애플리케이션 | V2 백엔드 애플리케이션 인프라 설계 | ECS on EC2, 단일 AZ 제약, 역할별 REST·WebSocket 용량 가정 |
| V2 채팅 | V2 WebSocket 메시지 버스와 Redis 도입 설계 | MySQL 원본 저장 후 Redis Pub/Sub fan-out, 유실·복구 한계 |
| V1 기준 구조 | V1 아키텍처 설계, Docker Compose 운영 | 단일 EC2, Nginx 단일 라우팅, Public Subnet |
| V1 CI/CD | CI 파이프라인 설계, CD 파이프라인 설계 | 빌드 1회·동일 이미지 승격, Recreate 배포, 무중단 전환 조건 |
1. 서비스 성장 상황 및 예상 Traffic 정의
모여타는 판교 출퇴근 택시팟 매칭에서 출발해 V2에서 수도권 6개 업무지구(판교, 강남, 여의도, 광화문·종로, 구로·가산, 마곡)로 확장하고, 실시간 혼잡도와 영수증 이미지 기반 정산 AI를 추가한다.
| 지표 | V1 | V2 | 산정 근거 |
|---|---|---|---|
| 잠재 이용자 | 판교 중심 | 1,150,000명 | 6개 업무지구 인원 합산 |
| MAU | - | 약 180,000명 | 잠재 이용자 × 도달률 15.7% |
| DAU | 1,500명 | 약 36,000명 (24배) | DAU/MAU 20% |
| 출퇴근 피크 HTTP | 1.25 RPS | 45.5 RPS (1세션/일), 91 RPS (2세션/일) | 피크 4시간에 세션 70%, 순간 계수 2, 세션당 13건 |
| 행사 단독 burst | - | 180 RPS | 3,600명 × 15건, 10분 삼각형 유입 |
| 출퇴근 + 행사 중첩 | - | 225.5 RPS | 45.5 + 180 (용량 상한 시나리오) |
| WebSocket | - | 1,800개, 신규 6개/초 | 행사 유입자 50% 채팅 접속 |
| 채팅 inbound | - | 36 msg/s | 평균 18 msg/s × 순간 계수 2 |
현재 백엔드 배치 설계는 핵심 사용자 여정 기준 rolling 28일 99.9% 가용성(오류 예산 약 43분)을 목표로 하며, ECS on EC2 단일 AZ 배치는 AZ 장애 시 서비스를 유지하지 못한다. 성장 가정 문서의 품질 목표 절에는 더 높은 목표(예: 99.95% 연결 성공률, p95 750ms/p99 2초, CUJ 98.5%)도 남아 있어 기준 SLO와 구분이 필요하다. 이 문서는 배포 구조와 직접 연결된 99.9%를 기준으로 쓰고, 높은 목표는 용량·성능 검증이 필요한 추가 목표로 본다. 현재 설계는 노드 1개 장애 후 용량 유지(N-1)를 보장하지 않는다.
2. 기존 EC2 + Docker Compose 구조의 한계 분석
상세: V1의 Nginx 컨테이너 부하 측정 · V1의 Spring Boot 컨테이너 부하 측정 · V1의 MySQL 부하 실측
V1은 t3.small 1대에 Nginx(0.15 vCPU·64 MiB), Spring Boot(1 vCPU·768 MB), MySQL(0.5 vCPU·768 MB), Next.js를 Docker Compose로 함께 실행한다. 1단계에서는 V1 피크 1.25 RPS를 근거로 빠른 출시와 최소 비용을 우선해 이 구조를 선택했다(V1 아키텍처 설계).
조회 증가 부하 결과 (S1)
| 목표 부하 | SLI 가용성 | 조회 p95 | 판정 |
|---|---|---|---|
| 45.5 RPS | 100% | 301 ms | V1 SLO 충족 |
| 91 RPS | 6.93% | 약 10초 (타임아웃) | 시작 60초 안에 붕괴 |
| 180 / 226 RPS | 0% | 약 10초 | 실패 |
V1의 조회 한계는 45.5~91 RPS 사이이며, V2 평시 피크(91 RPS)부터 수용하지 못한다.
측정으로 확인한 한계 지점
| 한계 지점 | 측정 근거 | 의미 |
|---|---|---|
| Nginx 단일 진입점에 연결 비용 집중 | 91 RPS에서 응답 대기 연결이 누적되어 Nginx 64 MiB 한도 OOM 38건. 0.25 vCPU 실험에서 200 conn/s 연결 수립 p95 3.57초 | 백엔드 지연이 진입점에서 전체 장애로 증폭되고, Nginx가 멈추면 프론트·API·WebSocket이 동시에 중단 |
| 장애 격리 경계가 호스트 | 컨테이너 한도 합 1,728 MiB + OS·Docker 약 450 MiB > 2 GiB. 180·226 RPS에서 호스트 global OOM으로 Java 2회 종료 | 컨테이너 한도가 있어도 여러 컨테이너가 동시에 커지면 부하와 무관한 서비스까지 중단 |
| REST·WebSocket이 같은 JVM 공유 | WebSocket 1,642개 추가 시 조회 p95 11→167 ms, p99 33→915 ms, GC pause p95 3.1→257 ms(82배), HikariCP·MySQL 지표는 변화 없음 | 연결 단가(28.2 KB/연결)는 작지만 누적된 heap·GC가 REST 꼬리 지연으로 전파 |
| HikariCP 공용 풀 점유 | 채팅 저장 47.4 msg/s에서 풀 10/10, 대기 53, 획득 p95 445 ms. 좌석 경합 동시 50건에서 실패율 59.5% | 병목은 DB CPU(조회 274.5 RPS에서 배정량의 21%)가 아니라 커넥션 점유 시간과 row lock 대기 |
정리하면, 가장 먼저 포화되는 자원은 DB 조회와 HikariCP이고, 단일 진입점과 호스트 단위 격리 경계가 그 포화를 서비스 전체 장애로 번지게 한다. HikariCP·MySQL 원인 자체의 개선은 V1의 MySQL 부하 실측에서 다룬다.
3. Scale-Up / Scale-Out 비교 및 선택 근거
| 대상 | Scale-Up | Scale-Out | 선택 | 근거 |
|---|---|---|---|---|
| 진입점 (Nginx) | 컨테이너 vCPU·메모리 증가 | CloudFront + ALB에서 경로 라우팅과 WebSocket Upgrade 처리 | Nginx 제거, ALB 사용 | ALB는 최소 2개 AZ subnet이 필요하다. 단일 AZ ECS 배치안과 target 배치 요건의 정합성을 배포 전 해결해야 한다. |
| 애플리케이션 (Spring) | 인스턴스·JVM 사양 증가 | REST와 WebSocket을 별도 Spring Boot 프로세스·ECS Service로 운영 | ECS on EC2, 역할 분리 | 각 서비스의 자원과 배포를 분리한다. REST 기준 용량은 같은 AZ On-Demand t3.small 2대이며, WebSocket 3대는 연결 수용량이 검증되지 않은 잠정 가정이다. |
| 데이터베이스 (MySQL) | 인스턴스·buffer pool 증가 | 읽기 복제본 등 | 현 구성 유지 후 재측정 | HikariCP 대기는 관측됐지만 쿼리·잠금·커넥션 점유·pool 크기 중 원인은 확정되지 않았다. pool 크기 증설이나 DB 확장을 먼저 결정하지 않는다. |
V2 확장의 기본 방향은 먼저 REST와 WebSocket을 분리하고, 단위 부하 측정으로 용량을 검증한 뒤 역할별로 확장하는 것이다. REST 추가 용량에는 같은 AZ의 Spot을 사용할 수 있지만 필수 처리량에는 포함하지 않는다. WebSocket은 연결 끊김과 재접속 위험 때문에 기본 용량을 On-Demand로 둔다.
4. Load Balancing 전략
Nginx Scale-up, ALB, Nginx 별도 인스턴스, CloudFront + Nginx 인스턴스, Nginx on ECS + CloudFront, CloudFront + ALB → ECS 서비스의 6가지 안을 비교했다. **CloudFront + ALB(ACM) → ECS 서비스(Nginx 제거)**를 채택한다.
Route 53 → CloudFront (정적 캐시 · 엣지 TLS)
→ ALB (ACM; subnet은 다중 AZ 필요)
├─ / → Next.js 대상 그룹
├─ /api/* → REST 대상 그룹
└─ /api/wss → WebSocket 대상 그룹
| V1 Nginx 역할 | V2 대체 수단 |
|---|---|
| 443 TLS 종료 | CloudFront 엣지 종료 + ALB/ACM |
| 경로 라우팅 | ALB 리스너 규칙 → 서비스별 대상 그룹 |
| WebSocket Upgrade | ALB 기본 지원. 유휴 타임아웃(기본 60초)을 하트비트 주기보다 길게 조정 |
/_next/static/ 장기 캐시 |
CloudFront 캐시 정책 |
| IP당 20 r/s 제한 | WAF rate-based 규칙(1분 이상 집계) 또는 애플리케이션 단 rate limit(Bucket4j + Redis) |
Nginx를 ECS 태스크로 옮기는 안은 태스크를 분산하는 순간 앞단 LB가 다시 필요해져 ALB 비용은 같고 hop만 늘어난다. 또 Nginx 태스크 교체 시 그 태스크의 WebSocket 연결이 일괄 끊기므로 채택하지 않는다.
5. Auto Scaling 기준 및 정책
확장 기준의 출발점: V2 서비스 확장 가정 및 성장 지표 - 3. 비정기적 이벤트 상황을 대비한 확장성 · V1의 Spring Boot 컨테이너 부하 측정 - 1-2
REST와 WebSocket은 부하 특성이 다르므로 ECS 서비스별로 서로 다른 지표를 사용한다. 행사 부하를 자동 확장으로 제때 받으려면 연결 수·요청 수 등 선행 지표가 후보지만, 확장 속도와 태스크별 한계는 아직 검증되지 않았다. 예고된 행사는 Scheduled Scaling으로 미리 준비하는 방안도 검토한다.
| 서비스 | 확장 지표 | 정책 | 최소 용량 |
|---|---|---|---|
| REST | ALB RequestCountPerTarget, 보조로 CPU·p95 지연 |
Target Tracking 후보. 목표값은 task 부하테스트 후 설정 | On-Demand t3.small 2대에 task 1개씩 (초기 기준, SLO 보증 아님) |
| WebSocket | task당 활성 연결 수(커스텀 지표), heap 사용률 | Target Tracking 후보. scale-in은 보수적으로 | On-Demand t3.small 3대는 잠정 배치 가정이며 1,800 연결 처리량 검증 필요 |
| Next.js | CPU·요청 수 | Target Tracking 후보 | task 목표와 최소 수량은 별도 검증 |
| 예고된 행사·출퇴근 피크 | 시간 | Scheduled Scaling 후보. 행사 전 용량 확보량은 단위 부하측정과 사전 준비시간으로 산정 | - |
- 태스크당 목표값(RPS/태스크, 연결 수/태스크)은 아직 확정되지 않았다. WebSocket 1,800 연결을 세 task에 나눈 600개/task는 계산상 배분일 뿐 측정된 수용량이 아니다.
- WebSocket scale-in은 연결 일괄 재접속을 만들기 때문에 ALB deregistration delay와 클라이언트 재접속·이력 복구를 함께 적용한다.
- 확장·축소 과정이 CUJ 실패를 만들지 않는지 부하테스트에서 함께 검증한다.
6. 장애 발생 시 가용성 유지 전략
| 장애 유형 | V1 영향 | V2 대응 |
|---|---|---|
| 진입점 장애 | Nginx 중단 시 전체 서비스 중단 | 관리형 ALB(다중 AZ). Nginx 제거 |
| 애플리케이션 노드 장애 | EC2·컨테이너 종료 시 전체 중단 | ECS가 실패 task를 재생성하고 ALB가 비정상 target을 제외한다. 재생성 전 용량 감소는 발생하며 N-1 처리량 보장은 검증되지 않음 |
| AZ 장애 | 단일 AZ | 현재 ECS on EC2 선택안은 단일 AZ이며 AZ 장애 시 전체 중단 가능. 다중 AZ 복구는 현재안에서 보장하지 않음 |
| 한 기능의 부하 전파 | 같은 JVM·호스트 공유로 전체 전파 | REST·WebSocket 서비스 분리, 조회·쓰기 HikariCP 분리, AI 경로를 핵심 CUJ와 분리 |
| WebSocket 태스크 장애 | 전체 연결 끊김 | 다른 태스크로 재접속, 메시지 이력·최신 팟 상태 복구, 인스턴스 간 전달은 브로커 경유 |
| 채팅 이벤트 전달 | 단일 프로세스 Simple Broker | MySQL 커밋 후 Redis Pub/Sub로 모든 WebSocket task에 fan-out. MySQL이 원본이며 Redis는 저장·재생·ACK를 보장하지 않음 |
| AI 처리 지연·실패 | - | 마지막 확정 결과나 처리 상태를 반환해 탐색·매칭·채팅 가용성에 연쇄 실패를 만들지 않음 |
| DB 장애 | EBS·S3 백업 의존, 백업 주기만큼 손실 가능 | [결정 필요] MySQL 컨테이너 유지 여부와 RDS Multi-AZ 전환 검토 |
장애 감지는 사용자 영향 기반 SLI로 하고, CPU·메모리·커넥션 지표는 원인 분석에 쓴다. 오류 예산을 50% 이상 빨리 소진하면 신규 기능 배포를 제한하고, 75%면 고위험 변경을 중단하며, 100%면 신뢰성 개선 외 배포를 중단한다.
7. 확장 구조에 맞게 변경된 CI/CD Pipeline
V1 기준: CI 파이프라인 설계 · CD 파이프라인 설계
V1 CD 문서는 다음 조건이 확인되면 Recreate에서 Blue-Green 또는 Rolling으로 전환한다고 정의했다. V2 구조는 이 조건을 충족한다.
- Prod 실행 단위가 2개 이상으로 증가함 → REST·WebSocket 각각 2개 이상 태스크
- 트래픽 전환 계층 확보 → ALB 대상 그룹
- 서비스 중단 허용 범위 축소 → CUJ 가용성 99.9%, rolling 28일 오류 예산 약 43분
| 항목 | V1 | V2 |
|---|---|---|
| 유지하는 원칙 | 빌드 1회, 동일 이미지 Dev→Prod 승격, 배포 게이트 | 동일 |
| 배포 단위 | Docker Compose 전체 | ECS 서비스별(Next.js, REST, WebSocket) Task Definition 리비전 |
| 배포 대상 | SSM으로 EC2에서 Compose 재기동 | ECS 서비스 업데이트 |
| 배포 전략 | Recreate (계획 중단) | Rolling 업데이트 (최소 정상 비율 100%로 신규 태스크를 먼저 띄운 뒤 교체) |
| 성공 판정 | 컨테이너 Health Check·Readiness·Smoke Test | ALB 대상 그룹 헬스체크 통과 + ALB 경유 Smoke Test + 배포 후 SLI 관찰 |
| 롤백 | 이전 이미지로 수동 재배포 스크립트 | ECS 배포 서킷 브레이커로 자동 롤백 + 직전 Task Definition 리비전 재배포 |
| WebSocket 배포 | 전체 연결 끊김 | deregistration delay 동안 연결 drain 후 교체, 클라이언트 재접속·이력 복구 |
| DB 마이그레이션 | 배포와 함께 수행 | 신·구 버전 태스크가 동시에 실행되므로 하위 호환(expand → contract) 마이그레이션을 별도 단계로 분리 |
- REST와 WebSocket을 같은 코드베이스의 실행 프로필로 나눌지, 별도 모듈·이미지로 나눌지에 따라 이미지 태그와 CI 트리거 범위가 달라진다. [결정 필요]
- 상세 파이프라인 정의는 별도 문서로 작성한다. [작성 예정]
8. 확장 전후 Architecture 비교
[V1]
Route 53 → EIP → EC2 t3.small 1대 (Public Subnet, 단일 AZ)
└─ Docker Compose
├─ Nginx (TLS · 라우팅 · rate limit)
├─ Next.js
├─ Spring Boot (REST + WebSocket + Simple Broker)
│ └─ HikariCP 10 (조회·쓰기·채팅 저장 공용)
└─ MySQL
[V2]
Route 53 → CloudFront → ALB (ALB subnet 다중 AZ 요건)
├─ ECS 서비스: Next.js (용량 검증 후 설정)
├─ ECS 서비스: REST (On-Demand 기준 task 2개 + 선택적 Spot 추가)
│ └─ 조회·쓰기 HikariCP 분리
└─ ECS 서비스: WebSocket (On-Demand t3.small 3대 잠정안)
└─ Redis Pub/Sub → 각 task의 로컬 Simple Broker
→ MySQL (채팅 이력 원본; 저장 topology 미확정)
ECS host/task는 현재 단일 AZ 안에 배치; AZ 장애 대비 미포함
| 항목 | V1 | V2 |
|---|---|---|
| 진입점 | Nginx 컨테이너 1개 | CloudFront + ALB (ALB subnet 다중 AZ 요건, ECS 단일 AZ와 정합성 확인 필요) |
| 실행 환경 | EC2 1대 + Docker Compose | ECS on EC2 (단일 AZ 선택안; Fargate는 비교안) |
| 장애 격리 경계 | 호스트 전체 | 서비스·태스크 단위 |
| REST·WebSocket | 같은 JVM | 서비스 분리 |
| 확장 단위 | EC2 전체 (수동) | 서비스별 태스크 (자동) |
| 실시간 메시지 전달 | Spring Simple Broker (단일 프로세스) | MySQL 커밋 후 Redis Pub/Sub fan-out, task별 로컬 Simple Broker |
| 배포 | Recreate | Rolling + 자동 롤백 |
| 가용성 목표 | 99.9% | 핵심 CUJ 99.9% (rolling 28일); 단일 AZ 장애 보장 아님 |
9. 아키텍처 최종 선택 근거
- 측정된 한계가 구조 문제다. 91 RPS 붕괴는 단일 진입점·호스트 단위 격리·JVM 공유·공용 풀이라는 구조적 공유 자원에서 발생했다. Scale-Up은 이 공유를 유지하므로 근본 해결이 아니다.
- REST·WebSocket 분리는 확정이다. 두 서버는 확장 지표(RPS·p95 vs 동시 연결 수·신규 연결률)가 다르므로 서비스 단위로 따로 확장해야 하고, 이 요구가 컨테이너 오케스트레이션(ECS)을 선택하는 이유가 된다.
- 진입점은 관리형으로 둔다. CloudFront와 ALB가 캐시·TLS·경로 라우팅·WebSocket Upgrade를 담당한다. ALB의 다중 AZ subnet 요건과 ECS host/task 단일 AZ 배치 사이의 정합성은 배포 전에 확정해야 한다.
- 현재 가용성 목표와 배치 한계를 함께 설명한다. CUJ 99.9%는 rolling 28일 목표다. 단일 AZ 선택안은 AZ 장애에서 서비스를 유지하지 못하므로 AZ 장애 보장을 포함하지 않는다.
- DB와 WebSocket 용량은 측정 전 확정하지 않는다. HikariCP pending만으로 병목 원인을 확정할 수 없으며, WebSocket의 세 task·600 연결/task도 검증된 한계가 아니다.
ECS on EC2와 역할별 서비스 분리, Redis Pub/Sub의 V2 fan-out 사용은 현재 설계에서 선택했다. 남은 확인 사항은 ALB subnet과 단일 AZ ECS 배치 정합성, task별 처리 용량, MySQL 저장 topology와 HikariCP 대기 원인이다. Redis Pub/Sub는 at-most-once 전달이며 놓친 실시간 이벤트는 MySQL 이력 API로 복구한다. 보존·재생이 필요해지면 outbox와 내구성 broker/stream을 별도 검토한다. 상세는 V2 백엔드 애플리케이션 인프라 설계와 V2 WebSocket 메시지 버스와 Redis 도입 설계를 따른다.
10. 확장 이후 새롭게 발생하는 문제와 트레이드오프
| 새로운 문제 | 내용 | 대응 방향 |
|---|---|---|
| 비용 증가 | ALB(서울 리전 월 약 22.42 USD + LCU), CloudFront 요청·전송량, EC2·EBS, Redis와 다중 태스크 비용 | task 부하테스트로 용량을 산정하고 중단 가능한 REST 추가 용량에 한해 Spot 검토 |
| NAT 비용 | 태스크를 Private Subnet에 두면 외부 AI·교통 API·ECR 호출에 NAT Gateway가 필요하다(V1 문서 기준 환경당 월 약 43 USD + 처리량) | Public Subnet 배치, VPC 엔드포인트, NAT 공유 중 보안·비용 기준으로 선택 [결정 필요] |
| WebSocket 분산 | 발신자와 수신자가 다른 태스크에 연결될 수 있음 | Redis Pub/Sub와 각 task의 로컬 Simple Broker로 fan-out. Pub/Sub는 재생·ACK가 없으므로 MySQL 이력 조회로 누락 복구 |
| 연결 재접속 폭주 | 배포·scale-in·태스크 장애 시 해당 태스크 연결이 한 번에 재접속 | deregistration delay, 클라이언트 지수 백오프·지터, 보수적 scale-in |
| rate limit 이전 | ALB는 IP당 초 단위 제한이 없음 | WAF(1분 이상 집계)로 완화하거나 애플리케이션 단으로 이전 |
| 채팅 전달과 저장 경계 | MySQL 커밋과 Redis 발행은 원자적이지 않음. 커밋 후 발행 실패 시 일부 실시간 전달이 누락될 수 있음 | DB 저장 성공과 상대방 실시간 수신을 구분하고, 멱등키·메시지 ID와 이력 재조회로 중복·누락을 처리. 필요해지면 transactional outbox 검토 |
| 배포 복잡도 | 신·구 버전 공존, 서비스별 배포 순서 | 하위 호환 API·DB 마이그레이션, 서킷 브레이커 자동 롤백 |
| 관측 범위 확대 | 요청이 CloudFront·ALB·여러 태스크를 거침 | ALB·CloudFront 로그와 애플리케이션 지표를 CUJ SLI 기준으로 연결 |
| 다음 병목 이동 | 앱 계층을 분리해도 DB topology와 HikariCP 대기 원인은 미확정 | rows examined, buffer pool hit, commit·fsync와 커넥션 점유 시간을 재측정한 뒤 확장 여부 결정 |
| 운영 부담 | 인프라 운영 담당 1명 | 관리형 서비스 우선, 표준화된 배포 Job 유지 |