Cloud 4단계 인프라 설계 의사결정 근거 - 100-hours-a-week/KTB4-5th-wiki GitHub Wiki

4단계 인프라 설계 — 의사결정 근거(이유·근거·트레이드오프)

이 문서는 팀 위키 ktb4-cloud-wiki-scaling.md에 기록된 인프라 확장 결정들의 상세 이유·근거·트레이드오프를 모아두는 작업 문서입니다.

목차


16. 인프라 확장 전략 고려

결정 요약: Scale-up(수직 확장) + Scale-out(수평 확장) 하이브리드 채택, DB는 쓰기 단일·읽기 Read Replica 분산.

이유

  • Scale-up만으로는 부족함 — 한 대에 의존하는 구조라 15번의 기존 한계인 SPOF가 그대로 남음
    • t3 계열은 버스터블 인스턴스라, 반복되는 Peak처럼 지속적인 고부하 상황에서는 CPU 크레딧이 소진되며 성능이 저하될 위험이 있음
  • Scale-out만으로도 부족함 — 최소 사양 인스턴스를 다수 구성하면 인스턴스별 고정 오버헤드(OS·커널·네트워크 스택, 별도 캐시·커넥션 풀)가 반복되어 총 처리 효율이 근소하게 떨어짐(아래 '근거' 참고)
  • 그래서 Scale-up + Scale-out 하이브리드로 절충 — 개별 인스턴스 사양을 어느 정도 상향(Scale-up)해 고정 오버헤드 비중을 줄이면서도, 그 단위를 여러 대로 복제(Scale-out)해 SPOF를 해소하고 트래픽 증감에 유연하게 대응(20번 Auto Scaling과 직결)
    • 15번에서 정의한 평시(8-14 RPS)-Peak(약 95 RPS) 사이 약 7-12배 차이에 대응하기 적합함
  • 읽기를 Read Replica로 먼저 분산 — 15번에서 정리한 읽기:쓰기 비율(7:1-14:1)상 읽기 트래픽이 압도적으로 많아 DB 확장에서 우선순위가 가장 높음
    • 캐시 도입도 대안이 될 수 있음(19번에서 함께 다룸)

근거

  • 비용 비교: 동일한 총 vCPU·메모리 기준으로는 Scale-up과 Scale-out의 순수 컴퓨트 비용이 사실상 동일하고, ALB 도입 비용도 Scale-up 대비 크게 불리하지 않음
    • AWS T3 패밀리는 스펙이 2배면 가격도 정확히 2배(서울 리전 기준 t3.small $0.026/hr → t3.medium $0.052/hr → t3.large $0.104/hr)
    • Scale-out에 필요한 ALB 비용(고정 $0.0225/hr × 730시간 ≈ 월 $16.4 + 예측 트래픽 처리량 기준 약 월 $2 ≈ 월 $18-18.4)도 t3.small 1대를 추가로 돌리는 비용(약 월 $19)과 비슷한 수준
  • 단일 인스턴스 확장의 실질적 리스크: 가장 큰 T3 인스턴스(t3.2xlarge, 8vCPU/32GiB) 한 대로 목표 트래픽(Peak RPS 약 95·CCU 약 7,200)을 감당할 수 있는지는 실제 부하 테스트 없이는 단정할 수 없음 — 처리량은 vCPU 수보다 요청당 처리 시간과 동기/비동기 아키텍처에 더 크게 좌우되기 때문
    • t3는 버스터블 인스턴스라 반복되는 고부하에서 CPU 크레딧 소진 위험이 있음
    • ALB의 고정비는 인스턴스 대수와 무관하게 일정해서, 20번에서 설계한 Peak 시점처럼 대수가 늘어날수록 ALB 비용이 차지하는 상대적 비중은 오히려 작아짐 — 두 가지 모두 Scale-out에 유리한 요인
  • 동일 총 스펙 기준 단일 서버 vs 분산 서버 처리량 비교: 총 스펙이 같다면(예: 큰 서버 1대 = 절반 스펙 서버 2대) 이론적으로 처리량도 비슷하지만, 인스턴스별 고정 오버헤드 때문에 분산 구조가 근소하게 불리할 수 있음
    • 다만 AWS 공식 EC2 SLA 기준 단일 인스턴스는 Instance-Level SLA 99.5%만 보장되고, 서로 다른 가용영역(AZ) 2곳 이상에 걸친 구성은 Region-Level SLA 99.99%가 보장되어, 처리량의 근소한 손해보다 가용성 확보 효과가 더 크다고 판단해 Scale-out과 멀티 AZ 배치를 유지함

트레이드오프

  • 운영 복잡도 증가 — 여러 인스턴스에 트래픽을 나누는 만큼 로드밸런서·오토스케일링 등 관리할 컴포넌트가 늘어나 Scale-up보다 복잡함
  • Stateless 전환 필요 — 인스턴스가 여러 대로 늘어나는 순간 v1처럼 특정 인스턴스에 세션 등 상태를 저장하는 구조는 더 이상 유지할 수 없음(17번에서 이어서 설계)
  • 비용 비교는 추정치 — 실제 트래픽 페이로드 크기를 측정하지 않은 대략적 추정치(요청당 5-20KB 가정)에 기반하므로, 서비스 출시 후 실제 트래픽 데이터로 재검증 필요

17. Application 구조 설계

결정 요약: 앱서버(BE)를 Stateless로 설계, 인증은 Access Token(JWT, Stateless) + Refresh Token(Redis 저장, RTR 방식)의 하이브리드로 구성. 업로드 이미지 중 프로필 사진은 S3에 저장해 BE를 거치지 않고 클라이언트가 직접 서빙받도록 분리하되, CloudFront 도입은 검토 후 채택하지 않음.

이유

  • Stateless 전환이 필요한 이유 — 오토스케일링으로 인스턴스가 수시로 뜨고 사라지는 환경에서, 특정 인스턴스에 상태(세션·파일)가 묶여 있으면 그 인스턴스가 스케일 인(제거)될 때 데이터가 유실되거나 사용자 경험이 끊김
    • Stateless 구조로 전환해야 어떤 인스턴스가 요청을 처리하든 동일하게 동작할 수 있어, 16번의 Scale-out과 20번의 Auto Scaling이 실제로 안전하게 동작할 수 있음
  • Access Token은 JWT(Stateless) 유지 — 매 요청마다 별도 저장소를 조회할 필요 없이 앱서버만 여러 대로 늘리면 됨
    • 16-18번에서 이미 정한 확장 구조에 별도 컴포넌트를 추가하지 않아도 되어 구성이 단순함(JWT vs Redis 세션 스토어 비교표는 위키 17번 참고)
  • Refresh Token은 Redis + RTR(Refresh Token Rotation)로 관리 — JWT 단독의 가장 큰 단점인 "만료 전 즉시 폐기가 어렵다"는 문제를, Access Token 전체가 아니라 상대적으로 수명이 긴 Refresh Token 쪽에서 보완하기 위함
    • Refresh Token을 Redis에 저장해두면 로그아웃·탈취 등의 상황에서 서버가 해당 Refresh Token을 즉시 무효화할 수 있음
    • RTR 방식 — Refresh Token을 사용할 때마다 새 토큰을 발급하고 이전 토큰은 폐기하며, 이미 폐기된 토큰이 재사용되면 탈취로 간주해 해당 토큰 계열 전체를 무효화 — 을 함께 적용해 탈취 여부까지 탐지 가능
    • Access Token 자체는 여전히 Stateless로 유지되므로, Redis는 Refresh Token 발급·갱신 시점에만 사용되고 매 요청마다 조회되지는 않음
  • 프로필 사진 서빙 — BE와 S3의 직접 연결은 분리하되 CloudFront는 도입하지 않음 — 지금까지는 이미지 업로드·서빙 경로에 S3가 BE와 직접 연결돼 있어, 이미지 트래픽이 API 요청 처리와 같은 BE 인스턴스 자원(대역폭·커넥션)을 두고 경합할 수 있는 구조였음. 이 문제를 풀기 위해 서빙(읽기) 경로를 클라이언트 → S3 직접 접근으로 바꿔 BE를 거치지 않도록 분리함
    • CloudFront 캐싱 도입도 함께 검토했으나 채택하지 않음 — CDN 캐싱은 동일 객체를 다수의 서로 다른 사용자가 반복 조회할 때 엣지 캐시 히트로 효과가 커지는데, 프로필 사진은 본인이 마이페이지에서 확인하거나 초대 목록에서 다른 유저가 드물게 조회하는 정도로만 쓰여 이런 "많은 사용자가 같은 캐시를 나눠 쓰는" 상황 자체가 거의 발생하지 않음. 캐시 히트율이 낮으면 CloudFront를 둬도 대부분 캐시 미스로 S3까지 왕복하게 되어 지연·비용 절감 효과가 크지 않은 반면, 컴포넌트·캐시 무효화 관리 부담만 늘어남
    • BE/S3 직접 연결 분리라는 원래 목적은 CloudFront 없이 클라이언트가 S3에 직접 접근하는 구조만으로도 달성되므로, 캐싱 이점이 없는 현재 접근 패턴에서는 CloudFront를 붙일 이유가 약함

트레이드오프

  • Access Token의 잔여 단점 — Access Token 자체는 여전히 Stateless라 발급 후 만료 전에는 즉시 폐기가 불가능하고, 토큰 크기만큼 매 요청 오버헤드도 그대로 남음
    • 다만 Access Token 수명을 짧게 가져가면 탈취되더라도 노출 시간이 Refresh Token보다 훨씬 짧아 피해 범위가 제한됨 — 탈취·보안 사고 대응 시나리오는 22번(장애 상황과 가용성 설계)에서 함께 고려
  • Redis 컴포넌트 신규 도입 — Refresh Token 관리만을 위한 목적이라도 Redis라는 별도 컴포넌트를 새로 운영해야 하는 부담(비용, 가용성 확보, 장애 지점 추가)이 생김
    • 다만 모든 세션 정보가 아니라 Refresh Token만 저장하고 매 요청이 아닌 토큰 갱신 시점에만 사용되므로, 순수 중앙화 Session Store 방식보다는 의존도가 제한적임
  • Stateless 전환 자체의 복잡도 증가 — v1은 EC2 1대에 상태를 그냥 두면 됐지만, JWT 설계(발급·검증·만료 정책)와 S3 도입 등 v1에는 없던 컴포넌트·비용이 새로 생김
    • 다만 이 복잡도는 Scale-out을 안전하게 하기 위해 감수해야 하는 필수 비용으로 판단함
  • S3가 트래픽 스파이크에 CDN 버퍼 없이 그대로 노출됨 — CloudFront 없이 클라이언트가 S3에 직접 접근하므로, 초대 목록 조회 등이 순간적으로 몰리는 경우에도 완충 없이 S3가 요청을 그대로 받음 — 다만 S3 자체가 매우 높은 처리량을 기본 지원하고 현재 접근 빈도가 낮아 당장은 감내 가능한 수준으로 판단
  • 접근 패턴이 바뀌면 재검토 필요 — 향후 프로필 사진을 더 넓은 범위(예: 피드·검색 등 다수 유저가 동일 이미지를 반복 조회하는 기능)에 노출하게 되면, 캐시 히트율이 올라가 CloudFront 도입 실익이 커지므로 그 시점에 다시 검토 대상

18. 컴퓨트(서버) 확장 방식 비교 및 아키텍처 설정

결정 요약: ECS — EC2 launch type + Auto Scaling Group + ALB 채택(Fargate·ECS Managed Instances·EKS는 배제, 오케스트레이션 없는 EC2 단독 방식도 배제).

이유

  • (a) EC2 단독보다 ECS 채택 — 오토스케일링 방식은 (a) 오케스트레이션 계층 없이 EC2만 직접 확장 / (b) ECS(launch type: EC2·Fargate·Managed Instances) / (c) EKS 세 갈래로 나뉨
    • (a)는 스케일 아웃 수단이 인스턴스 단위 하나뿐이라 늘릴 때마다 새 EC2 부팅 지연(1-3분)을 그대로 겪음
    • ECS는 이미 확보된 노드에 여유 자원이 있으면 Task만 늘리는 초 단위 스케일링이 가능해 (a)보다 반응성이 나음 — 이 이점이 결정적이라 (a) 대신 ECS를 채택함
  • (b) launch type은 EC2 선택 — Fargate·ECS Managed Instances(2025년 9월 GA, EC2와 Fargate 사이의 세 번째 옵션)는 인스턴스 관리를 AWS에 위임하는 대가로 추가 비용이 붙음(Fargate: vCPU/메모리 단위 과금, Managed Instances: EC2 비용 위에 별도 관리 수수료)
    • 인스턴스 프로비저닝·패치 자체가 아직 팀이 감당 못 할 부담은 아니라고 판단해, 그 비용을 내면서까지 관리를 위임할 필요는 없다고 결론 내림 — EC2 launch type을 최종 선택한 핵심 이유
  • (c) EKS는 배제 — 셋(EC2 단독/ECS/EKS) 중 러닝커브·운영 복잡도가 가장 높아 지금 다먹자의 서비스 규모·운영 성숙도에는 과잉
  • 트래픽 특성과도 부합 — 다먹자의 트래픽은 15·20번에서 정리했듯 예측 가능한 스파이크가 중심이라 평상시엔 Scheduled Scaling으로 대부분 대응 가능하지만, 그 대응이 노드(EC2) 레벨뿐 아니라 Task 레벨까지 이어지면 반응 속도를 더 끌어올릴 수 있고, 예측을 벗어난 돌발 트래픽에서도 ECS의 빠른 Task 단위 스케일링이 특히 유효함

근거

  • Target Tracking은 EC2와 ECS에서 서로 다른 서비스로 동작함 — EC2 Auto Scaling Group은 자체 제공하는 Target Tracking으로 인스턴스 대수를 직접 조정하지만, ECS Service는 AWS의 범용 스케일링 서비스인 Application Auto Scaling을 통해 태스크(desired count) 수를 조정함
    • 태스크를 띄울 EC2 노드 자체가 부족해지면 별도의 노드 확장이 필요해 아래와 동일한 부팅 지연이 발생 — 즉 ECS는 "Task 수 조정"과 "노드 수 조정"이라는 이중 구조를 관리해야 함(EC2 단독 방식은 이 이중 구조 없이 인스턴스 단위로만 단순하게 관리)
    • 그 대신 노드 여유 자원 안에서는 훨씬 빠르게 반응할 수 있어 이 복잡도를 감수함
  • 컨테이너 기반(ECS/EKS) 오토스케일링의 속도-상한 관계 — "컨테이너가 EC2보다 빠르다"는 장점은 사전 확보된 여유 자원 안에서만 유효함
    • 컨테이너(태스크/파드) 단위 스케일링은 이미 확보된 EC2 노드에 여유 자원이 있을 때만 초 단위로 빠르고, 노드 자체가 부족해지면 결국 새 EC2가 필요해 EC2 단독 방식과 동일한 부팅 지연(1-3분)이 발생함
    • 다만 총 처리 가능한 용량 자체는 EC2 단독 방식과 마찬가지로 노드를 최대 몇 대까지 준비해두느냐에 달려있어, 컨테이너 방식이라고 목표 트래픽(Peak RPS 약 95)을 처리하지 못하는 건 아님
    • 저녁 요리 준비 시간대(RPS Peak, 1시간)처럼 예측 가능한 피크에는 20번의 Scheduled Scaling을 노드 레벨까지 확장 적용해야 컨테이너의 빠른 반응 장점을 실제로 살릴 수 있고, 그렇지 않으면 버스트 순간에 노드 부족 병목이 생길 수 있음
    • 반면 알림 발송 직후 아침 버스트(CCU Peak, 10분)는 20번에서 정한 대로 Target Tracking(Dynamic Scaling)이 기본 정책이라, 노드 레벨 대응도 결국 이 반응 지연(1-3분) 범위 안에서 이뤄짐 — 이 구간에서는 컨테이너의 빠른 Task 레벨 반응이 여유 노드가 확보되기 전까지의 공백을 일부 줄여주는 역할을 함
    • Fargate를 택하면 노드 관리 자체가 없어 이 병목이 원천적으로 사라지는 대신 비용·제어권 트레이드오프(위키 18번 비교표 참고)가 있음
  • CloudWatch 모니터링 대상 범위 — ASG/ECS뿐 아니라 AWS 리소스 전체 — 15번 한계 목록의 7번(운영/관측성 부재)과 직결되는 부분으로, CloudWatch를 오토스케일링과 직결되는 ASG/ECS 지표에만 한정하지 않고 ALB·DB(자체구축 EC2)·Redis·S3·CloudFront까지 이 아키텍처의 AWS 리소스 전체로 모니터링 대상을 넓힘 — AWS가 각 서비스에 기본 제공하는 CloudWatch 지표를 최대한 활용해 별도 모니터링 도구 도입 없이도 관측 공백을 줄일 수 있음(위키 18번 다이어그램 참고)
    • 다만 이는 모니터링 "대상 범위"만 정한 것이고, 리소스별 구체적인 지표·알림 임계치·대시보드 구성 등 세부 설계는 22-24번(운영/관측성)에서 이어서 다룸

트레이드오프

  • 관리 컴포넌트·고정 비용 증가 — 어떤 컴퓨트 방식을 택하든 v1보다 관리할 컴포넌트(ALB, ASG, ECS)가 늘어남
    • ALB 기본요금 등 1단계에서 "규모가 작아 과함"이라며 배제했던 컴포넌트들 — 15번에서 정의한 성장 규모를 감당하기 위해 불가피하게 감수하는 비용으로 판단
  • ECS 전환 비용 — 컨테이너 오케스트레이션의 관리형 스케줄링·배치 이점은 가져가지만, Task/노드 이중 구조를 관리해야 하고(위 '근거' 참고), 신규 개념(Task Definition, Service 등) 학습이 필요함
    • 기존 SSH 기반 CD 파이프라인(3단계)을 ECS 배포 방식으로 재설계하는 전환 비용도 발생 — 구체적인 자동화 방식은 23번(CI/CD 변경사항 설계)에서 이어서 다룸
  • EC2 launch type의 대가 — Fargate·ECS Managed Instances의 관리 수수료를 아끼는 대신, 인스턴스 프로비저닝·용량 조정·보안 패치를 팀이 계속 직접 수행해야 함
    • 지금은 이 부담이 크지 않다고 판단했지만, 팀 규모가 커지거나 관리할 인스턴스 대수가 늘어나면(20번 ASG max capacity 확대 등) 이 판단은 재검토 대상
  • 모니터링 대상이 늘어난 만큼 비용·설정 부담도 함께 늘어남 — CloudWatch 자체는 각 AWS 서비스에 기본 지표를 무료로 제공하지만, 커스텀 지표·알림(Alarm)·로그 보관은 리소스 수·지표 수에 비례해 비용이 늘어남. 특히 DB는 RDS가 아니라 자체구축 EC2라 RDS처럼 관리형 지표가 기본 제공되지 않아, CloudWatch Agent를 직접 설치해야 DB 프로세스 수준의 지표(커넥션 수, 쿼리 지연 등)까지 볼 수 있음 — 이 설정 자체도 별도 작업으로 필요함

19. DB 확장 방식 비교 및 아키텍처 설정

결정 요약: DB는 자체구축 EC2(Multi-AZ + Read Replica)로 확장(RDS는 배제).

이유

  • 재검토 배경 — DB 확장 방식(자체구축 유지 vs RDS 전환)은 v1에서도 "규모가 커지면 검토"로 남겨뒀던 항목이라(1단계 문서), 이번 확장 시점에 맞춰 아래 항목에서 최종 결론을 냄
  • 자체구축 EC2(Multi-AZ+Read Replica) 채택 — RDS 대비 비용 재계산 결과 — db.t3.small급 기준으로 컴퓨트 비용만 비교하면 자체구축(EC2 3대: Primary+Standby+Replica)이 RDS(Multi-AZ+Replica, 인스턴스 3대분 과금)보다 월 약 21%(약 $57 vs $78) 저렴함
    • RDS의 강점인 Multi-AZ 복제 무료 혜택이 이 월 $21 격차를 상쇄하려면 월 약 1,000GB 수준의 AZ 간 복제 트래픽이 필요함(자체구축 쪽 AZ 간 전송 표준 요금 왕복 약 $0.02/GB 기준 역산)
    • 다먹자의 쓰기 패턴(MAU가 주 1회 장보기 시점에 쓰기 발생)을 기준으로 추정한 월 복제 트래픽은 약 0.8GB로, 손익분기점(약 1,000GB)에 크게 못 미쳐 RDS의 무료 복제 혜택이 이 규모에서는 사실상 작동하지 않음
    • 따라서 컴퓨트 비용 우위가 그대로 유지된다고 판단해 RDS 대신 자체구축 EC2(Multi-AZ+Read Replica)를 최종 채택 — 이로 인해 팀이 새로 떠안는 운영 부담은 아래 '트레이드오프' 참고
    • 이 손익분기점 계산은 컴퓨트 단가·복제 트래픽 추정치에 기반한 근사치이며, 서비스 출시 후 실제 쓰기 트래픽으로 재검증 필요

트레이드오프

  • 자체구축 DB의 운영 부담 — RDS를 택했다면 관리형으로 제공받았을 백업·장애조치(Failover)·Read Replica 생성을 팀이 직접 구축·운영해야 함
    • 복제 토폴로지 구성, 장애 발생 시 페일오버 전환(페일오버 자동화 도구는 22번에서 Orchestrator로 최종 결정 — 후보 비교·근거는 22번 참고), 정기 백업·복구 테스트를 모두 직접 마련해야 함
    • 지금 팀의 운영 경험·자동화 수준에서는 이 구축·운영 부담이 상당할 수 있어, 서비스 성장 속도나 운영 자동화 수준에 따라 재검토 대상
    • 재검토 조건: 클라우드 인프라팀이 DB 운영(복제·장애조치·백업) 외에도 처리할 업무가 과중해져 이 부담을 감당하기 어려워지는 시점이 오면, 컴퓨트 비용 우위를 일부 포기하더라도 RDS 도입을 다시 고려함

20. Auto Scaling 전략 설계

결정 요약: Target Tracking(Dynamic Scaling)을 하루 전체에 상시 적용되는 기본 정책으로 채택 — Dynamic Scaling 중에서도 별도 임계치·조정폭 설계가 필요 없는 Target Tracking을 선택(Step Scaling·Simple Scaling은 배제). 저녁 요리 준비 시간대의 RPS Peak에는 예측 가능한 시각에 맞춰 Scheduled Scaling으로 desired/min capacity를 선제적으로 높여 Target Tracking의 반응 지연을 보완하고, CCU Peak(알림 발송 직후 아침 버스트)과 평시 시간대는 이런 사전 조정 없이 Target Tracking 단독으로 대응. min capacity는 평시 RPS(8-14), max capacity는 Peak RPS(약 95) 기준. ALB 1대, 서로 다른 AZ 2곳 이상 배치, Launch Template에 AMI 연결.

이유

  • Target Tracking을 상시 기본 정책으로 두고, RPS Peak에는 Scheduled Scaling을 추가로 얹음 — Target Tracking은 한 번 설정하면 24시간 내내 백그라운드에서 목표 지표를 보고 계속 반응하는 정책이라, 평시·CCU Peak·RPS Peak 구분 없이 항상 동작함. "RPS Peak엔 Scheduled 정책, CCU Peak엔 Target Tracking 정책"처럼 시간대별로 정책 자체가 바뀌는 게 아니라, Target Tracking이라는 하나의 상시 정책 위에 RPS Peak 시간대에만 Scheduled Scaling이 선택적으로 추가되는 구조임
    • RPS Peak(저녁 요리 준비 시간대)은 15번에서부터 발생 시각을 예측할 수 있는 패턴이라, Target Tracking의 반응 지연(감지→스케일아웃까지 1-3분)을 굳이 감수할 필요가 없음 — 그래서 이 시간대 시작 전에 Scheduled Scaling으로 desired/min capacity를 미리 한 단계 높여둬, Target Tracking이 반응할 부담 자체를 줄여줌
    • CCU Peak(알림 발송 직후 아침 버스트)과 평시 시간대는 이런 사전 조정 없이 Target Tracking 단독으로 대응함
      • CCU Peak은 발생 창이 10분으로 좁고, 그 규모가 "즉시반응 비율"이라는 단일 변수 하나에 크게 좌우됨(작업노트 3번 항목) — 이 변수 자체가 벤치마크(Airship 2021, 4-8%)보다 임의로 높여 잡은 값(10%)이라고 작업노트에도 명시돼 있어, 이 추정이 벗어나면 그 오차가 좁은 시간창에 그대로 몰려 순간 수치가 크게 흔들릴 위험이 큼
      • 여기에 더해, 다먹자처럼 성장 중인 서비스는 구조적으로 "지금 알고 있는 이 알림 하나"가 CCU 스파이크의 전부라는 보장이 없음 — 서비스가 커지면서 새로운 알림 트리거(예: 주간 요약, 재입고 알림, 추천 요리 알림 등)가 계속 추가될 가능성이 높고, 이런 신규 트리거들은 아직 발생 시각·규모를 알 수 없어 애초에 스케줄에 넣을 수조차 없음
      • Scheduled Scaling은 "언제, 얼마나"를 미리 알아야 동작하는 방식이라 이런 미지의 스파이크에는 원천적으로 대응할 수 없는 반면, Target Tracking은 원인이나 시각을 몰라도 실제 부하에 반응하므로 이런 구조적 불확실성에 더 안전하게 대응할 수 있음
  • Dynamic Scaling 중에서도 Target Tracking을 선택한 이유 — AWS Auto Scaling의 반응형(Dynamic) 정책은 크게 Target Tracking·Step Scaling·Simple Scaling 세 가지로 나뉨
    • Target Tracking은 목표 지표(예: 평균 CPU 사용률, ALB 요청 수)만 지정하면 AWS가 필요한 CloudWatch 알람과 조정폭을 자동으로 계산해줘, 팀이 직접 알람 임계치·조정폭을 설계·튜닝할 필요가 없음
    • Step Scaling은 알람이 임계치를 얼마나 크게 벗어났는지에 따라 조정 폭을 단계별로 세밀하게 지정할 수 있어 반응을 더 정교하게 통제할 수 있지만, 그 단계·임계치를 팀이 직접 설계하고 트래픽 패턴이 바뀔 때마다 재조정해야 함
    • Simple Scaling은 알람 하나당 조정 한 번 + 쿨다운 방식의 가장 오래된 정책으로, 쿨다운이 끝나기 전에는 추가 조정이 불가능해 빠르게 변하는 부하에는 반응이 느림 — AWS도 현재는 Step Scaling이나 Target Tracking을 권장해 사실상 레거시로 취급됨
    • 다먹자는 지금까지의 트래픽 가정 자체가 실측 데이터 없는 추정치(15번)라 Step Scaling에 필요한 세밀한 임계치·조정폭을 지금 시점에 설계해도 출시 후 곧 다시 손봐야 할 가능성이 높음 — 그래서 별도 튜닝 없이 목표치 하나로 AWS가 알아서 맞춰주는 Target Tracking이 현재 데이터 성숙도에 더 적합하다고 판단함
    • Simple Scaling은 반응 속도 자체가 셋 중 가장 느려 애초에 후보에서 제외

근거

  • 멀티 AZ 배치와 N+1 여유의 근거 — AWS 공식 EC2 SLA가 단일 인스턴스에는 Instance-Level 99.5%만 보장하고, 서로 다른 AZ 2곳 이상에 걸친 구성에는 Region-Level 99.99%를 보장하기 때문
    • 목표 가용성을 구체적으로 정의하는 22번(장애 상황과 가용성 설계)에서 이 수치를 함께 고려

트레이드오프

  • Target Tracking의 반응 지연이 CCU Peak에 그대로 노출됨 — CCU Peak을 이제 Target Tracking이 기본으로 담당하는 만큼, 인스턴스가 실제로 추가되기까지의 반응 지연(1-3분)이 알림 발송 직후 버스트(10분) 초반의 상당 부분을 차지할 수 있어, 초반에 접속하는 사용자들은 일시적으로 응답 지연을 겪을 위험이 RPS Peak보다 커짐
    • 다만 서비스가 성장하며 CCU Peak 규모의 데이터가 충분히 쌓이면, 최소한의 기저치만큼은 다시 Scheduled Scaling으로 선행 확보해두는 하이브리드 방식으로 재검토할 수 있음
  • 정책 범위를 넓힌 만큼 min/max capacity를 더 여유 있게 잡아야 함 — "지금 아는 8시 버스트"뿐 아니라 향후 생길 수 있는 미지의 CCU 스파이크까지 Target Tracking으로 감당하려면, 이미 알려진 Peak 수치만 겨우 넘기는 수준이 아니라 어느 정도 여유(헤드룸)를 둔 max capacity가 필요함 — 이 여유분만큼 평상시에도 유휴 자원 비용이 늘어날 수 있음
  • Target Tracking은 세밀한 단계별 반응이 없음 — Step Scaling처럼 "임계치를 많이 벗어날수록 더 크게 늘린다"는 세밀한 조정은 없고, 목표치에 맞추는 동일한 로직으로만 반응함 — 트래픽 실측 데이터가 쌓여 급격한 스파이크와 완만한 증가를 구분해 다르게 대응하고 싶어지면, 그때 Step Scaling 도입을 재검토할 수 있음
  • RPS Peak의 Scheduled Scaling은 예측 의존 — 반응 지연 문제는 없지만, 정확한 시각·예상 트래픽량을 미리 예측해야 함
    • 예측이 틀리면(예상보다 트래픽이 적으면 자원 낭비, 많으면 여전히 부족) 오히려 비효율이 생길 수 있음
  • RPS Peak의 예측 정확도 의존도 증가 — RPS Peak에는 Scheduled Scaling을 기본 정책으로 두는 만큼, 예측이 크게 벗어난 경우 Target Tracking이 보조로 반응하기까지는 위 반응 지연(1-3분)이 그대로 발생함
  • 운영 복잡도 증가 — 두 정책을 함께 쓰면 위 두 위험을 서로 보완할 수 있지만, 정책이 두 개로 늘어나는 만큼 설정·관리해야 할 대상도 늘어남
  • min/max capacity 산정의 불확실성 — min/max capacity를 실제 인스턴스당 처리량 없이 목표 RPS만으로 설정하면 과대/과소 산정 위험이 있음
    • 출시 후 부하테스트로 인스턴스당 처리 가능한 RPS를 측정해 min/max 대수를 재조정해야 함
    • 15번에서 짚었듯 재료 등록(영수증 인식) 흐름의 "이미지 분석 요청"처럼 처리 시간이 긴 요청의 비중에 따라 인스턴스당 처리량이 달라지므로, 이런 무거운 요청 비율까지 포함해 측정해야 실제 Peak 상황에서도 안전함
  • N+1 여유도 같은 제약 — 정확히 산정하려면 결국 인스턴스당 처리 가능한 RPS 데이터가 필요해, 위와 마찬가지로 부하테스트 이후 재조정이 필요함
  • AMI 방식의 추가 작업 — AMI를 미리 구워두는 방식은 인스턴스 기동 속도는 빠르지만, 배포할 때마다 AMI를 새로 빌드하는 파이프라인이 필요해 3단계 CD 파이프라인에 추가 작업이 생김
    • 매 배포마다 AMI를 새로 굽는 방식(Golden AMI)과 인스턴스 기동 시 최신 코드만 받아오는 방식 사이의 구체적 트레이드오프는 23번(CI/CD 변경사항 설계)에서 이어서 다룸

21. 알림(Notification) 아키텍처 설계

결정 요약: 알림 전달을 **SSE(Server-Sent Events)**와 **OS/브라우저 푸시(FCM·APNs·Web Push)**의 이중 채널로 구성하되, 채널 선택 기준은 알림 유형이 아니라 발송 시점의 앱 연결 상태 — 알림이 발생했을 때 해당 유저의 SSE 연결이 살아있으면 SSE로, 연결이 없으면(앱이 백그라운드·종료 상태) OS/브라우저 푸시로 전달. 인앱 상태에서는 벤더 인프라를 거치지 않는 SSE로 더 낮고 예측 가능한 지연을 확보하고, 앱이 닫혀있을 때는 푸시가 전달을 담당해 어떤 알림이든 최소한 한 채널로는 반드시 도달하도록 함. SSE의 다중 인스턴스 라우팅은 직접 브로드캐스트 방식을 채택 — 동시 SSE 연결 수가 많아지는 시점(아침 알림 확인 버스트)과 실제로 SSE로 보낼 이벤트가 발생하는 빈도(냉장고 공유 초대/탈퇴)가 서로 달라, 브로드캐스트의 트래픽 증가 단점이 실질적으로는 부담이 되지 않는다고 판단함(이유 참고).

BE와의 협의(github issue 기반)를 거쳐 다음 세 가지를 추가로 확정함: ① 브로드캐스트와 ACK 응답은 Redis Pub/Sub을 전파 수단으로 사용, ② SSE 서버는 별도로 분리하지 않고 **기존 BE(WAS)**에서 함께 처리(구체적인 구현 방식은 아직 미정), ③ 재연결 갭 보완은 후보 **B(재연결 시 캐치업 조회)**를 최종 채택.

이유

  • DB 폴링 방식의 한계가 확장 이후 커짐
    • v1의 DB 폴링 방식은 단일 인스턴스 구조에서는 별 문제가 없었음 — 알림 배치가 BE 프로세스와 같은 인스턴스 안에서 함께 돌아도 자원 경합이 크지 않았고, 폴링 주기로 인한 지연도 인지하기 어려운 수준이었음
    • 16-18번에서 다중 인스턴스(Scale-out) 구조로 전환하면서 이 한계가 함께 커짐 — 폴링 주기가 곧 알림 지연 시간이 되고, 인스턴스 대수가 늘어날수록 각 인스턴스가 반복적으로 DB를 조회하는 부하도 함께 늘어나 15번 한계 목록의 8번(구조적 결합도)이 확장 이후에는 더 크게 불거질 위험이 있음
    • SSE는 서버가 클라이언트에 이벤트를 즉시 밀어넣을 수 있어 폴링 주기만큼의 지연을 없앨 수 있고, 배치 작업을 BE의 일반 API 요청 처리 경로와 분리할 수 있어 8번(구조적 결합도)을 해소하는 방향과 맞음
  • OS/브라우저 푸시만으로는 충분하지 않음
    • 알림은 모두 OS/브라우저 푸시로도 전달될 수 있고, 이 채널은 앱이 포그라운드일 때도 앱 코드로 데이터를 직접 넘겨줄 수 있어 원칙적으로는 인앱 갱신도 커버 가능함
    • 다만 이 경로는 구글·애플 등 벤더의 푸시 인프라를 한 번 더 거치기 때문에 그 구간의 지연을 우리가 직접 통제할 수 없음 — 앱이 이미 열려 있어 실시간성이 특히 중요한 인앱 상황에서는, 벤더를 거치지 않고 우리 백엔드가 클라이언트와 직접 연결하는 SSE 쪽이 더 낮고 예측 가능한 지연을 보장할 수 있다고 판단해 SSE를 채택함
  • SSE 다중 인스턴스 라우팅은 직접 브로드캐스트로 채택
    • 다중 인스턴스 환경에서는 알림 이벤트가 발생한 인스턴스와 해당 유저의 SSE 연결을 들고 있는 인스턴스가 다를 수 있어, 이를 연결하는 라우팅 방식이 필요함
    • 브로드캐스트의 단점은 "동시 연결 수가 많을수록 관련 없는 인스턴스까지 이벤트를 수신하는 트래픽이 늘어난다"는 것인데, 이 단점은 "연결 수가 많은 시점에 실제로 보낼 이벤트도 많다"는 전제에서만 크게 작동함
    • 다먹자는 이 둘이 어긋남 — SSE 동시 연결이 몰리는 시점은 아침 알림 확인 버스트(많은 유저가 푸시로 온 유통기한 임박 알림을 보고 거의 동시에 접속)인데, 이때 발송해야 할 SSE 이벤트(냉장고 공유 초대·탈퇴 알림)는 공유·탈퇴 자체가 자주 일어나는 동작이 아니라서 거의 발생하지 않음
    • 즉 연결 수는 순간적으로 많아져도 브로드캐스트로 전파할 이벤트 자체가 드물어, 트래픽 증가라는 단점이 실질적인 부담으로 이어지지 않음 — 반면 연결 레지스트리·SNS+SQS는 이 정도의 이벤트 빈도에는 굳이 필요하지 않은 관리 포인트(매핑 갱신, 추가 컴포넌트)만 늘리는 셈이라 배제함
  • 브로드캐스트·ACK 전파 수단을 Redis Pub/Sub으로 정한 이유
    • BE와 협의한 결과, 이미 17번에서 Refresh Token Store로 Redis를 두고 있어 별도 컴포넌트 추가 없이 Pub/Sub 채널을 바로 활용할 수 있음
    • 인스턴스 간 이벤트 전파처럼 지연 시간이 짧고 순간적인 메시지 전달에는 Pub/Sub의 가벼운 발행-구독 모델이 적합하다고 판단 — SNS+SQS를 배제한 이유(불필요한 컴포넌트 추가)와 같은 맥락
  • SSE 서버를 분리하지 않고 기존 BE(WAS)에서 처리하기로 한 이유
    • 별도 SSE 서버로 분리하면 자원 격리와 독립적인 확장이 가능하지만, 인증·권한 처리를 다시 구현하거나 기존 BE와 내부 통신을 새로 만들어야 하고 배포·모니터링 대상도 하나 늘어남
    • 반면 기존 BE에서 처리하면 기존 인증·권한 로직을 그대로 재사용할 수 있음 — 지금 규모에서는 SSE 연결 부하가 일반 API 처리에 영향을 줄 정도로 크지 않다고 판단해 미분리로 결정. SSE 연결 부하가 커지거나 독립 확장이 필요해지면 서버 분리를 재검토
    • 다만 이는 "어느 프로세스에서 처리할지"까지만 정한 것이고, 그 안에서 구체적으로 어떤 방식(예: Spring MVC의 SseEmitter 등)으로 구현할지는 아직 정하지 않음
  • 멀티 디바이스로 인한 다중 ACK를 정상으로 간주해 모두 허용하기로 한 이유
    • 17번에서 이미 Refresh Token을 유저-디바이스 1:N 구조로 관리하고(Push 토큰도 동일한 1:N 구조), RTR로 토큰 탈취 자체는 별도로 탐지·대응하고 있어, 한 유저가 여러 기기에서 동시에 로그인해 있는 것 자체는 이미 정상적으로 지원되는 사용 패턴임
    • 여러 인스턴스에서 ACK가 동시에 온다는 건 그만큼 여러 기기가 실제로 연결돼 있다는 뜻일 뿐이라, 이를 zero-trust 관점에서 이상 신호·에러로 처리하면 정상적인 멀티 디바이스 사용자까지 차단하게 되고, 정작 보안 위협(토큰 탈취)은 이미 RTR이 별도로 담당하고 있어 다중 연결 자체를 의심할 근거가 없음
  • 재연결 갭 보완을 B(재연결 시 캐치업 조회)로 최종 채택한 이유
    • 알림 본문과 읽음 상태는 이미 notifications·notification_recipients 테이블에 영속화돼 있어, Redis(브로드캐스트 경로)에서 이벤트가 유실돼도 재연결 후 마지막으로 받은 notificationId 이후 데이터를 DB에서 조회하면 복구할 수 있음
    • C-1/C-2(Last-Event-ID + Redis Streams 또는 DB)도 검토했으나, 별도 이벤트 재생 프로토콜을 구현하는 것 대비 얻는 이점에 비해 알림 DB와 역할이 중복되고 보존·정리·중복 처리 등 운영 복잡도가 커진다는 단점이 현재 단계에서는 더 크다고 판단함

근거

  • v1은 EC2 1대 구조라 알림 배치와 API 서버가 같은 프로세스·같은 인스턴스 안에 있었고, 폴링 주기를 짧게 잡을수록 DB 조회 빈도가 함께 올라가는 트레이드오프가 있어 결과적으로 "적당히 긴 폴링 주기 + 인지 가능한 수준의 지연"으로 운영됨 — 규모가 작을 때는 이 지연이 감내할 만한 수준이었음
  • 확장 이후(16-18번) 인스턴스가 여러 대로 늘어난 상황에서도, Read Replica(19번)로 조회를 분산하는 것만으로는 알림만을 위한 불필요한 조회가 반복되는 구조적 비효율 자체는 해소되지 않음
  • SSE는 HTTP 위에서 단방향 실시간 푸시를 표준 브라우저 API(EventSource)로 지원해 WebSocket 대비 도입 복잡도가 낮고, 알림처럼 서버→클라이언트 단방향 전달만 필요한 경우에 적합함

Push 채널 설계

  • 클라이언트가 로그인 이후 플랫폼별 푸시 SDK(FCM/APNs/Web Push 등)로부터 디바이스 토큰(또는 구독 정보)을 발급받아 BE에 등록 — 한 유저가 여러 디바이스를 가질 수 있으므로 유저-디바이스 1:N 구조로 별도 저장
  • 알림 이벤트가 발생했을 때 대상 유저의 SSE 연결이 없는 것으로 판단되면(아래 SSE 채널 설계의 연결 상태 확인 로직 참고), BE가 그 유저의 등록된 토큰들을 조회해 해당 벤더의 발송 API를 호출 — 실제 기기까지의 전달은 벤더가 대행
  • 토큰은 앱 재설치·만료 등으로 무효화될 수 있어, 벤더 응답의 실패 코드를 기준으로 무효 토큰을 정리(삭제 또는 갱신 요청)하는 처리가 필요
  • 지원 플랫폼(웹/네이티브)이 아직 확정되지 않아, 실제 사용할 벤더 조합(FCM만 / FCM+APNs / FCM+APNs+Web Push)과 벤더별 세부 연동(SDK, 인증키 관리, 또는 AWS SNS Mobile Push 같은 통합 발송 서비스 활용 여부)은 플랫폼 확정 후 별도로 정함

SSE 채널 설계

  • 클라이언트는 로그인 이후 SSE 엔드포인트에 연결을 열어두고, 앱이 포그라운드에 있는 동안 이 연결을 유지

  • SSE 서버는 별도로 분리하지 않고 기존 BE(WAS) 프로세스에서 함께 처리

  • 알림 이벤트가 발생하면 BE는 이벤트를 Redis Pub/Sub 채널로 모든 인스턴스에 브로드캐스트하고, 각 인스턴스는 자신이 그 유저의 SSE 연결을 들고 있는지 확인해 들고 있으면 그 연결로 이벤트를 전달함(아래 라우팅 후보 비교 참고)

  • "연결이 없다"(=SSE로 아무도 받지 못함)는 것은, 브로드캐스트한 이벤트에 대해 일정 시간 내 어떤 인스턴스로부터도 전달 확인(ACK)이 오지 않으면 연결이 없는 것으로 간주해 Push 채널로 폴백하는 타임아웃 방식으로 판단 — 연결 레지스트리처럼 조회 즉시 알 수 있는 방식이 아니라, 이 타임아웃만큼의 판단 지연이 생김(트레이드오프 참고). ACK도 브로드캐스트와 같은 Redis Pub/Sub 채널을 통해 발신 인스턴스로 회신되며, 구체적인 채널 구성(요청-응답 매칭 방식)과 타임아웃 값은 아직 미정

  • 한 유저가 여러 기기·탭에서 동시에 SSE 연결을 맺고 있으면 ACK가 여러 인스턴스로부터 동시에 올 수 있음 — 이는 이상 신호가 아니라 정상 상황으로 간주해 모두 허용하며, ACK를 보낸 인스턴스 각각이 자신이 들고 있는 연결로 이벤트를 전달해 연결된 모든 기기가 실시간으로 갱신되도록 함(이유 참고)

    후보 방식 장점 단점
    직접 브로드캐스트 ✅ 이벤트를 모든 인스턴스에 전파하고, 각 인스턴스가 자신이 들고 있는 연결인지 확인 후 전달 별도 라우팅 정보(연결 레지스트리)를 관리할 필요 없어 구현이 가장 단순 인스턴스 수가 늘수록 관련 없는 인스턴스까지 이벤트를 수신하는 불필요한 트래픽이 비례해서 늘어남(다만 다먹자는 SSE 이벤트 발생 빈도 자체가 낮아 이 단점이 실질적 부담은 아님 — 이유 참고)
    연결 레지스트리(예: Redis) 유저→인스턴스 매핑을 중앙 저장소에 유지, 이벤트 발생 시 매핑을 조회해 해당 인스턴스에만 전달 필요한 인스턴스에만 정확히 전달돼 트래픽 낭비가 적음 연결 생성·종료마다 매핑을 갱신해야 해 관리 포인트가 늘고, 인스턴스 비정상 종료 시 매핑이 stale해지지 않도록 별도 정리가 필요
    SNS + SQS(Pub/Sub + 인스턴스별 큐) 이벤트를 SNS로 발행해 인스턴스별 SQS 큐로 팬아웃, 각 인스턴스가 자기 큐만 구독 셋 중 전달 안정성(메시지 유실 방지, 재시도)이 가장 높음 컴포넌트(SNS+SQS)가 추가되고, 구조 복잡도·지연·비용도 셋 중 가장 큼
  • 연결이 바뀌는 순간(재연결 갭)의 문제 — 브로드캐스트는 유저→인스턴스 매핑을 따로 관리하지 않아, 연결이 다른 인스턴스로 옮겨가도(재연결) 다음 이벤트부터는 그 시점의 실제 연결 여부를 각 인스턴스가 다시 확인하므로 "매핑이 낡아서 잘못된 인스턴스로 보낸다"는 문제 자체는 생기지 않음

    • 다만 기존 연결이 끊기고 새 인스턴스에 다시 붙기까지 걸리는 짧은 공백(오토스케일링으로 인한 인스턴스 제거, 네트워크 순단 등) 동안 정확히 이벤트가 발생하면, 그 순간엔 어떤 인스턴스도 연결을 들고 있지 않아 ACK가 오지 않고 타임아웃 후 Push로 폴백됨 — 알림 자체가 유실되지는 않지만(Push로는 도착), 앱이 사실상 열려 있었는데도(재연결 중이었을 뿐) 그 순간엔 Push로만 전달되고, 재연결이 끝난 뒤에는 SSE로 그 이벤트가 다시 오지 않아 인앱 화면은 최신 상태를 반영하지 못한 채로 남을 수 있음

    재연결 갭 보완 후보

    후보 방식 장점 단점
    A. 현재 설계 유지(Push 폴백만) 타임아웃 후 Push로만 보완, 재연결 후 별도 동기화 로직 없음 추가 구현이 필요 없음 재연결 직후 인앱 화면이 최신 상태를 반영하지 못할 수 있음 — 유저가 직접 새로고침하거나 다른 화면 전환으로 다시 조회해야 반영됨
    B. 재연결 시 캐치업 조회 ✅ 클라이언트가 SSE 재연결(연결 성공)을 감지할 때마다 "놓친 이벤트가 있는지" 가벼운 API를 한 번 호출해 최신 상태로 동기화 이벤트 빈도가 낮아 구현·비용 부담이 작고, 브로드캐스트 구조를 그대로 유지하면서 재연결 갭만 보완 가능 클라이언트에 재연결 감지·동기화 호출 로직을 별도로 추가해야 하고, 캐치업 응답은 평소 SSE 이벤트와 다른 경로(REST 응답)로 오므로 클라이언트가 두 경로를 각각 처리해야 함. 서버 쪽도 알림 기록용 DB 테이블·조회 API를 새로 구현해야 함
    C-1. Last-Event-ID + 이벤트 버퍼(예: Redis Streams) 이벤트를 일정 기간 버퍼에 저장해두고, 재연결 시 브라우저가 자동으로 보내는 Last-Event-ID 이후의 이벤트를 SSE 재생으로 전달 클라이언트는 표준 재연결 동작만 쓰면 되어 커스텀 로직이 없고, 재생된 이벤트도 평소 SSE 핸들러로 동일하게 처리됨(캐치업 전용 경로가 따로 없음) 버퍼 컴포넌트(Redis Streams 등)와 보관 정책을 새로 설계해야 해, 지금의 낮은 이벤트 빈도 대비 과한 복잡도일 수 있음
    C-2. Last-Event-ID + DB 조회(버퍼 대신 DB) C-1과 동일하게 Last-Event-ID 재연결 프로토콜은 그대로 쓰되, 재생 대상을 별도 버퍼가 아니라 알림 DB 테이블에서 조회해 SSE로 재생 — 알림 테이블의 PK(또는 별도 순번)를 그대로 이벤트 id로 사용 C-1의 장점(클라이언트 표준 동작, 단일 이벤트 처리 경로)을 유지하면서, 새 버퍼 컴포넌트 없이 기존 DB만으로 구현 가능. 알림 이력도 자연히 DB에 영구로 남아 향후 알림함 등 기능에 재활용 가능 인메모리 버퍼보다 조회 지연이 다소 있음(지금 이벤트 빈도에서는 미미). 버퍼처럼 자동 정리되지 않아 테이블이 쌓이면 별도 보관·정리 정책이 필요해짐(시급하지 않음)

    BE와 협의한 결과 **B(재연결 시 캐치업 조회)**를 최종 채택함(이유 참고) — 알림 본문·읽음 상태가 이미 DB에 영속화돼 있어 별도 버퍼·재생 프로토콜(C-1/C-2) 없이도 캐치업이 가능하다고 판단. 클라이언트는 재연결 성공 시점(예: EventSource의 onopen)에 마지막으로 받은 notificationId를 파라미터로 캐치업 API를 호출해 놓친 알림을 채우고, 이후 이어지는 SSE 이벤트를 정상 수신하는 흐름

트레이드오프

  • SSE+Push 이중 채널 구성 자체에 대한 트레이드오프
    • DB 폴링 대비 얻는 것과 잃는 것의 절충 — 폴링 방식의 단순함(별도 연결 유지 없이 주기적 조회만 하면 됨)을 포기하는 대신 실시간성과 DB 부하 감소를 얻는 절충으로 판단함
    • 이중 채널 운영 부담 — SSE와 OS/브라우저 푸시 두 채널을 동시에 운영해야 해서, SSE 서버와 푸시 발송 연동이라는 두 시스템을 각각 구축·유지보수해야 하는 부담이 생김
    • 연결 유지 비용 — SSE는 연결을 계속 열어두는 방식이라, 동시 접속자 수만큼 인스턴스가 열린 커넥션을 유지해야 하는 부담이 생김 — 폴링처럼 요청-응답이 끝나면 자원을 반납하는 구조보다 인스턴스당 동시 처리 가능한 연결 수를 함께 고려해야 함
    • SSE 연결 상태 판단의 정확도에 전체 흐름이 의존함 — 발송 시점에 "연결이 살아있다"고 잘못 판단하면(예: 막 끊긴 직후처럼 갱신이 늦은 경우) 알림이 SSE로 전송되다 유실될 수 있고, 반대로 "연결이 없다"고 잘못 판단하면 앱이 열려있는데도 푸시로만 전달돼 지연이 커짐 — 판단 로직의 정확도와, SSE 전송 실패 시 즉시 Push로 재시도하는 폴백 처리가 함께 필요함
    • SSE·Push 동시 도달로 인한 중복 수신 가능성 — ACK 타임아웃 경계 부근에서 SSE 전달이 타임아웃 직전·직후로 늦게 성공하는 동시에 Push도 이미 폴백으로 발송됐다면, 같은 알림이 SSE와 Push 두 채널로 한 유저에게 중복 도달할 수 있음 — 알림 ID 기준으로 클라이언트가 이미 표시한 알림을 걸러내는 중복 제거 처리가 필요하며, 현재는 이 처리가 설계돼 있지 않음
  • SSE 다중 인스턴스 라우팅(직접 브로드캐스트)에 대한 트레이드오프
    • 이벤트 발생 빈도가 늘어나면 브로드캐스트의 트래픽 단점이 그대로 드러남 — 지금은 SSE로 보낼 이벤트(냉장고 공유 초대·탈퇴)가 드물어 문제되지 않지만, 향후 SSE로 처리할 알림 유형이 늘거나 해당 이벤트 빈도 자체가 늘면 인스턴스 수에 비례한 불필요한 트래픽이 실제 부담이 될 수 있어, 그 시점에는 연결 레지스트리 등으로 재검토 필요
    • 연결 여부 판단에 타임아웃만큼의 지연이 생김 — 브로드캐스트 방식은 연결 레지스트리처럼 즉시 조회가 안 돼, "아무도 응답하지 않음"을 확인하려면 일정 시간을 기다려야 하고, 이 대기 시간만큼 Push 폴백이 늦어짐 — 다만 SSE 자체가 인앱 상태를 노리는 채널이라 폴백이 다소 늦어도 치명적이지 않다고 판단함
  • 브로드캐스트·ACK 전파 수단(Redis Pub/Sub)에 대한 트레이드오프
    • 브로드캐스트·ACK가 Redis Pub/Sub에 의존하게 됨 — SSE 다중 인스턴스 전파와 ACK 회신을 Redis Pub/Sub으로 구현하기로 하면서, Redis가 담당하는 역할이 17번의 Refresh Token 저장에서 SSE 이벤트 전달까지 넓어짐
    • 다만 Redis Pub/Sub이 끊겨도 각 인스턴스의 ACK 자체가 오지 않게 되어 기존 타임아웃 메커니즘으로 자동으로 Push 폴백되므로, 알림 유실로 이어지지는 않음 — 다만 그 시점 SSE 경로 전체가 죽는 것과 같아 인앱 화면 갱신 누락이 늘 수 있고, 22번의 "Redis 단일 인스턴스 유지(보류)" 트레이드오프와 영향 범위가 겹침
  • 재연결 갭 보완(B안: 재연결 시 캐치업 조회)에 대한 트레이드오프
    • 오토스케일링과의 상호작용 — 인스턴스가 스케일 인(제거)되면 그 인스턴스에 연결돼 있던 SSE 연결이 끊기고 클라이언트가 재연결해야 함(브라우저 EventSource는 자동 재연결을 지원). B안 채택으로 이 갭 동안 이벤트가 발생해도 재연결 후 캐치업 API로 화면을 복구할 수 있게 됐지만, 클라이언트에 재연결 감지·캐치업 호출·응답 병합 로직을 별도로 구현해야 하는 부담은 그대로 남음(SSE 채널 설계 참고)
  • SSE 서버 미분리에 대한 트레이드오프
    • SSE 서버를 분리하지 않은 데 따른 자원 공유 부담 — SSE 연결 유지·브로드캐스트 수신·이벤트 전달이 모두 기존 BE 프로세스 안에서 이뤄지므로, SSE 연결 수가 많아지면 일반 API 요청 처리와 자원(스레드·커넥션 등)을 나눠 써야 함
  • Push 채널에 대한 트레이드오프
    • 푸시 벤더가 아직 미확정 — 지원 플랫폼(웹/네이티브)이 정해지지 않아 Push 채널 설계를 특정 벤더에 종속되지 않게 추상화해둔 상태 — 플랫폼이 정해지면 벤더별 세부 연동(SDK 통합, 인증키 관리 등)을 추가로 설계해야 함

22. 장애 상황과 가용성 설계

결정 요약: 16-20번에서 컴포넌트별로 따로 마련해둔 이중화 장치(Scale-out, Multi-AZ, ASG, Read Replica 등)를 장애 시나리오별로 한데 모아 실제 영향과 대응을 정리.

  • DB Primary 장애조치 자동화 — 후보 비교 끝에 Orchestrator를 채택. 쿼럼 기반 장애 판단으로 split-brain 위험을 줄일 수 있다는 근거를 바탕으로 함(아래 근거 참고)
  • Read Replica 장애 — Primary 폴백·Standby 라우팅을 모두 배제하고 성능 저하를 감수하는 쪽으로 결정. 다만 적극적인 대응책을 마련한 것은 아니라는 점에서 상태는 보류로 표기
  • Redis 이중화 — 장애 영향 범위(로그인 갱신·로그아웃·SSE Pub/Sub)가 제한적이라고 판단해 별도 가용성 처리 없이 단일 인스턴스 유지(보류)
  • Noisy Neighbor 대응 — 18번에서 ECS EC2 launch type을 채택하며 생긴 문제로, 컨테이너별 memory·memoryReservation 설정 계획을 명시

컴포넌트별 장애 시나리오 대응

시나리오 영향 현재 설계상 대응 상태
앱서버 EC2 인스턴스 1대 장애 해당 인스턴스로 가던 요청 실패 ALB 헬스체크가 자동으로 제외하고 ASG가 대체 인스턴스를 기동(18, 20번) — min capacity가 N+1을 고려해 설정돼 있어(20번) 남은 인스턴스만으로도 평시 트래픽은 감당 가능. 이 방식은 인스턴스를 "폐기 후 재생성"하는 방식으로, 17번에서 Stateless로 설계해둔 덕에 로컬 상태 손실을 걱정할 필요가 없어 자연스럽게 맞음 해결
앱서버 컨테이너(Task) 자원 과다 사용 컨테이너 하나가 메모리 누수·CPU 스파이크로 자원을 과다 사용하면, 같은 EC2 호스트에 함께 배치된 다른 Task까지 자원 부족의 영향을 받을 수 있음(Noisy Neighbor) ECS Task Definition에 컨테이너별 memory(하드 리밋)·memoryReservation(소프트 리밋/예약)을 설정해, 문제가 생긴 컨테이너만 OOM Kill되어 격리·재시작되도록 계획 중 — 구체적인 수치는 실제 부하테스트 이후 산정 예정 해결(계획)
가용영역(AZ) 전체 장애 해당 AZ의 인스턴스·DB 컴포넌트 전체 사용 불가 앱서버는 ASG가 서로 다른 AZ 2곳 이상에 분산 배치(18, 20번)돼 다른 AZ로 트래픽을 지속 처리함 — 해결. DB는 Primary/Standby가 서로 다른 AZ(19번)라 쓰기는 Standby로 failover 가능하지만, Read Replica는 Primary와 같은 AZ에 있어 AZ 전체 장애 시 함께 사라짐 — 새 Replica를 다시 프로비저닝하기 전까지 읽기 경로는 복구되지 않음. DB Primary 장애조치 자동화(아래 참고)는 Primary 한 대만 죽는 상황(예: 과도한 쓰기 부하로 인한 다운)을 전제로 설계한 것으로, AZ 전체 장애로 DB 컴포넌트가 한꺼번에 사라지는 상황은 이번 설계 범위에 포함하지 않음(아래 이유·트레이드오프 참고) 해결(앱서버) / 부분 해결(DB — 쓰기만, 읽기는 범위 밖)
ALB 장애 전체 서비스 접근 불가 AWS가 여러 AZ에 걸쳐 관리형으로 운영해 Region-level 가용성을 자체 보장(20번 근거) — 팀이 별도로 설계할 대상이 아님 해결(AWS 관리형)
S3 장애 프로필 사진 업로드·조회 불가 AWS가 매우 높은 내구성·가용성을 관리형으로 제공 — 별도 설계 불필요 해결(AWS 관리형)
DB Primary 장애 쓰기(Write) 요청 전체 실패 Standby로 failover(19번 Multi-AZ) — 자체구축이라 RDS처럼 자동 failover가 기본 제공되지 않아, 장애 감지·승격 자동화 도구로 Orchestrator를 채택(후보 비교·근거는 아래 이유·근거 참고). Orchestrator 자체의 HA는 Semi HA, 장애 후 클라이언트 리다이렉션은 VIP/DNS 방식으로 결정함(후보 비교는 아래 이유 참고). 대비 대상은 Primary 한 대만 죽는 상황으로 한정 — AZ 전체 장애 시나리오는 위 "가용영역(AZ) 전체 장애" 행 참고 해결(방식 결정 완료, 세부 구현은 별도 설계 필요)
Read Replica 장애 읽기(Read) 트래픽 일부 실패 Primary·Standby 어디로도 읽기를 돌리지 않고 성능 저하를 감수(폴백 없음) — Primary는 쓰기 전용을 유지해 읽기 스탬피드가 발생해도 쓰기 경로에는 영향이 없도록 함(아래 이유 참고) 보류(성능 저하 감수, 별도 대응 없음)
Redis(Refresh Token Store·SSE Pub/Sub) 장애 Refresh Token 발급·갱신·즉시 무효화(로그아웃) 및 SSE 브로드캐스트·ACK 불가 Access Token은 Stateless라 만료 전까지는 서비스 자체는 계속 동작하고, SSE도 타임아웃 후 Push로 폴백돼 알림 자체는 유실되지 않음 — 로그인 갱신·강제 로그아웃·SSE 인앱 갱신 기능만 Redis 장애 동안 멈춤. 영향 범위가 제한적이라 판단해 이중화 없이 단일 인스턴스를 유지하기로 결정(아래 이유 참고) 보류(단일 인스턴스 유지, 가용성 처리 안 함)
신규 배포로 인한 장애 배포 직후 오류 발생 배포·롤백 절차는 23번(CI/CD 변경사항 설계)에서 다룸 23번으로 위임
Refresh Token 탈취(보안 사고) 탈취된 토큰으로 공격자가 세션을 유지할 위험 RTR(17번)로 이미 폐기된 토큰이 재사용되면 탈취로 간주해 해당 토큰 계열 전체를 무효화 — 이번에 여기에 "탈취 감지 시 해당 유저의 모든 활성 세션을 로그아웃 처리하고 재로그인을 요구"하는 절차까지 추가로 정의함(아래 이유 참고) 해결(정책 신규 정의)

이유

  • 컴포넌트별로 따로 존재하던 이중화 장치를 장애 시나리오 매트릭스로 재정리한 이유

    • 16-20번은 "이 컴포넌트를 어떻게 확장할까"라는 관점에서 각각 결정됐지만, 정작 "이 컴포넌트가 죽으면 전체 서비스가 어떻게 되는가"라는 질문에 답한 적은 없었음
    • 매트릭스로 모아보니 DB failover 자동화, Read Replica 폴백, Redis 이중화처럼 각 섹션에서 "트레이드오프"나 "추후 검토"로만 짧게 언급되고 넘어갔던 항목들이 실제로는 아직 아무 결론도 없는 상태라는 게 드러남 — 이 문서의 목적상 이런 공백을 숨기지 않고 명시적으로 남겨두는 것이 맞다고 판단함
  • DB Primary 장애조치 자동화가 전제하는 장애 범위 — 이 설계(와 아래 Orchestrator 채택 검토)는 Primary 한 대만 죽는 상황(예: 과도한 쓰기 부하로 인한 다운, 프로세스 크래시 등)을 대비 대상으로 함. AZ 전체가 통째로 사라지는 상황은 이번 설계에서 예정하지 않음 — 쓰기는 19번에서 이미 Standby를 다른 AZ에 둔 덕분에 이 경우에도 failover가 가능하지만, Read Replica가 Primary와 같은 AZ(AZ-a)에만 있어 AZ 전체 장애 시 함께 사라지고, 이 읽기 경로 공백에 대한 대응은 아직 마련하지 않음(트레이드오프 참고)

  • DB Primary 장애조치 자동화 도구로 Orchestrator를 채택한 이유 — 처음에는(A, 최초 검토안) 헬스체크를 주기적으로 보내다 응답이 없으면 Standby를 Primary로 승격하는 자체 스크립트 방식을 검토했으나, 이 방식은 관찰자 혼자만의 판단에 의존해 "Primary가 실제로 죽은 상황"과 "관찰자와 Primary 사이 네트워크만 끊긴 상황"을 구분하지 못한다는 문제가 확인됨 — 후자를 전자로 오판하면 여전히 살아있는 기존 Primary와 새로 승격된 Standby가 동시에 쓰기를 받는 split-brain으로 이어질 수 있음. 이 위험을 구조적으로 해결하는 도구가 있는지 비교하기 위해 A(자체 헬스체크, 최초 검토안)·B(수동)·C-1(Orchestrator)·C-2(MHA) 네 후보를 비교한 끝에 Orchestrator를 최종 채택함(관련 근거는 아래 근거 참고)

    후보 방식 장점 단점
    A. 자체 헬스체크 스크립트(최초 검토안) 팀이 직접 헬스체크를 주기적으로 보내다 일정 시간 응답이 없으면 Standby를 Primary로 승격 별도 도구 학습 없이 바로 시작 가능 단일 관찰자로는 "Primary 실제 장애"와 "나만 연결 끊김"을 구분 못해 split-brain 위험, fencing 처리 누락 가능, 헬스체크 주체 자체가 새 단일 장애점, 복제 지연·데이터 최신성 검증도 직접 구현 필요 — MHA가 실제로 겪은 네트워크 오판 문제를 그대로 재현할 위험이 큼
    B. 수동 failover 장애를 감지한 팀원이 직접 Standby를 Primary로 승격 도구 도입 없이 바로 시작 가능 대응 속도가 사람(감지·판단·실행)에 의존해 RTO가 길어짐
    C-1. Orchestrator ✅ 여러 노드의 관점을 쿼럼 방식으로 종합해 장애를 판단하고 자동 승격(무료 오픈소스, MySQL 계열 지원) 단일 관찰자보다 split-brain 위험이 낮고, 네트워크 장애처럼 "진짜 장애"와 "관찰자만 연결이 끊긴 상황"을 구분하기 어려운 경우에도 여러 노드의 관점을 종합해 오판을 줄임(근거 참고) 별도 메타데이터 스토어가 필요하고, 장애 감지 이후 애플리케이션이 새 Primary로 자동 전환되게 하려면 ProxySQL 등 클라이언트 리다이렉션 구성 요소가 추가로 필요함(트레이드오프 참고)
    C-2. MHA 승격 시 데이터 유실이 가장 적은 replica를 자동으로 선택(무료 오픈소스, MySQL 계열 지원) 구성이 가볍고 데이터 유실 최소화에 특화 오랫동안 기능 추가·유지보수가 없고 Perl 기반이라 유지보수 부담이 크며, Source 서버 상태만으로 장애를 판단해 네트워크 장애를 실제 장애로 오판하기 쉬움(근거 참고)

    A(자체 헬스체크)는 검증된 도구 없이 split-brain·fencing·오판 문제를 직접 풀어야 해 배제, B(수동)는 대응 속도가 사람에 의존해 배제, C-2(MHA)는 오랜 기간 유지보수가 끊겼고 네트워크 장애 오판이라는 구조적 한계가 실제 대규모 운영 사례(근거 참고)에서도 확인돼 배제 — 결국 쿼럼 기반으로 오판을 줄이면서 무료로 쓸 수 있는 Orchestrator(C-1)를 최종 채택함. PostgreSQL 전용인 Patroni는 DB가 MySQL이라 애초에 후보에서 제외함

  • Orchestrator가 DB 증설 이후에도 별도 재구성 없이 계속 사용 가능한 이유 — Orchestrator는 등록된 인스턴스 하나를 시작점으로 주기적 polling과 재귀적 topology crawling(마스터·Replica 관계를 반복적으로 조회해 전체 복제 구조를 스스로 파악)을 수행함 — Read Replica를 증설해도 새 노드를 Orchestrator에 수동으로 등록할 필요 없이 다음 polling 주기에 자동으로 모니터링 대상에 편입됨(근거 참고). DB를 증설(Read Replica 추가 등)해도 장애 감지·failover 자동화 도구를 다시 구성하거나 교체할 필요가 없다는 점에서, 지금 규모뿐 아니라 향후 DB가 커지는 상황에도 그대로 쓸 수 있다고 판단함

  • Orchestrator 자체의 HA 구성과 클라이언트 리다이렉션 방식을 결정한 이유 — Orchestrator 채택 시점에는 미정으로 남겨뒀던 두 가지, 즉 ① Orchestrator 자체의 HA 구성 방식과 ② 장애 후 애플리케이션이 새 Primary로 자동 전환되는 방식(클라이언트 리다이렉션)을 아래와 같이 결정함

    ① Orchestrator 자체 HA 구성

    후보 방식 장점 단점
    A. No HA 단일 인스턴스로 운영 추가 인프라 없이 바로 시작 가능 Orchestrator 자체가 새 단일 장애점 — 죽으면 자동 failover 판단 자체가 멈추고 수동 대응으로 폴백해야 함
    B. Semi HA ✅ 완전한 Raft 쿼럼 없이 2대로 구성 단일 인스턴스보다 여유가 있으면서 Raft 3대 구성보다 비용·운영 부담이 적음 완전한 쿼럼 기반 판단은 아니라, 두 노드 간 판단이 갈리는 상황에 대한 동작은 별도로 설계해야 함(트레이드오프 참고)
    C. Raft HA(최소 3대) Orchestrator 기본 지원 방식 쿼럼 기반으로 가장 안정적(근거 참고) 다먹자 규모엔 오버스펙 — 인스턴스 3대분 비용·운영 부담(근거 참고)

    ② 클라이언트 리다이렉션

    후보 방식 장점 단점
    A. 미연동(수동/재연결 의존) 별도 구성 없이 앱서버 재연결에 의존 구성 요소 추가 없이 바로 시작 가능 너무 수동적 — 누군가 커넥션 정보를 직접 바꿔주기 전까지 쓰기 자체가 막혀 RTO가 크게 늘어남
    B. VIP/DNS ✅ Orchestrator의 failover 훅(PostMasterFailoverProcesses)으로 Route53·Elastic IP 갱신 상시 떠 있는 컴포넌트 없이 구현 가능, 앱서버는 고정 주소만 바라보면 됨 ProxySQL 대비 신속성이 낮음(DNS TTL 캐싱 등), 앱서버 쪽 재연결·재시도 로직은 별도로 필요
    C. ProxySQL 프록시가 쿼리를 가로채 자동 라우팅 + 읽기 분산까지 처리 가장 완전한 자동화, 앱서버가 DB 토폴로지를 몰라도 됨 비용 증가 + 상시 관리해야 하는 컴포넌트가 하나 더 늘어남 — v2 규모에는 부적합, v3(트래픽·운영 여력 증가 시) 후보로 유지

    A(미연동)는 재연결이 전적으로 사람·애플리케이션 재시도에 의존해 RTO가 크게 늘어나 배제, C(ProxySQL)는 신속성·기능 면에서는 가장 완전하지만 비용 증가와 상시 관리 컴포넌트 추가라는 부담이 지금 규모에는 맞지 않다고 판단해 배제(v3 후보로 유지) — 결국 신속성은 다소 떨어지더라도 추가 상시 컴포넌트 없이 구현 가능한 VIP/DNS(B)를 채택함. Orchestrator 자체 HA는 단일 인스턴스(A)의 SPOF 위험과 Raft 3대(C)의 오버스펙 사이에서 Semi HA(B)를 채택함

  • Read Replica 장애 시 읽기를 성능 저하 감수로 정리한 이유(Primary 폴백·Standby 라우팅 모두 배제, 상태는 보류로 표기)

    • 15번에서 정리한 읽기:쓰기 비율(7:1~14:1)상 이 서비스는 읽기가 압도적으로 많고 쓰기는 적음. 이런 트래픽 구조에서는 Primary를 "쓰기 전용"으로 엄격히 유지하는 것 자체가 안정성의 핵심 전제라, Read Replica 장애 시 읽기 트래픽을 Primary로 돌리는 안(후보 A)은 애초에 배제함 — 쓰기만 처리하던 Primary가 읽기 스탬피드까지 떠안으면 쓰기 가용성까지 함께 위협받을 수 있기 때문
    • Standby로 읽기를 돌리는 안(후보 C, Postgres hot standby 구조상 기술적으로는 가능)도 같은 맥락에서 배제함 — Standby의 존재 이유는 Primary 장애 시 즉시 승격 가능한 상태를 유지하는 것인데, 평소 읽기 부하까지 얹으면 복제 지연이 늘거나(특히 동기 복제라면 Primary 쓰기 지연에도 영향) Primary·Read Replica가 동시에 문제되는 최악의 순간에 Standby가 읽기 부하로 바쁜 상태에서 승격해야 하는 상황이 생길 수 있음
    • 결국 Read Replica 장애 시에는 어디로도 우회하지 않고 성능 저하를 그대로 감수하는 쪽을 택함 — Primary·Standby 모두 배제라는 판단 자체는 확정이지만, 적극적인 대응책을 마련한 것은 아니므로 상태는 "해결"이 아니라 "보류"로 표기함
  • Redis 이중화를 하지 않기로 결정한 이유(보류) — A(단일 인스턴스 유지)·B(ElastiCache Replication Group)·C(자체구축 Sentinel/Cluster) 세 후보 중 A를 최종 채택함

    후보 방식 장점 단점
    A. 단일 인스턴스 유지 ✅ 현재처럼 Redis를 한 대로 운영 추가 비용·복잡도 없음 Redis 장애 시 로그인 갱신·로그아웃·SSE Pub/Sub 기능이 함께 일시 중단됨
    B. ElastiCache Redis Replication Group Multi-AZ 관리형 자동 failover 관리형이라 자체 구축 부담 없음 비용 증가
    C. 자체구축 Redis Sentinel/Cluster 직접 구성하는 Sentinel/Cluster 기반 이중화 관리형 대비 비용 절감 가능 19번의 DB 자체구축과 같은 운영 부담이 하나 더 늘어남
    • Redis 장애 시 영향 범위가 로그인 갱신(Refresh Token 발급·갱신)·로그아웃·SSE Pub/Sub(브로드캐스트·ACK)로 제한적이고, Access Token은 Stateless라 서비스 자체는 계속 동작하며 SSE도 타임아웃 후 Push로 폴백돼 알림 자체가 유실되지는 않음 — 이 정도 영향은 지금 단계에서 감내 가능한 수준이라 판단해 B·C의 비용·운영 부담을 들일 우선순위는 아니라고 봄
    • 이는 17번에서 Redis를 Refresh Token 저장 용도로 좁게 설계해둔 방향과도 일치함 — BE가 Redis의 책임 범위를 처음부터 좁게(매 요청 조회되는 범용 세션 스토어가 아니라 토큰 발급·갱신 시점에만 쓰도록) 설계해둔 덕에, 장애가 나도 영향받는 기능이 이 정도로 제한되는 것이므로 "BE 설계에 맞춘다"기보다는 "BE가 이미 좁게 설계해둔 책임 범위 때문에 이중화 없이도 감내할 수 있다"는 인과관계로 보는 것이 더 정확함
  • 토큰 탈취 감지 시 전체 세션 로그아웃을 새로 정의한 이유

    • 17번에서 RTR로 "탈취 감지"까지는 이미 설계했지만, 감지 이후에 실제로 무엇을 할지는 비어 있었음
    • 탈취가 의심되는 순간 해당 토큰 계열만 무효화하고 넘어가면, 공격자가 이미 발급받은 다른 Access Token(만료 전)으로는 여전히 활동할 수 있는 창이 남음 — 그래서 감지 즉시 그 유저의 모든 활성 세션을 로그아웃 처리해 그 창을 없애는 쪽으로 정의함
  • 앱서버 인스턴스 복구를 ASG 교체 방식으로 정리한 이유(EC2 Auto Recovery는 제외)

    • AWS에는 장애 인스턴스를 복구하는 방식이 두 가지 있음 — ① ASG 헬스체크 기반 교체(비정상 인스턴스를 종료하고 새 인스턴스를 띄우는 "폐기 후 재생성" 방식), ② EC2 Auto Recovery(CloudWatch의 System Status Check(기반 하드웨어 레벨 장애 감지) 알람에 Recover 액션을 걸어, 같은 인스턴스를 다른 물리 호스트로 옮기거나 재부팅해 인스턴스 ID·IP·로컬 디스크 상태를 그대로 유지하는 방식)
    • 이 서비스는 17번에서 이미 Stateless로 설계해 인스턴스 로컬 상태를 보존할 이유가 없고, ASG 헬스체크는 하드웨어 장애뿐 아니라 애플리케이션 프로세스 장애까지 감지 범위가 더 넓어, 이미 구성된 ASG 교체 방식만으로 충분하다고 판단해 EC2 Auto Recovery는 별도로 추가하지 않음
  • 컨테이너별 memory·memoryReservation 설정을 계획한 이유

    • 18번에서 ECS 컴퓨트 방식을 EC2 launch type으로 결정했는데, Fargate와 달리 EC2 launch type은 하나의 EC2 인스턴스 위에 여러 Task가 함께 배치되어 호스트의 CPU·메모리를 공유함. 컨테이너별 limit·request 설정이 없으면 한 Task의 메모리 누수나 CPU 스파이크가 같은 호스트의 다른 Task까지 자원 부족으로 몰아넣을 수 있음(Noisy Neighbor)
    • memory(하드 리밋)를 걸어두면 문제가 생긴 컨테이너만 OOM Kill되어 자기 자신 안에서 장애가 끝나고, memoryReservation(소프트 리밋)으로 스케줄링 시 필요한 자원을 미리 예약해 Task 배치 단계에서도 과밀 배치를 방지함

근거

  • MHA·Orchestrator의 실제 동작 방식 차이 — MHA는 Source 서버의 상태만 확인해 장애를 판단하는 구조라 "Source 자체가 다운된 상황"과 "관찰자가 네트워크 문제로 Source에 접근하지 못하는 상황"을 구분하지 못함(Replica 상태는 애초에 모니터링 대상이 아님). 소스가 10년 넘게 버전 업데이트·패치 없이 방치돼 있고 Perl로 작성돼 있어, 문제가 생겨도 팀이 직접 코드를 고쳐 대응하기 어려움 — 다먹자처럼 DB 전담 인력 없이 인프라팀이 직접 운영하는 규모에서는 이 유지보수 부담이 실제 병목이 될 수 있음
    • Orchestrator는 Source뿐 아니라 여러 Replica의 복제 상태까지 함께 확인해 장애를 판단하고, Raft 합의 알고리즘으로 Orchestrator 자체를 클러스터로 구성하면(최소 3대, 홀수 권장) 과반수(쿼럼) 동의가 있어야 장애를 확정함 — 네트워크 단절로 일부 노드만 연결이 끊긴 상황과 실제 장애를 구분할 수 있어, 단일 관찰자 방식(MHA, 자체 헬스체크)보다 split-brain 위험이 낮음. 이는 위 이유에서 정리한 "여러 노드의 관점을 종합해 오판을 줄인다"는 설명이 실제로 구현되는 방식
    • failover 소요 시간은 MySQL의 slave_net_timeout 같은 시스템 변수 설정에 크게 좌우됨 — 이 값을 서비스 특성에 맞게 튜닝하면 Source가 실제로 죽은 경우뿐 아니라 네트워크만 끊긴 경우의 failover도 수 초~수십 초 내로 단축할 수 있음이 실제 대규모 운영 환경에서 확인됨 — 다먹자도 Orchestrator 도입 시 이 값을 함께 검토해야 함
    • 이 정보의 출처는 다수의 MySQL을 대규모로 운영하는 환경(카카오 기술 블로그, "MySQL Orchestrator 기반의 새로운 HA 표준 개발기")이라, Orchestrator 자체의 HA까지 확보하는 Raft 클러스터(최소 3대) 구성이나 사내 시스템 연동을 위한 자체 개발까지는 다먹자 규모에 그대로 적용할 필요는 없음 — 다먹자에 맞는 구성 수준은 아래 트레이드오프에서 별도로 판단함
  • Orchestrator의 topology discovery 방식 — 공식 문서에 따르면 orchestrator는 등록된 인스턴스를 대상으로 InstancePollSeconds 주기마다 각 서버를 probe하고, 마스터·Replica 관계를 재귀적으로 조회해("interrogate this instance for its master and replicas, recursively moving on until the entire topology is revealed") 전체 토폴로지를 자동으로 발견함 — 새 노드를 개별적으로 등록하지 않아도 기존에 모니터링 중인 클러스터에 연결되면 다음 polling 주기에 자동으로 발견·편입됨(출처: openark/orchestrator 공식 문서, first-steps.md·configuration-discovery-basic.md)

트레이드오프

  • 매트릭스로 정리했다고 실제 복원력이 검증된 것은 아님 — 문서상으로 "해결"이라고 표시한 항목들도 실제 장애 상황에서 설계대로 동작하는지는 별도의 장애 주입 테스트(예: 인스턴스 강제 종료, AZ 격리)로 검증해야 확신할 수 있음 — 이 문서는 설계 의도를 정리한 것이지 검증 결과가 아님
  • Orchestrator 채택에 대한 트레이드오프
    • AZ 전체 장애로 Primary와 Read Replica가 동시에 사라지는 상황은 이번 설계에서 다루지 않음 — Standby가 다른 AZ에 있어 쓰기는 복구되지만, 읽기를 담당하던 Replica가 AZ-a에만 있어 새로 프로비저닝하기 전까지 읽기 경로 공백이 생김. Standby와 같은 AZ(AZ-b)에 Replica를 추가로 두는 방안(체인 복제 시 failover 직후에도 repointing 없이 바로 읽기 가능)은 후보로만 남겨두고 최종 결정은 하지 않음 — 평시 지연 증가(2-hop)와 Standby 상태에 대한 신선도 종속, 인스턴스 추가 비용이라는 트레이드오프가 있어 별도 검토가 필요함
    • 장애 감지·Standby 승격 자체는 Orchestrator가 자동으로 처리하며, 그 이후 애플리케이션이 새 Primary로 연결을 자동으로 바꾸는 부분은 VIP/DNS 방식(Orchestrator의 failover 훅으로 Route53·Elastic IP 갱신)으로 결정함(후보 비교는 위 이유 참고). 다만 DNS를 쓸 경우 TTL 캐싱으로 전환이 완전히 즉시는 아닐 수 있고, 이미 맺어진 커넥션을 앱서버가 스스로 끊고 재연결하는 로직이 없으면 체감 복구가 늦어짐 — 이 재연결 로직은 별도 과제로 남음
    • Orchestrator 자체의 고가용성은 완전한 Raft 쿼럼(최소 3대, 근거 참고)이 다먹자 규모엔 오버스펙이라고 판단해 단일 인스턴스와 Raft 3대 사이의 Semi HA로 결정함(후보 비교는 위 이유 참고). 다만 완전한 쿼럼 기반 판단은 아니라서, 두 노드 간 판단이 갈리는 split 상황에 대한 구체적 동작(어느 쪽을 신뢰할지, 상태를 어떻게 공유할지 등)은 세부 설계가 아직 남아있음
    • ProxySQL은 자동 라우팅과 읽기 분산까지 한 번에 해결하는 가장 완전한 방식이지만, 비용 증가와 상시 관리 컴포넌트 추가 부담 때문에 v2에서는 채택하지 않음 — 읽기 트래픽이 늘어 읽기 분산 필요성이 커지면 v3에서 재검토 대상으로 남겨둠
    • Go 언어 기반 오픈소스라 MHA(Perl) 대비 유지보수 부담은 낮지만, 팀 입장에서는 신규 도구 학습·운영 경험을 새로 쌓아야 하는 부담은 그대로 있음
    • 다만 자동 편입은 "모니터링 대상에 새 노드가 포함된다"는 의미일 뿐, 새 Replica로 읽기 트래픽을 실제로 분산시키는 라우팅(ProxySQL 등 클라이언트 리다이렉션) 설정이나 초기 동기화 완료 여부 확인까지 자동으로 이뤄지는 것은 아니므로, 실제 증설 시에는 이 부분을 별도로 확인·구성해야 함
  • Redis 단일 인스턴스 유지(이중화 안 함)에 대한 트레이드오프
    • Redis 장애 시 로그인 갱신·로그아웃·SSE Pub/Sub 세 기능이 한꺼번에 멈춤 — 지금은 이 영향이 감내 가능한 수준이라고 판단했지만, 21번에서 Redis의 역할이 Refresh Token 저장에서 SSE 브로드캐스트·ACK까지 넓어진 만큼 장애 영향 범위도 이미 넓어졌다는 점은 이 결정에 함께 반영된 사실임 — SSE 활용도가 더 커지면(예: SSE로 처리하는 알림 유형이 늘어나면) 이 판단은 재검토 대상
    • 장애 감지·복구를 위한 별도 대응이 없으므로, Redis가 실제로 죽으면 복구는 팀원이 직접 재기동하는 수동 대응에 의존함 — 이 시간만큼 로그인 갱신·로그아웃·SSE 인앱 갱신 기능이 정지됨
    • 재검토 조건: Redis 장애가 실제로 반복되거나 위 영향이 감내 가능한 수준을 넘어서면, 관리형(B, ElastiCache)보다 자체구축(C, Sentinel/Cluster)으로 재검토함 — 19번에서 이미 DB를 자체구축으로 운영하기로 한 방향과 일관되게, 관리형 비용을 늘리기보다는 팀이 직접 운영 역량을 쌓는 쪽을 우선함
  • 전체 세션 로그아웃 정책의 오탐 비용 — 정상적인 멀티 디바이스 사용 패턴(여러 기기에서 동시 로그인 등)이 탈취로 오판될 경우, 실제 공격이 아닌데도 유저의 모든 기기가 로그아웃되는 불편이 발생할 수 있음 — 오탐률은 실제 운영 데이터가 쌓인 뒤 재검토 필요
  • EC2 Auto Recovery를 채택하지 않아 특정 하드웨어 장애에서는 ASG 교체가 더 느릴 수 있음 — ASG 교체는 새 인스턴스를 처음부터 부팅해야 해서(18번의 AMI 활용으로 시간은 줄였지만) 완전히 새로 뜨는 데 걸리는 시간이 있는 반면, EC2 Auto Recovery는 같은 인스턴스를 재부팅·이전하는 방식이라 이 특정 케이스(기반 하드웨어 장애)에서는 더 빠를 수 있음 — 다만 이 차이가 실제로 문제 될 만큼 큰지는 검증되지 않았고, ASG 교체가 감지 범위(앱 레벨 장애 포함)는 더 넓으므로 현재는 ASG 단독으로도 충분하다고 판단함
  • memory·memoryReservation 수치를 아직 실측 데이터 없이 정해야 함 — 실제 트래픽 기반 부하테스트 이전이라, 값을 너무 타이트하게 잡으면 정상적인 트래픽 스파이크에서도 OOM Kill이 발생해 오히려 가용성을 해치고, 너무 느슨하게 잡으면 Noisy Neighbor 방지 효과가 약해짐 — 정확한 값은 부하테스트 이후 재산정 필요
  • Read Replica 장애 시 폴백을 두지 않아 장애 동안 읽기 성능 저하가 사용자에게 그대로 노출됨 — Primary·Standby 어디로도 우회하지 않기로 했으므로, Read Replica가 복구되거나 대체 인스턴스가 뜨기 전까지는 남은 Replica(있다면)로만 읽기를 처리해야 해 응답 지연이나 일부 요청 실패가 사용자에게 직접 보일 수 있음 — 복구 속도는 Read Replica 자체의 장애조치 방식(미해결)에 달려 있어 이 트레이드오프의 크기는 그 항목이 정해져야 구체화됨
  • 인스턴스 자원 총량과의 연동 관리 부담 — 한 EC2 인스턴스에 배치되는 Task들의 memoryReservation 합이 그 인스턴스의 전체 메모리를 넘지 않아야 하므로(16번 인스턴스 사이징, 20번 min/max capacity와 연동), Task 수를 늘리거나 인스턴스 타입을 바꿀 때마다 값을 재계산해야 하는 운영 부담이 생김

23. CI/CD 변경사항 설계

CI 도구 선택

  • 결정요약: v1의 GitHub Actions를 그대로 유지. GitLab CI/CD, Jenkins를 후보로 검토했으나 채택하지 않음

  • 이유:

    • GitLab이 제공하는 self-hosted 옵션과 통합 DevOps 플랫폼 강점을 다먹자가 실제로 활용하지 않아 제외함 — 강점의 세부 내용은 근거 참고
    • GitLab CI로 이전하려면 폴리레포(BE/FE/AI 3개 저장소) 전체의 파이프라인 재작성과 저장소 자체 이전이 필요한 대규모 작업인데, 위 이유로 이전 실익이 크지 않아 착수하지 않음
    • Jenkins는 저장소 이전 없이 웹훅 연동만으로 붙일 수 있어 이전 범위는 좁지만, Jenkins 서버 자체를 상시 운영해야 하는 부담이 새로 생김 — v1 CD 설계에서도 self-hosted GitHub Actions Runner 도입을 검토했다가 서버를 상시 운영해야 하는 같은 부담 때문에 배제한 적이 있어, 이번 Jenkins 배제도 그때와 같은 기준(상시 운영 부담)을 적용한 결과임
  • 근거:

    • GitLab이 GitHub 대비 내세우는 DevOps 강점은 크게 네 가지:
      • 무료 self-hosting: GitLab CE(Community Edition)는 무료로 자체 서버에 설치해 운영 가능. GitHub은 self-hosted(Enterprise Server)가 유료 라이선스 필요
      • 통합 CI/CD 플랫폼: 기획빌드배포~모니터링까지 하나의 플랫폼에 내장돼 있어 외부 도구를 조합하지 않고도 파이프라인을 완결할 수 있음
      • Kubernetes 통합: In-cluster Agent와 Flux CD 연동으로 GitOps 방식의 클러스터 배포·관리를 지원
      • 내장 컨테이너 레지스트리: CI/CD와 바로 연동되는 자체 Docker 레지스트리를 제공
      • 보안·컴플라이언스 스캔: SAST(정적 코드 분석)와 Secret Detection(비밀 정보 탐지)은 전 등급 무료, DAST(런타임 취약점 탐지)와 심화 Dependency Scanning(SBOM 생성)은 Ultimate 유료 등급에서 제공하며, 커스텀 컴플라이언스 프레임워크 관리도 지원
    • 다먹자에 대입하면 이 강점들이 대부분 해당사항이 없음: self-hosting을 하지 않기로 했으니 무료 self-hosting은 의미가 없고, 18번에서 컴퓨트를 Kubernetes가 아닌 ECS로 결정해 Kubernetes 통합도 쓸 일이 없으며, 이미지 레지스트리도 이미 GHCR을 쓰고 있어 내장 레지스트리 역시 불필요함
    • 남는 건 보안 스캔인데, 그중 무료 등급(SAST·Secret Detection)에 해당하는 기능을 GitHub도 Public 저장소에서 CodeQL(SAST 동급)·Secret Scanning+Push Protection·Dependabot(SCA 동급)으로 동일 수준 무료 제공 중 — v1에서 GHCR·저장소를 Public으로 유지한 결정과 맞물려, GitLab 무료 기능 대부분이 이미 GitHub에도 있는 셈. GitLab에만 있는 진짜 차별 기능(DAST, 심화 Dependency Scanning/SBOM)은 Ultimate 유료 등급으로 한정돼, 지금 단계에서 그 비용을 감수할 필요성이 크지 않다고 판단함
    • GitHub Actions → GitLab CI·Jenkins 이전 작업 범위 — GitLab CI 이전 시 jobs→stages, env→variables 등 문법 자체가 달라 파이프라인 재작성이 필요하고 저장소도 함께 옮겨야 하며, Jenkins는 저장소는 그대로 두고 Jenkinsfile 작성 + Jenkins 서버(EC2 등) 설치 + Docker 권한 설정 + 자격증명 이관이 필요함
  • 트레이드오프:

    • 이번 결정은 self-hosting을 하지 않는 v2 범위에 한정된 판단 — 추후 온프레미스 전환(v3 후보)을 검토할 때는 Jenkins, self-hosted GitLab이 다시 후보로 재검토될 수 있음
    • GitLab Ultimate의 DAST·심화 SCA 같은 기능은 지금은 포기하는 셈이라, 런타임 취약점 탐지가 필요해지는 시점이 오면 별도로 재검토 필요
  • 출처:

CI 빌드 환경(컴퓨팅 스펙) 결정

  • 결정요약: CI 빌드는 GitHub Actions의 Public 저장소 기본 제공 러너(4 vCPU/16GB RAM, 무료·무제한)를 그대로 사용 — AWS CodeBuild 같은 유료 빌드 환경으로의 전환은 지금 도입하지 않고, 빌드 시간이 10분을 넘거나 문제가 발생하는 시점에 재검토하기로 함

  • 이유:

    • CI 도구 자체는 바로 위 "CI 도구 선택"에서 이미 GitHub Actions로 채택돼 있고, 이번 결정은 그 GitHub Actions를 어떤 컴퓨팅 사양으로 돌릴지에 대한 하위 결정임
    • 바로 아래 "저장소 공개 범위"에서 BE/FE/AI 폴리레포 3개를 Public으로 유지하기로 했는데, Public 저장소에서는 GitHub Actions 표준 러너가 4 vCPU/16GB RAM 사양으로 무료·무제한 제공됨 — 비교 검토한 AWS CodeBuild 유료 인스턴스와 비교해도 스펙상 밀리지 않아 추가 비용을 들여 전환할 유인이 없음
    • 지금 시점에 정확한 빌드 소요 시간을 예측할 실사용 데이터가 부족해 최적 스펙을 미리 정하기보다, 일단 무료 스펙으로 운영하며 실측치를 쌓고 문제가 생기면 그때 대안을 검토하는 쪽을 택함
  • 근거:

    • GitHub Actions Public 저장소 표준 Linux 러너(ubuntu-latest) 사양: 4 vCPU / 16GB RAM / 14GB SSD, 무료·무제한(2024년 1월 2 vCPU에서 상향됨)
    • AWS CodeBuild 온디맨드 EC2 컴퓨팅 유형(서울 리전, 월 100회·평균 빌드 3분 가정 시 300 청구 분) 비교 견적: general1.medium(4 vCPU/7GB, $0.010/분) 월 약 3달러, general1.large(8 vCPU/15GB, $0.020/분) 월 약 6달러 — vCPU를 GitHub Actions와 동일하게 맞추면(general1.medium) 메모리가 절반 이하(7GB)이고, 메모리를 비슷하게 맞추면(general1.large) vCPU가 2배(8)인데도 비용이 발생함
    • 빌드 횟수(월 100회·평균 3분)는 런타임 트래픽 규모(MAU·RPS)가 아니라 개발 활동량(PR·커밋 빈도)에 좌우되는 지표라, 15번에서 정의한 서비스 성장 가정(MAU 약 1,150배)과는 무관하게 현재 실측치 기준으로 판단함
  • 트레이드오프:

    • "일단 무료 스펙으로 쓰다가 임계치(빌드 10분 초과 또는 문제 발생) 도달 시 재검토"하는 방식이라, 그 임계치에 도달하기 전까지는 최적 스펙인지 스스로 확신할 근거는 부족한 상태로 운영됨 — 다만 이 문서 전반에서 써온 "일단 결정하고 필요 시 재검토" 접근과 일관됨
    • 이번 결정은 바로 아래 "저장소 공개 범위" 결정(Public 유지)에 의존함 — Private 저장소였다면 Actions 러너가 2 vCPU/8GB로 다운그레이드되고 분당 과금까지 붙어 이번 비교 자체가 성립하지 않았을 것이므로, 추후 저장소 공개 범위 결정이 바뀌면 이 결정도 함께 재검토해야 함
  • 출처:

저장소 공개 범위

  • 결정요약: BE/FE/AI 폴리레포 3개 전체가 v1의 Public 상태를 그대로 유지 — Private 전환을 검토·결정했다가 재검토 끝에 폐기(검토 경위와 원래 내용은 25번 폐기된 의사결정 참고)

  • 이유:

    • 팀이 속한 GitHub 조직이 Team 플랜을 쓰고 있는데, Actions 실행 분(minutes)의 무료 한도(Team 기준 월 3,000분)는 저장소 하나하나가 아니라 조직 전체에 걸쳐 하나로 풀링되는 구조임을 확인함 — Private 저장소를 하나라도 추가하면 그 사용량이 조직 전체가 공유하는 한도를 함께 소모하게 됨
    • 이 한도 자체를 조정·확인하려면 조직 승인 주체(강사)의 승인이 필요한 구조인데, 이 프로젝트는 실제 사업체가 아니라 KTB 교육과정 팀 프로젝트라 Private 전환의 원래 목적이었던 "회사 비즈니스 로직 보호"라는 전제가 상대적으로 약함 — 그 승인 절차를 거칠 실익이 크지 않다고 판단해 재검토 끝에 Public 유지로 재확정함
  • 근거:

    • GitHub Actions 무료 분(minutes) 한도는 계정·조직이 소유한 모든 Private 저장소의 사용량을 합산해 하나의 월 한도로 관리됨(공식 문서: "Minutes usage is charged to the repository owner") — 저장소별로 별도 한도가 주어지는 구조가 아니라, BE/FE/AI 3개를 Private으로 전환하면 셋의 사용량이 조직의 한도 하나를 함께 소모하게 됨
    • 실측 사용량(3주 약 430분·저장소 1개 기준, 월 환산 약 620분)만 놓고 보면 Team 플랜의 월 3,000분 한도 안에서 여유가 있는 편이었지만, 이 한도가 조직 단위로 공유되고 조직 승인 없이는 팀원이 직접 확인·조정할 수 없는 구조라는 점이 실질적인 걸림돌로 확인됨
    • v1에서 Public을 채택했던 근거(민감 데이터 없음, GHCR 500MB 제한 회피)는 이번 재검토에서도 여전히 유효한 것으로 확인됨
  • 트레이드오프:

    • Public 유지로 코드가 계속 공개 상태에 남아, Private 전환으로 얻으려 했던 코드 경쟁력·IP 노출 방지 효과는 포기함 — 다만 실제 사업체가 아닌 교육과정 팀 프로젝트라는 성격을 고려하면 감내 가능한 수준으로 판단
    • 추후 프로젝트가 실제 서비스로 이어지거나 조직 승인 절차 부담이 줄어드는 시점이 오면 Private 전환을 다시 검토할 수 있음 — 검토했던 내용은 삭제하지 않고 25번 폐기된 의사결정에 그대로 보존해 재검토 시 참고할 수 있도록 함

CD(배포 실행) 방식

  • 결정요약: GitHub Actions가 직접 ECS Native Blue/Green(CodeDeploy 없이 ECS 자체가 제공하는 Blue/Green 배포 전략)을 호출해 배포 — CodeDeploy Blue/Green, 직접 스크립트(ecs update-service), EventBridge+Lambda 간접화, CodePipeline+CodeBuild를 후보로 검토했으나 채택하지 않음

  • 이유:

    • 배포 대상은 18번에서 이미 ECS(EC2 launch type)로 결정돼 있고, 이번 결정은 그 위에서 실제로 어떻게 배포를 실행할지에 대한 하위 결정임
    • v1의 배포 검증은 Health Check 단일 기준이라 특정 API만 깨진 경우는 감지하지 못하는 한계가 있었음(ktb4-cloud-wiki-cd.md 13번) — v2에서는 이 한계를 넘어서는 배포 검증·자동 롤백 체계를 원해, ECS 자체 rolling 배포(직접 스크립트)보다 트래픽을 점진 전환하고 실패 시 자동 롤백하는 Blue/Green 계열을 우선 검토함
    • Blue/Green을 구현하는 두 갈래(CodeDeploy vs ECS Native Blue/Green) 중에서는 ECS Native를 선택함 — CodeDeploy는 Application·Deployment Group·전용 IAM 서비스 역할이라는 별도 리소스를 새로 만들어 관리해야 하는 반면, ECS Native Blue/Green은 ECS 서비스 정의(deploymentConfiguration) 안에서 끝나 관리 컴포넌트가 더 적음
    • ECS Native Blue/Green은 2025년 7월 출시돼 2019년부터 있었던 CodeDeploy만큼 오래 검증된 기능은 아니지만, 2026년 5월 Linear·Canary 전략까지 추가되며 트래픽 전환 세분화 기능에서의 격차도 해소됨
    • 18번에서 v2 아키텍처에 이미 ALB가 포함돼 있어(ECS EC2 launch type + ASG + ALB), Blue/Green에 필요한 타겟 그룹 2개 추가는 기존 ALB에 얹는 정도라 추가 인프라 비용 부담이 크지 않음
    • 트리거는 EventBridge를 거치지 않고 GitHub Actions가 직접 호출하는 방식을 택함 — 트리거 소스가 GitHub Actions 하나뿐인 지금 규모에서는 이벤트 버스·Lambda라는 컴포넌트를 추가로 두는 비용 대비 실익이 크지 않다고 판단함
    • CodeDeploy·ECS Native 모두 Task Definition/Task Set 레벨에서 동작해 이미지가 어느 레지스트리에서 오는지와 무관하므로, 23번에서 이미 결정한 GHCR 사용(CI)과 충돌 없이 그대로 맞물림
  • 근거:

    • AWS CodeDeploy는 Amazon EC2·ECS·Lambda로의 배포에는 추가 요금이 없고, on-premises 인스턴스 업데이트에만 건당 $0.02가 부과됨 — ECS Native Blue/Green도 ECS 오케스트레이션 자체에 별도 요금이 없어, 두 방식 모두 비용보다 관리 컴포넌트 수·성숙도 차이로 구분됨
    • ECS Native Blue/Green은 deploymentController: ECS + deploymentConfiguration.strategy: BLUE_GREEN 설정만으로 동작하며, ALB 타겟 그룹 2개(Blue/Green)와 Lambda 기반 Lifecycle Hook을 CodeDeploy와 유사하게 지원함 — Fargate뿐 아니라 EC2 launch type도 지원함("start with an Amazon ECS service (Fargate or EC2)", AWS DevOps Blog)
    • 롤백은 새 Task를 다시 배포하는 대신 Bake 기간 동안 Blue/Green이 동시에 떠 있는 상태에서 트래픽 가중치를 되돌리는 방식이라 사실상 즉시 롤백되며, CloudWatch 알람과 연동한 자동 롤백도 지원함
  • 트레이드오프:

    • ECS Native Blue/Green은 2025년 7월 출시된 비교적 최근 기능이라, CodeDeploy만큼 축적된 커뮤니티 자료·트러블슈팅 사례가 아직 적음 — 예상치 못한 이슈 발생 시 참고할 자료가 상대적으로 부족할 수 있음
    • CodeDeploy는 ECS 외에 EC2·Lambda 등 다른 컴퓨트에도 범용으로 쓸 수 있는 반면, ECS Native Blue/Green은 ECS 서비스 전용이라 향후 배포 대상이 ECS 외로 확장되면 별도 방식이 필요함 — 다만 지금은 배포 대상이 ECS 하나뿐이라 이 제약이 실질적인 영향을 주지 않음
    • EventBridge를 거치지 않고 GitHub Actions가 직접 호출하는 구조라, 트리거 소스와 배포 실행 로직이 GitHub Actions 워크플로우 파일에 함께 묶여 있음 — 추후 다른 트리거 소스가 생기거나 배포 로직을 독립적으로 재사용해야 하는 상황이 오면 EventBridge 간접화를 재검토할 수 있음
  • 출처:


25. 폐기된 의사결정

한 번 결정까지 마쳤다가 이후 상황이 바뀌어 최종적으로 폐기한 항목을 모아두는 곳. 삭제하지 않고 폐기 사유와 함께 원래 내용을 그대로 보존함 — 관련 상황이 다시 바뀌면 재검토 시 참고.

저장소 공개 범위(Public→Private) 전환 — 폐기됨

폐기 사유: 팀이 속한 GitHub 조직이 Team 플랜을 쓰고 있어 Actions 실행 분(minutes)의 무료 한도가 저장소별이 아니라 조직 전체에 하나로 풀링되는 구조임을 확인함 — Private 전환 시 이 한도를 공유하게 돼 조직 승인 주체(강사)의 승인이 필요했음. 이 프로젝트가 실제 사업체가 아니라 KTB 교육과정 팀 프로젝트라는 점에서 애초 Private 전환의 목적(회사 비즈니스 로직 보호)이 상대적으로 약했던 것과 맞물려, 승인 절차를 거칠 실익이 크지 않다고 판단해 재검토 끝에 Public 유지로 최종 결정함(최종 결정은 23번 "저장소 공개 범위" 참고). 아래는 Private 전환을 결정했던 시점에 작성된 원래 내용을 그대로 보존한 것.

  • 결정요약: v1에서 Public으로 유지했던 BE/FE/AI 폴리레포 3개 전체를 v2에서 Private으로 전환

  • 이유:

    • 회사 비즈니스 로직(BE 로직, AI 추천 알고리즘 등)이 공개돼 있는 것 자체가 적절하지 않다고 판단 — v1에서 Public을 택한 근거였던 "민감 데이터 없음"과는 별개로, 코드 자체의 경쟁력·IP 보호 문제로 접근
    • 통일성을 위해 BE·AI뿐 아니라 FE를 포함한 3개 저장소 전체를 Private으로 전환
  • 근거:

    • Public 저장소가 v1에서 가져다주던 혜택 세 가지가 Private 전환으로 약화되거나 사라짐:
      • GitHub Actions 무제한 실행 시간: Public 저장소는 GitHub-hosted 러너 사용이 완전 무료·무제한이나, Private 전환 시 플랜별 월 한도가 적용됨(GitHub Free 월 2,000분, Team 월 3,000분, 초과분은 분당 과금). 다만 러너 자체의 하드웨어 성능(CPU·RAM·디스크)이나 동시 실행 가능 Job 수(concurrency)는 저장소 공개 여부가 아니라 계정·조직의 플랜으로 결정되므로 이 부분은 변하지 않음 — "느려지는 것"이 아니라 "월 사용 가능 총량에 상한이 생기는 것"
      • GHCR 저장·전송 비용: 공식 요금표상 Private 패키지는 무료 한도 초과 시 저장 0.25$/GB·전송 0.50$/GB가 과금되나, 실제로는 컨테이너 이미지에 한해 지금까지 이 과금이 시행되지 않고 있음(GitHub이 "30일 사전 고지 후 시작"이라 해둔 채 유예 중) — 당장 청구되지는 않지만 문서상 남아있는 리스크
      • GitHub 보안 스캔 무료 제공: CodeQL(Code Security, 활성 커미터당 월 30$)과 Secret Protection(활성 커미터당 월 19$)이 Public에서는 무료였으나 Private에서는 GitHub Advanced Security 유료 애드온으로 전환됨. 다만 Dependabot 알림(의존성 취약점 탐지)은 Public·Private 무관하게 전 플랜 무료로 유지됨
    • 위 세 번째 항목은 23번의 CI 도구 선택에서 "GitHub이 Public 저장소에서 GitLab 무료 보안 기능과 동급을 제공한다"고 정리했던 근거를 약화시킴 — GitLab의 무료 SAST·Secret Detection은 저장소 공개 여부와 무관하게 제공되므로, Private 전환 이후에는 GitHub의 해당 무료 이점이 사라지는 반면 GitLab은 그대로 유지됨. 다만 CI 도구를 GitHub Actions로 유지하기로 한 결정 자체(self-hosting 미사용, Kubernetes·레지스트리 강점 무관)는 이 전환과 무관하게 별도로 유효함
  • 트레이드오프:

    • 현재 파이프라인 사용량이 Private 전환 후 월 한도(Free 2,000분/Team 3,000분)에 근접하면 추가 과금 또는 실행 대기가 발생할 수 있어 사용량 모니터링이 필요함
    • 회사 로직 보호라는 이득과, GitHub Actions·GHCR·보안 스캔에서 발생할 수 있는 잠재적 추가 비용·관리 부담을 맞바꾸는 결정
    • 향후 보안 스캔(SAST·Secret Detection)을 무료로 강화하고 싶어지는 시점이 오면, Private 환경에서도 동일 기능을 무료 제공하는 GitLab이 다시 후보로 검토될 수 있음
  • 출처: