본문
트래픽이 요동쳐도 사용자 경험은 그대로: MSA 환경별 세션 유실 방지 전략
MSA 기반 서비스 운영 경험이 쌓이면서 뼈저리게 느낀 점은, 서비스의 유연함을 위해 도입한 Auto Scaling이 자칫하면 사용자 경험을 해치는 양날의 검이 될 수 있다는 사실입니다. 특히 트랜잭션의 연속성이 중요한 금융 서비스나 커머스 분야에서 스케일 인/아웃 도중 사용자 세션이 끊기는 문제는 치명적입니다.
단순히 서버를 늘리고 줄이는 것을 넘어, 그 과정에서 어떻게 사용자의 연결을 안전하게 보호할 수 있을까요?
다양한 인프라 환경에서 치열하게 고민하고 적용했던 세션 유실 방지 전략을 정리해 보았습니다.
핵심 원칙: 시스템의 유연성을 위한 Stateless 구조 설계
어떤 화려한 플랫폼을 사용하든, 세션 유실을 막는 가장 확실하고 근본적인 해결책은 애플리케이션을 무상태(Stateless)로 만드는 것입니다. 특정 인스턴스가 자체 메모리에 세션을 저장하는 구조에서는 어떤 방법을 써도 스케일 인 시점의 유실 위험을 100% 막기 어렵습니다.
따라서 이후 설명할 환경 전반에 걸쳐, 세션 데이터를 Redis(ElastiCache)와 같은 외부 고성능 저장소로 분리하여 애플리케이션의 독립성을 확보하는 것을 최우선적으로 고려할 필요가 있습니다 (참고: 만약 Spring Boot 기반의 애플리케이션이라면 Spring Session 등을 활용하여 이를 쉽게 구현할 수 있습니다.)
이 대전제를 바탕으로 각 환경별 특징을 살펴보겠습니다.
💡 Stateless(스테이트리스, 무상태)란?
서버가 이전 대화 내용을 기억하지 못하는 '단기 기억' 상태와 같습니다.
상세 설명:
- 요청이 들어올 때마다 서버는 새로운 사용자를 대하듯 동작합니다.
- 상태 정보를 서버 외부(클라이언트나 DB)에 두어, 서버 자체는 가볍고 단순한 상태를 유지합니다.
1. AWS ECS (Fargate/EC2) + ALB 환경
AWS 관리형 환경에서도 예외는 없습니다. Task 컨테이너는 언제든 사라질 수 있다는 가정하에 설계해야 합니다.
- [우선순위 1] Redis 기반 상태 분리: 가장 먼저 해야 할 일은 모든 ECS Task가 Redis 같은 외부 저장소를 바라보게 하여, 어떤 Task가 죽더라도 데이터가 유실되지 않게 만드는 것입니다. 이게 되면 사실상 아래의 ALB 기능은 부차적인 것이 됩니다.
- [우선순위 2] ALB Stickiness (차선책): 만약 로컬 인메모리 캐시 사용 등 특수한 성능 이슈로 인해 어쩔 수 없이 요청을 특정 인스턴스로 고정해야 한다면, 차선책으로 ALB Target Group의 Stickiness 기능을 활성화합니다. ALB가 자체 쿠키(AWSALB)를 이용해 클라이언트를 특정 Task에 묶어줍니다.
- 필수 운영 설정 (Connection Draining): 스케일 인으로 Task가 종료될 때 안전장치로 Deregistration Delay를 반드시 설정해야 합니다. Task 상태가 'Draining'이 되면 ALB는 새 요청을 막고, 설정된 시간(예: 300초) 동안 기존 연결이 마무리될 때까지 기다려줍니다.

💡 Connection Draining(연결 배수)이란?
연결 배수 = Connection Draining → 기존에 맺어져 있던 연결(커넥션)을 천천히 빼내는(Drain) 과정
로드밸런서 뒤에 있는 서버(EC2, ECS Task 등)가 종료되거나 스케일 인(scale-in)될 때 → 갑자기 연결을 끊어버리면 사용자가 보는 화면이 에러 나거나 요청이 중간에 끊깁니다.
그래서 AWS는 이런 상황을 방지하기 위해 아래처럼 동작합니다:
- 서버가 unhealthy 판정되거나 deregister(등록 해제) 시작
- 로드밸런서는 새로운 요청은 그 서버로 보내지 않음 - 이미 연결되어 진행 중이던 요청(in-flight requests)은
→ 설정한 시간(예: 300초 = 5분) 동안 기다려줌
- 그 시간 안에 자연스럽게 연결이 끝나면 좋고,
→ 시간이 다 되면 강제로 끊음
이 과정을 "연결을 배수한다" 또는 "드레인한다"라고 표현합니다.
(참고: ALB/NLB에서는 주로 "등록 취소 지연(Deregistration Delay)"이라고도 부름)
2. Kubernetes (K8s) / EKS 환경
Pod가 수시로 뜨고 지는 역동적인 K8s 환경에서는 Stateless 구조가 선택이 아닌 필수입니다.
- [우선순위 1] Redis 기반 상태 분리: K8s에서도 제1원칙은 변함없습니다. Redis를 도입해 Pod가 세션 데이터를 들고 있지 않게 만들어야 합니다. 이렇게 하면 굳이 복잡한 Ingress 설정으로 트래픽을 특정 Pod에 고정할 필요 자체가 사라집니다.
- [우선순위 2] Ingress Cookie Affinity (보완책): 불가피하게 특정 Pod로 연결을 유지해야 한다면 Nginx Ingress 등의 어노테이션을 활용해 Cookie Affinity를 설정할 수 있습니다.
- 주의: 이 방식은 특정 Pod에 트래픽이 몰리는 불균형 문제를 야기할 수 있습니다. HPA 설정을 민감하게 하거나 세션 타임아웃을 짧게 가져가는 식으로 완화할 순 있지만, 결국 가장 확실한 해결책은 [우선순위 1]인 Stateless 구조로 전환하여 굳이 특정 Pod에 고정될 필요를 없애는 것입니다.
- 필수 운영 설정 (Pod Graceful Shutdown): Pod 종료 시 애플리케이션이 SIGTERM 신호를 받고 하던 작업을 안전하게 마무리하도록 PreStop Hook과 terminationGracePeriodSeconds를 설정하는 것이 매우 중요합니다.

3. Spring Cloud (Legacy/VM) 환경
VM 기반의 전통적인 아키텍처나 초기 MSA 모델에서도 핵심은 동일합니다.
- [우선순위 1] Redis 기반 Global Session Store: 과거의 Tomcat 세션 클러스터링 방식은 성능 문제로 지양해야 합니다. VM 환경에서도 표준은 spring-session-data-redis 같은 라이브러리를 활용해 모든 VM 인스턴스가 중앙 Redis를 바라보게 구성하는 것입니다.
- Gateway 레벨 제어 (보조): 앞단에 Spring Cloud Gateway나 Zuul이 있다면, 세션 ID 기반 해싱 필터로 특정 사용자를 특정 VM으로 라우팅할 수 있지만, 어디까지나 보조적인 수단일 뿐입니다.

마무리하며
결국 어떤 환경이든 핵심은 "세션 데이터를 애플리케이션 인스턴스에서 분리하는 것(Stateless)" 그리고 "인스턴스가 사라질 때 기존 작업을 안전하게 마무리할 시간을 주는 것(Graceful Shutdown)"으로 귀결됩니다.
이 두 가지 원칙만 확실히 지킨다면, 트래픽이 아무리 요동쳐도 사용자는 끊김 없는 서비스를 경험할 수 있습니다.
댓글