Graceful Shutdown 문제 해결과 ECS 전환 이야기 (Spring Cloud Netflix + Docker Swarm)

프롤로그: MSA 도입의 출발점
기존 시스템은 모놀리틱 아키텍처로 구성되어 있어 여러 대의 서버를 운영하는 데 따른 복잡성과 느린 배포 속도가 큰 고민거리였습니다. 특히, 코드 수정이 적음에도 전체 시스템을 빌드하고 배포해야 하는 비효율성이 존재했습니다. 이로 인해 기능을 모아서 배포할 수밖에 없었고, 이는 신속한 배포를 저해하는 주요 원인이었습니다.
2021년, 새로운 플랫폼을 구축하면서 MSA(Microservice Architecture) 도입을 결정했습니다.
기술 스택: Spring Cloud Netflix OSS (Eureka, Zuul), Docker
팀 경험: MSA, Docker, 클라우드 네이티브 환경 실무 경험 부족
목표: 안정적인 서비스 디스커버리와 API 게이트웨이 구축
설레는 마음으로 시작했지만 곧 현실의 벽을 마주하게 되었습니다.
Chapter 1: Docker Swarm과의 첫 만남
왜 Docker Swarm을 선택했나?
당시 컨테이너 오케스트레이션 도구로 Docker Swarm을 선택한 이유는 다음과 같았습니다.
Kubernetes는 너무 복잡해 보였음
Docker Swarm은 상대적으로 진입 장벽이 낮아 보였음
Docker와 함께 제공되어 별도 설치가 불필요했음
초기 구성은 순조로웠다
# Docker Swarm 클러스터 초기화
docker swarm init
# 워커 노드 추가
docker swarm join --token <token> <manager-ip>:2377
# Eureka 서버 배포
docker service create \
--name eureka-server \
--network my-network \
--replicas 2 \
my-eureka:latest
Eureka, Zuul 같은 Netflix OSS 컴포넌트들을 Docker 서비스로 배포하는 것까지는 무난했습니다.
Chapter 2: Graceful Shutdown 악몽의 시작
문제 발견
서비스 업데이트를 위해 docker service update
명령어를 실행했을 때 문제가 발생했습니다.
docker service update --image my-app:v2 my-service
기대했던 시나리오:
새 컨테이너 시작
기존 컨테이너가 SIGTERM 수신
Spring Boot가 Eureka 등록 해제 후 graceful shutdown
기존 컨테이너 종료
실제 일어난 일:
새 컨테이너 시작 ✅
기존 컨테이너 즉시 종료 ❌
Eureka에 죽은 인스턴스 정보가 남아 있음 ❌
API 게이트웨이에서 503 에러 발생 ❌
문제 분석: Eureka의 내부 동작 이해
종료된 서비스로 요청을 전달하는 문제가 발견되어 Eureka 서버의 코드를 분석했습니다. PeerAwareInstanceRegistry 구현체는 eureka-core
라이브러리에서 제공됩니다.
classDiagram
LookupService <|-- InstanceRegistry
LeaseManager <|-- InstanceRegistry
InstanceRegistry <|-- PeerAwareInstanceRegistry
InstanceRegistry <|-- AbstractInstanceRegistry
PeerAwareInstanceRegistry <|-- PeerAwareInstanceRegistryImpl
AbstractInstanceRegistry <|-- PeerAwareInstanceRegistryImpl
class InstanceRegistry {
<<Interface>>
}
class AbstractInstanceRegistry {
<<Abstract>>
}
class PeerAwareInstanceRegistry {
<<Interface>>
}
class LookupService {
<<Interface>>
}
class LeaseManager {
<<Interface>>
}
public class EurekaServerAutoConfiguration implements WebMvcConfigurer {
@Bean
public PeerAwareInstanceRegistry peerAwareInstanceRegistry(
ServerCodecs serverCodecs) {
this.eurekaClient.getApplications(); // force initialization
return new InstanceRegistry(this.eurekaServerConfig, this.eurekaClientConfig,
serverCodecs, this.eurekaClient,
this.instanceRegistryProperties.getExpectedNumberOfClientsSendingRenews(),
this.instanceRegistryProperties.getDefaultOpenForTrafficCount());
}
}
public class PeerAwareInstanceRegistryImpl extends AbstractInstanceRegistry {
@Inject
public PeerAwareInstanceRegistryImpl(/* ... */) {
// 인스턴스 상태 결정 규칙 체인
this.instanceStatusOverrideRule = new FirstMatchWinsCompositeRule(
new DownOrStartingRule(), // 1순위
new OverrideExistsRule(overrides), // 2순위
new LeaseExistsRule() // 3순위
);
}
}
인스턴스의 상태 변경을 레지스트리에 갱신하기 위해서는 특정 Rule을 통과해야 합니다.
DownOrStartingRule: DOWN과 STARTING 상태만 통과
OverrideExistsRule: 인자로 전달된 Map에 존재하는 인스턴스만 통과
OUT_OF_SERVICE
상태가 통과할 수 있는 Rule은 OverrideExistsRule
뿐입니다. 그러나 해당 Map이 비어 있어 OUT_OF_SERVICE
는 Rule 체인을 통과하지 못하고 기존 상태인 UP으로 유지됩니다.
해결책: 커스텀 Graceful Shutdown 라이브러리 개발
라이프사이클에 개입하는 다소 위험한 방식으로 해결했습니다. 아래 코드는 OUT_OF_SERVICE
상태가 Rule 체인을 통과하도록 강제합니다.
@EventListener(ContextClosedEvent.class)
@Order(Ordered.HIGHEST_PRECEDENCE)
public void contextClosed(ContextClosedEvent event) throws InterruptedException {
if (isEventFromLocalContext(event)) {
updateHealthToOutOfService();
waitForContainerStatusToSee();
}
}
private void updateHealthToOutOfService() {
health = new Health.Builder()
.status(props.getHealthDuringShutdown())
.build();
LOG.info("Health status set to {}", health.toString());
}
여전히 남은 Docker Swarm의 한계
제한적인 AWS 통합
운영 부담: 클러스터 및 노드 관리
Chapter 3: AWS ECS로의 전환
현실적인 선택
팀의 상황을 재평가한 결과:
Kubernetes (EKS): 너무 복잡하고 학습 곡선이 가파름
Docker Swarm: 기능은 작동하지만 AWS 환경과의 통합에 한계
AWS ECS: AWS 생태계와의 통합이 용이하고 관리가 단순
ECS의 장점들
1. AWS 네이티브 통합
ALB 연동
CloudWatch 로그 자동 수집
IAM 역할 기반 권한 관리
Fargate로 인스턴스 관리 최소화
2. 운영 편의성
# 서비스 상태 확인
aws ecs describe-services --cluster my-cluster --services my-service
# 로그 확인
aws logs get-log-events --log-group-name /ecs/my-service
3. 안정적인 배포
롤링 업데이트 자동 지원
헬스체크 기반 트래픽 라우팅
자동 롤백 기능
4. CI/CD 파이프라인 단순화
#!/bin/bash
# deploy.sh
docker build -t my-app:$BUILD_NUMBER .
aws ecr get-login-password | docker login --username AWS --password-stdin $ECR_URI
docker tag my-app:$BUILD_NUMBER $ECR_URI/my-app:$BUILD_NUMBER
docker push $ECR_URI/my-app:$BUILD_NUMBER
aws ecs register-task-definition \
--cli-input-json file://task-definition.json
aws ecs update-service \
--cluster my-cluster \
--service my-service \
--task-definition my-app:$BUILD_NUMBER
Chapter 4: 교훈과 인사이트
기술 선택의 현실성
"최신 기술"과 "업계 표준"보다 더 중요한 것은:
팀의 현재 역량
실제 요구사항
운영 가능한 복잡도 수준
MSA의 복잡성 체감
graph TD
A[API Gateway] --> B[User Service]
A --> C[Order Service]
A --> D[Payment Service]
B --> E[User DB]
C --> F[Order DB]
D --> G[Payment DB]
B --> H[Redis Cache]
C --> H
D --> H
단순해 보이는 구조도 실제로는 다음과 같은 고려사항이 많았습니다.
서비스 간 네트워크 통신
분산 트랜잭션 관리
서비스 디스커버리
모니터링 및 로깅
장애 전파 방지
다음 편 예고: "AWS ECS 기반 CI/CD 파이프라인 구축과 운영 노하우"에서는 실제 운영 환경에서의 배포 전략, 인증/인가, 트러블슈팅 경험을 공유하겠습니다.
참고 자료
Subscribe to my newsletter
Read articles from CheolHo Jung directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
