Track 2. AI Infrastructure/Financial AI Arch

회복탄력적인(Resilient) PaaS 설계의 기본과 잘못된 설계 결과

Context Lab 2025. 10. 25. 11:11

Resilient PaaS(Platform as a Service) 설계의 두 가지 핵심 목표는 **'탄력성'****'장애 격리'**입니다.

 

탄력성(Elasticity): 수요에 따라 플랫폼이 자동으로 확장/축소되어, 리소스를 낭비하지 않으면서도 안정적인 서비스를 제공하는 능력입니다.

 

장애 격리(Fault Isolation): 플랫폼의 한 구성 요소나, 그 위에서 실행되는 특정 애플리케이션의 장애가 다른 애플리케이션이나 플랫폼 전체로 전파(Cascading Failure)되는 것을 막는 능력입니다.

 

이 두 가지 목표를 달성하기 위한 7가지 설계 지침을 정리해 봅니다.

 

공통 기반 (The Foundation)

탄력성과 장애 격리 모두를 위한 가장 기본적인 전제 조건입니다.

 

1. 컨테이너 및 쿠버네티스(K8s) 기반 표준화

현대적인 PaaS는 사실상 쿠버네티스(K8s) 플랫폼을 의미합니다. K8s는 그 자체가 탄력성과 장애 격리를 위해 설계된 도구의 집합체입니다.

  • 탄력성: HPA(Horizontal Pod Autoscaler)를 통해 애플리케이션(Pod)을 자동으로 확장/축소합니다.
  • 장애 격리: Namespace로 논리적 격리를, Cgroup을 통해 프로세스(컨테이너)의 리소스 사용을 물리적으로 격리합니다.

 

2. 상태 비저장(Stateless) 아키텍처 지향

PaaS에서 실행되는 애플리케이션은 상태 비저장(Stateless)을 원칙으로 설계해야 합니다.

  • 탄력성 기여: 애플리케이션 내부에 세션이나 임시 파일 같은 '상태(State)'가 없어야, 수요에 따라 인스턴스(Pod)를 자유롭게 늘리거나 줄일 수 있습니다. 상태는 Redis 같은 외부 캐시나 DB에 저장해야 합니다.
  • 장애 격리 기여: Pod가 장애로 죽더라도, 상태 정보가 외부에 있으므로 K8s가 즉시 다른 노드에 새 Pod를 띄워도 서비스가 중단 없이 이어집니다.

 

탄력성(Elasticity)을 위한 설계

수요의 급격한 변화에 PaaS가 어떻게 대응하는지에 대한 지침입니다.

 

3. '플랫폼'''의 이중 자동 확장 (Dual Auto-scaling)

탄력성은 ''과 그 앱이 실행되는 '인프라' 두 계층 모두에서 일어나야 합니다.

  • 앱 확장 (HPA): Horizontal Pod Autoscaler를 사용해, CPU나 메모리 사용량 같은 메트릭에 따라 앱(Pod)의 개수를 1개에서 10개로 자동으로 늘립니다.
  • 플랫폼 확장 (CA): Cluster Autoscaler를 사용해, Pod가 늘어나 K8s 워커 노드(VM/서버)가 부족해지면, PaaS가 스스로 OpenStack/AWS 등에 요청하여 워커 노드(VM) 자체를 2대에서 5대로 늘립니다. 수요가 줄면 반대로 노드를 반납합니다.

 

4. 비동기식 처리와 '버퍼' 도입 (Decoupling)

모든 요청을 동기식(Synchronous)으로 처리하면, 순간적인 트래픽 폭증(Spike)PaaS 전체를 마비시킬 수 있습니다.

  • '충격 흡수 장치' 설계: KafkaRabbitMQ 같은 **메시지 큐(Message Queue)**PaaS의 기본 서비스로 제공해야 합니다.

탄력성 기여: 요청이 폭주하면, 일단 큐에 모든 요청을 '쌓아두고' 즉시 사용자에게 "처리 중"이라고 응답합니다. 뒷단의 워커(Consumer) 애플리케이션들은 큐에 쌓인 작업을 감당할 수 있는 만큼만 가져가 처리하며, HPA를 통해 천천히 확장됩니다. 이는 시스템 전체의 안정성을 극대화합니다.

 

장애 격리(Fault Isolation)를 위한 설계

"한 놈이 죽어도, 다른 놈은 살아야 한다"는 원칙입니다.

 

5. 엄격한 다중 테넌시(Multi-Tenancy) 및 자원 격리

PaaS에는 수십, 수백 개의 서로 다른 팀(Tenant)의 앱이 공존합니다. 한 팀의 잘못된 코드가 다른 팀의 앱을 죽이는 '시끄러운 이웃(Noisy Neighbor)' 문제를 반드시 막아야 합니다.

  • 논리적 격리 (Namespace): K8sNamespace를 사용해 팀별/서비스별로 논리적인 ''을 분리합니다.
  • 물리적 자원 격리 (Quota): ResourceQuotaLimitRange를 사용해, A팀이 사용할 수 있는 총 CPU와 메모리를 '10 Core, 20GB'**강제로 할당(Hard Limit)**합니다. A팀 앱에 메모리 누수가 발생해도, 할당된 20GB만 차지하고 죽을 뿐, 다른 팀의 앱이나 K8s 시스템 자체에는 영향을 주지 못합니다.

 

6. 제로 트러스트(Zero Trust) 네트워크 격리

"같은 PaaS에 있으니 서로 믿어도 되겠지"라는 생각을 버려야 합니다. 기본값은 **'아무도 믿지 않는다(Zero Trust)'**입니다.

  • K8s NetworkPolicy: Namespace 간의 모든 네트워크 통신을 **기본적으로 '차단(Deny All)'**하는 것을 원칙으로 합니다.
  • 명시적 허용: 'A팀의 앱''B팀의 API'를 호출해야 할 경우에만, NetworkPolicy를 통해 "AB에게 3000번 포트로 접근하는 것만 허용한다"라고 명시적으로 정책을 열어줘야 합니다. 이는 한 앱이 해킹당해도, 해커가 PaaS 내부의 다른 앱으로 이동(Lateral Movement)하는 것을 원천 차단합니다.

 

7. 장애 도메인(Failure Domain) 분산

물리적인 인프라(서버, , 전원)의 장애를 대비해야 합니다.

  • AZ/Rack 분산: PaaS를 구성하는 K8s 워커 노드들을 **서로 다른 가용 영역(Availability Zone, AZ)이나 물리적 랙(Rack)**에 분산 배치해야 합니다.
  • K8s topologySpreadConstraints: K8s 스케줄러 설정을 통해, 특정 앱(e.g., '결제' Pod 3)절대 하나의 노드나 하나의 AZ에 몰리지 않고, 강제로 여러 AZ/랙에 분산되어 배포되도록 설정합니다.
  • 효과: 1AZ 전체가 정전되더라도, 2AZ에서 실행 중인 나머지 Pod들이 즉시 서비스를 이어받아 무중단 운영이 가능해집니다.

 

이 두 가지 목표는 PaaS 설계의 핵심이지만, 실제 구축 시 '편의성'이나 '기존 방식'을 타협하다가 쉽게 놓치게 됩니다.

제가 아키텍트로서 겪은, **'탄력성'****'장애 격리'**를 치명적으로 무너뜨리는 가장 흔한 잘못된 설계(Anti-Pattern) 5가지를 정리하면 다음과 같습니다.

 

 

1. '시끄러운 이웃' 방치 (리소스 격리 실패)

가장 흔하고 치명적인 실수입니다.

  • 잘못된 설계 (현상):

PaaS에 입주한 A팀과 B팀의 애플리케이션이 하나의 K8s 워커 노드(VM)에 함께 배포됩니다. 하지만 아무도 A팀 앱의 '최대 사용량(Resource Limits)'을 설정하지 않았습니다.

  • 결과 (장애 격리 실패):

A팀의 앱에 사소한 메모리 누수(Memory Leak) 버그가 발생합니다. 이 앱은 자신이 실행 중인 노드(VM)의 모든 메모리를 점유할 때까지 폭주합니다. 결국 OS 커널이 메모리 부족으로 해당 노드 전체를 다운시킵니다.

  • 치명타:

A팀의 버그 하나 때문에, 그 노드에서 잘 작동하던 B, C, D팀의 앱 수십 개가 '아무 잘못 없이' 함께 다운됩니다. 이것이 '장애 격리'가 완벽하게 실패한 사례입니다.

 

2. '평평한 네트워크' (네트워크 격리 실패)

"일단 다 통하게" 하려는 안일한 보안 의식의 문제입니다.

  • 잘못된 설계 (현상):

PaaS를 구축할 때, '편의상' 모든 앱(Pod)이 서로 자유롭게 통신할 수 있도록 K8sNetworkPolicy를 설정하지 않고 **네트워크를 완전히 개방(Flat Network)**합니다.

  • 결과 (장애 격리 실패):

공격자가 가장 방어에 취약한 A팀의 '이벤트 경품 응모' 웹사이트(DMZ)를 해킹하는 데 성공합니다.

  • 치명타:

공격자는 이 웹사이트 Pod 안에서 PaaS 내부 네트워크를 스캔합니다. 네트워크 격리가 안 되어 있기 때문에, PaaS 내부망에만 존재해야 할 'B팀의 핵심 계정계 DB' IP가 노출되고 접근까지 허용됩니다. '격리'가 실패하여 단순 해킹이 치명적인 내부 데이터 유출로 이어집니다.

 

3. '상태 저장(Stateful)' 애플리케이션

개발자가 클라우드 네이티브(Cloud-Native)를 전혀 이해하지 못하고, PaaS20년 전 '물리 서버'처럼 사용할 때 발생합니다.

  • 잘못된 설계 (현상):

개발자가 PaaS에 앱을 배포하면서, 사용자 세션 정보나 첨부파일을 자신이 실행 중인 컨테이너(Pod)'로컬 디스크'에 저장하도록 코딩합니다.

  • 결과 (탄력성 실패):

- 확장 불가: 트래픽이 몰려 PaaS가 이 앱을 2대에서 10대로 자동 확장(Scale-out)했습니다. 하지만 사용자가 '로그인'1번 서버가 아닌, '신규' 2번 서버에 다음 요청이 도달하면 "로그인 정보가 없다"며 오류가 발생합니다. (상태가 1번에만 있으므로)

- 회복 불가: 1번 서버(Pod)가 장애로 죽고, PaaS11번 서버를 새로 띄웠습니다. 하지만 1번 서버의 로컬 디스크에 저장된 모든 세션과 첨부파일 데이터가 함께 영구적으로 소실됩니다.

  • 치명타:

'탄력성'의 핵심인 '자유롭게 늘어나고 줄어들며 죽고 살아나는' PaaS의 장점을 전혀 활용하지 못하고, 오히려 데이터 유실이라는 최악의 장애를 맞이합니다.

 

4. '수동 확장' 플랫폼 (플랫폼 탄력성 부재)

(HPA)은 자동인데, 플랫폼(CA)은 수동일 때 발생합니다.

  • 잘못된 설계 (현상):

개발팀의 앱(Pod)에는 HPA(Horizontal Pod Autoscaler)를 설정하여 "CPU 70%가 넘으면 Pod50개까지 늘려라"고 설정했습니다. 하지만 PaaS 플랫폼(K8s) 자체에는 워커 노드(VM)를 자동으로 늘려주는 CA(Cluster Autoscaler)가 설정되어 있지 않습니다.

  • 결과 (탄력성 실패):

이벤트로 트래픽이 폭주하여 HPA가 작동합니다. Pod10개에서 50개로 늘어나려 하지만, 현재 3대의 워커 노드(VM)가 꽉 찼습니다. 나머지 40개의 Pod"배포할 노드가 없습니다(Pending)" 상태로 무한 대기합니다.

  • 치명타:

플랫폼(인프라)이 앱의 탄력성을 받쳐주지 못해 서비스가 마비됩니다. 담당자는 새벽에 전화를 받고 일어나, 수동으로 OpenStack/AWS에 접속해 VM을 생성하고 K8s 클러스터에 추가해야 합니다.

 

5. '동기식 연쇄 호출' (트래픽 관리 부재)

시스템 간의 의존성을 '(Queue)'로 분리(Decoupling)하지 않았을 때 발생합니다.

  • 잘못된 설계 (현상):

'주문' 앱이 요청을 받으면, 반드시 동기식(Synchronous) API'재고' 앱과 '결제' , '배송' 앱을 차례로 호출하고 모든 응답을 받아야만 사용자에게 "주문 완료"라고 응답합니다.

  • 결과 (탄력성 실패):

'결제' (PG사 연동)이 외부 요인으로 5초간 느려졌습니다. 그 결과, '결제' 앱뿐만 아니라 '결제' 앱을 호출한 '주문' 앱까지 모든 스레드(Thread)가 응답 대기 상태에 빠지며 함께 마비됩니다.

  • 치명타:

시스템 전체가 가장 느린 '결제' 앱의 속도에 발목 잡힙니다. 만약 '주문' 앱이 Kafka 같은 큐(Queue)"주문 발생" 이벤트를 던지고 (비동기), 다른 앱들이 알아서 이벤트를 처리하게 했다면 '주문' 앱 자체는 마비되지 않았을 것입니다. 탄력성이란, 이처럼 병목을 분리하는 설계에서 나옵니다.