본문 바로가기
Framework/Spring Boot

[Spring Boot] 카오스 엔지니어링 (Chaos Engineering) - 프로덕션 환경에 원숭이를 풀어 고의로 장애를 유발하고 서버의 회복력(Resiliency) 테스트하기

반응형

우리는 지난 54단계까지 단위 테스트(BDD), 통합 테스트(Testcontainers 격리), 커버리지 강제 검증 게이트(Jacoco/SonarQube), 그리고 극한의 성능 테스트(JMeter/nGrinder)라는 완벽한 4중 방어막을 구축했습니다. 이제 내 코드는 논리적으로 버그가 0개이며, 초당 1만 건의 트래픽(TPS)을 가뿐히 버텨냅니다. 완벽하죠? 하지만 실무의 거친 프로덕션(Production) 세계는 당신의 논리 정연한 코드와 테스트 결과를 비웃습니다.

클라우드 벤더사(AWS, GCP)의 데이터센터 스위치가 갑자기 벼락을 맞아 타버려서 DB와의 네트워크가 5초간 단절된다면? 우리가 결제를 연동 중인 외부 PG사(카카오페이, 토스)의 API 서버가 트래픽 폭주로 인해 갑자기 500 에러를 뿜어내며 기절해 버린다면? 완벽하게 짠 당신의 서버 역시 아무런 방어막 없이 외부 장애의 파도에 휩쓸려 연쇄적으로 붕괴(Cascading Failure)하고 맙니다. "완벽한 분산 시스템은 존재하지 않는다. 서버는 반드시 한 번은 죽고, 네트워크는 반드시 끊긴다." 이 철학을 뼈저리게 인정하는 데서부터 진정한 시니어 아키텍트의 길은 시작됩니다. 그리고 이 철학을 극단적으로 실천하는 광기의 방법론이 있습니다. 장애가 언제 터질지 벌벌 떨며 기다리지 말고, 우리가 스스로 대낮에 운영 서버의 랜선을 뽑아버리고 DB 커넥션을 강제로 다운시켜 버리면서 서버가 과연 살아남는지(회복력, Resilience)를 검증하는 미친 짓!

이번 55단계(Phase 11의 대미) 포스팅에서는 넷플릭스(Netflix)가 창시한 충격적인 테스팅 기법, Chaos Engineering(카오스 엔지니어링)과 이를 스프링 부트에 완벽히 이식한 Chaos Monkey for Spring Boot 라이브러리의 살벌한 활용법, 그리고 서킷 브레이커 검증술에 대해 상세히 알아보겠습니다.

1. 분산 컴퓨팅의 8가지 오류와 카오스 엔지니어링의 탄생

과거의 개발자들은 한 대의 거대한 슈퍼컴퓨터 서버(모놀리식) 안에 모든 기능을 구겨 넣고, 어떻게든 이 한 대의 서버가 안 죽게 만드는 데 혈안이 되어 있었습니다. 하지만 지금은 마이크로서비스(MSA)의 시대입니다. 유저가 버튼 하나를 누르면 주문 서버가 결제 서버를 부르고, 결제 서버는 재고 서버를 부르고, 재고 서버는 배송 서버를 부릅니다. 이 과정에서 필연적으로 '네트워크 통신(HTTP/gRPC)'이 발생합니다. 분산 컴퓨팅의 가장 첫 번째 원칙이자 오류는 "네트워크는 절대로 신뢰할 수 없다"는 것입니다. 패킷은 언제든 유실될 수 있고, 지연(Latency)은 결코 0이 아니며, 대역폭은 무한하지 않습니다.

Chaos Monkey for Spring Boot 아키텍처 다이어그램 및 서킷 브레이커 연동

세계 1위의 트래픽을 감당하는 넷플릭스는 2011년 AWS 서버가 통째로 날아가는 대형 장애를 겪은 후, 아예 사고방식을 180도 뒤집어버립니다. "AWS가 죽을까 봐 두려워하지 말고, 우리가 평일 대낮에 개발자들 다 출근해 있을 때 강제로 AWS 인스턴스 서버를 랜덤으로 하나씩 죽여보자! 그래도 고객들이 넷플릭스 영화를 보는데 전혀 지장이 없는 튼튼한 구조(폴백, 서킷 브레이커)를 만들어내자!" 이렇게 탄생한 것이 바로 서버에 무작위로 미친 원숭이를 풀어 난장판을 만드는 Simian Army(원숭이 군단) 프로젝트이자 카오스 엔지니어링의 시초입니다. 장애가 터지기를 '기다리지' 말고, 먼저 장애를 '주입(Inject)'하여 백신을 맞는 것입니다.

2. 내 프로젝트에 원숭이 키우기: Chaos Monkey for Spring Boot 세팅

넷플릭스의 사상은 훌륭하지만, 일반 기업의 주니어 개발자가 인프라 자체를 함부로 부수기는 너무나도 부담스럽습니다. 다행히 인프라가 아닌 소스 코드 레벨에서 이 원숭이를 키울 수 있는 chaos-monkey-spring-boot 라이브러리가 존재합니다. 이 라이브러리를 의존성(`build.gradle`)에 추가하고 런타임 프로파일(Profile)을 chaos-monkey로 활성화하는 순간, 여러분의 평화롭던 스프링 부트 애플리케이션 내부에는 통제 불능의 원숭이가 뛰어놀기 시작합니다.

🎯 원숭이의 타겟팅: Watcher (감시자) 설정

원숭이가 아무 곳에서나 난장을 피우게 할 수는 없습니다. 우리는 application.yml 설정을 통해 원숭이가 침투할 AOP 타겟(Watcher)을 정밀하게 지정합니다.

  • @RestController 감시: 사용자가 API를 호출하는 최초 진입점을 타겟으로 삼습니다. 클라이언트가 Timeout을 어떻게 처리하는지 봅니다.
  • @Service 감시: 핵심 비즈니스 로직이 수행되는 구간을 타겟으로 삼습니다.
  • 🌟 @Repository 감시: DB와 통신하는 구간입니다. 실무 장애의 90%는 DB 쿼리 응답 지연(Slow Query)이나 락(Lock) 대기에서 발생하므로 반드시 원숭이를 투입해야 합니다.
  • RestTemplate / WebClient 감시: 외부 시스템(타 MSA 서버나 결제사 API)을 호출하는 구간입니다. 외부 API가 죽었을 때 내 서버가 같이 뻗는지 테스트합니다.

💣 4대 무자비한 공격 패턴 (Assaults)

원숭이는 감시된 타겟을 지나가는 트래픽 중 설정된 확률(예: level=5 이면 20% 확률)로 끔찍한 4대 공격(Assault)을 무작위로 가합니다.

1. Latency Assault (지연 공격): 개발자가 짠 코드는 0.1초 만에 끝나야 하지만, 원숭이가 강제로 Thread.sleep(3000)~10000 사이의 랜덤한 딜레이를 먹여버립니다. 만약 톰캣 쓰레드 풀 200개가 이 10초 대기 때문에 순식간에 꽉 차버린다면 서버는 그대로 터지게 됩니다.

2. Exception Assault (예외 공격): 로직이 정상적으로 처리되었음에도 불구하고, 원숭이가 로직 마지막에 갑자기 RuntimeException이나 SQLException을 강제로 던져버립니다(Throw). 예외 처리가 제대로 안 된 코드는 500 에러를 뿜으며 처참하게 무너집니다.

3. AppKiller Assault (암살 공격): 최악의 공격입니다. 로직이 실행되다가 갑자기 JVM 레벨에서 System.exit(1)을 호출하여 스프링 부트 프로세스 자체를 아예 죽여버립니다. 쿠버네티스(K8s)의 자동 복구(Restart/Liveness Probe) 메커니즘이 제대로 작동하여 컨테이너가 다시 살아나는지 검증할 때 씁니다.

4. Memory Assault (메모리 공격): 원숭이가 가짜 바이트 배열 객체를 무한히 생성하여 JVM Heap 메모리를 가득 채워버립니다. GC(Garbage Collector)가 발작하다가 결국 OOM(Out of Memory)이 터졌을 때 시스템이 힙 덤프를 제대로 남기고 뻗는지 확인합니다.

3. 방패와 창의 대결: 서킷 브레이커(Resilience4j)와 폴백(Fallback) 검증

"아니, 멀쩡한 서버를 고의로 망가뜨려서 500 에러를 내면 도대체 무슨 의미가 있나요?" 카오스 엔지니어링의 핵심은 장애를 일으키는 것 자체가 아니라, 내가 세팅해 둔 서킷 브레이커(Circuit Breaker)와 폴백(Fallback) 방어막이 '실제 장애 상황에서' 완벽하게 동작하는지를 두 눈으로 직접 검증하기 위함입니다.

우리는 지난 포스팅들에서 타 MSA 서버 장애 시 내 서버까지 연쇄적으로 말라 죽는 것을 막기 위해 Resilience4j를 도입하여 서킷 브레이커 방어막을 달아두었습니다. 만약 외부 결제 API 호출 메서드에 @CircuitBreaker(fallbackMethod = "cachedData")를 달아두었다면, 카오스 몽키가 10초 지연(Latency) 공격이나 예외(Exception) 공격을 퍼부었을 때, 시스템 전체가 쓰레드 고갈로 뻗어버리는 것이 아니라 서킷 브레이커가 예민하게 반응하여 회로를 즉시 차단(Open)하고 안전하게 cachedData(임시 데이터나 캐시된 이전 데이터)를 유저에게 0.1초 만에 뱉어내는지를 반드시 모니터링해야 합니다. 만약 카오스 몽키가 날뛰는데 서킷 브레이커가 작동하지 않고 톰캣 쓰레드가 말라 죽는다면, 당신의 서킷 브레이커 타임아웃(Timeout)이나 슬라이딩 윈도우 설정값이 완전히 잘못되었다는 뜻입니다. 장애가 실제 프로덕션에서 고객을 덮치기 전에 '미리' 설정 오류를 바로잡을 수 있는 유일하고도 가장 강력한 백신 훈련이 바로 카오스 엔지니어링입니다.

정리하며 (Phase 11 종료)

이번 55단계 포스팅을 끝으로, 완벽한 백엔드 서버를 검증하기 위한 Phase 11: Advanced Testing & QA의 거대한 대장정이 마무리되었습니다. Testcontainers를 활용한 프로덕션급 환경 격리부터 BDD 방식의 테스트 설계, Jacoco/SonarQube 정적 분석 통제, JMeter 부하 테스트로 병목 추적, 그리고 극한의 생존 훈련인 카오스 몽키(Chaos Engineering)까지... 이 다섯 가지 테스트 및 QA 무기를 모두 마스터했다면, 여러분의 코드는 그 어떤 실무의 거친 파도나 악의적인 트래픽 폭주 앞에서도 절대 가라앉지 않는 불침함(Unsinkable Ship)이 된 것입니다. 자, 이제 테스트와 검증의 영역을 든든히 다졌으니 넘어서 다시 거대한 데이터 아키텍처의 심연으로 빠져볼 시간입니다. 데이터베이스 단 한 대로는 1만 명 이상의 동시 트래픽을 절대 감당할 수 없습니다. 읽기(Read)와 쓰기(Write)의 역할을 물리적으로 분리해야만 합니다!

다음 56단계(Phase 12 시작) 포스팅에서는 "DB Replication (Master-Slave): AbstractRoutingDataSource 라우팅을 통한 읽기/쓰기 분리(Read/Write Splitting) 마스터 아키텍처 완벽 구현 가이드"에 대해 아주 치밀하게 딥다이브 해보겠습니다!

반응형