Resilient와 Battle-Tested는 같은 말이 아닙니다
AI 도구로 빠르게 만든 애플리케이션을 곧바로 production-grade나 battle-tested라고 부르는 것은 신뢰성의 서로 다른 축을 혼동하는 일입니다. Resilient는 예상한 오류를 견디도록 설계됐다는 뜻이고, battle-tested는 실제 운영 장애와 예측하지 못한 조건을 겪고 수정한 이력까지 포함해야 합니다.

본문
AI 도구가 아이디어를 작동하는 애플리케이션으로 바꾸는 시간을 크게 줄이면서, 이제는 빠르게 출시한 프로젝트를 곧바로 “production-grade” 또는 “battle-tested”라고 부르는 사례가 늘고 있습니다. 글쓴이는 더 많은 사람이 무언가를 만들고 출시하는 흐름 자체에는 긍정적이지만, 데모가 작동하고 몇 명의 사용자가 좋아했다는 사실만으로는 운영 환경에서 검증된 신뢰성을 주장할 수 없다고 지적합니다. “생존했다”는 말이 실제로 무엇을 의미하는지 체감한 경험을 계기로, 두 용어가 과장된 수식어가 아니라 시스템이 어떤 조건을 견뎠는지에 대한 경험적 주장이라고 설명합니다.
작동하는 애플리케이션과 운영 검증된 시스템은 다릅니다
“완전하고 기능적이며 resilient하다”는 표현은 기능이 명세대로 작동하고, 개발자가 예상한 오류를 처리하며, 정상적인 사용에서 충돌하지 않고, 적절한 테스트 커버리지를 갖추고, 데모에서 완성된 것처럼 보인다는 의미로 정리할 수 있습니다. 이는 현대적인 개발 도구를 사용해 훨씬 빠르게 달성할 수 있는 실제적이고 중요한 단계입니다. 글쓴이는 이런 결과를 README에 적고 자랑해도 좋지만, 아직 전쟁을 뜻하는 표현까지 사용할 필요는 없다고 말합니다.
반면 “production-grade”와 “battle-tested”는 단순히 더 다듬어졌거나 버그가 적다는 뜻이 아닙니다. 개발자가 완전히 예상하거나 시뮬레이션하기 어려운 조건에 노출됐는지가 핵심입니다. Resilient한 코드는 예상한 오류를 처리합니다. Battle-tested한 코드는 아무도 테스트 항목에 넣지 않았던 오류를 실제로 겪은 뒤에도 운영을 이어간 이력을 가집니다.
예상한 오류와 아무도 예상하지 못한 오류
운영 환경에서는 서드파티 의존성이 오류를 반환하는 대신 형식이 잘못된 데이터를 조용히 돌려줄 수 있습니다. 서버 사이의 clock skew가 발생할 수도 있고, 트래픽 급증으로 큐가 평소보다 10,000배 쌓일 수도 있습니다. 상위 서비스에서 발생한 하나의 느린 쿼리 때문에 connection pool이 고갈될 수도 있습니다. 서드파티 API가 HTTP 200 응답과 함께 엉터리 데이터를 반환하는 상황은 설계 단계에서 일부러 넣기 어렵고, 대개 실제 장애를 겪으며 발견합니다.
장애가 새벽 3시에 발생했을 때 온콜 엔지니어가 로그, 메트릭, 트레이스만으로 무슨 일이 일어났는지 파악할 수 있는지도 중요한 차이입니다. 코드 리뷰를 통과할 정도로 로그를 남기는 것과 실시간 장애를 디버깅할 수 있을 정도로 관측 가능성을 확보하는 것은 같지 않습니다. 어떤 수준의 로깅을 갖췄는지는 장애가 발생하기 전까지 알기 어렵고, 장애는 그 차이를 분명하게 드러냅니다.
하위 서비스가 중단됐을 때 애플리케이션 전체가 함께 무너지는지, 아니면 캐시된 데이터와 축소된 기능, 지연된 재시도 같은 방식으로 부분적인 기능 저하를 선택하는지도 확인해야 합니다. 부분 장애를 설계하는 작업은 눈에 잘 띄지 않으며, 이를 하지 않아 실제 피해를 겪은 뒤에야 중요성을 깨닫는 경우가 많습니다.
결제 중복 처리 사례가 보여주는 battle-tested의 의미
글에서는 결제 엔드포인트를 예로 듭니다. 계정에 금액을 적립하기 전에 해당 거래가 이미 처리됐는지 데이터베이스에서 확인하는 구현은 안전해 보입니다. 그러나 결제 제공자가 동일한 webhook을 수 밀리초 간격으로 두 번 재시도하면 두 요청이 모두 데이터베이스를 확인하고, 두 요청 모두 아직 처리되지 않았다고 판단한 뒤 계정에 금액을 적립할 수 있습니다.
테스트는 통과했고 staging 환경도 문제가 없었으며, production에서 몇 달 동안 정상적으로 동작했을 수 있습니다. 그럼에도 한 번의 운 나쁜 재시도가 race condition을 실제 금융 문제로 바꿀 수 있습니다. 이런 실패는 정상 입력과 일반적인 테스트만으로는 나타나지 않고, 개발자가 예상하지 못한 운영 조건에서 드러납니다.
기능적인 코드는 정상적인 입력을 처리합니다. Battle-tested한 코드는 rate limit을 점검하고, 인증 경계를 공격적으로 확인하며, injection 벡터를 던지고, 자원을 한계까지 압박하는 상황을 거칩니다. 이런 검증은 한 번 수행하고 끝나는 절차가 아니라, 실제 장애가 발생할 때마다 시스템을 조금씩 깎아내고 단단하게 만드는 과정입니다. 친절한 사용자는 좋은 UI를 보고 탭을 닫지만, 실제 운영 환경은 그렇게 친절하지 않습니다.
운영 역량은 코드만으로 완성되지 않습니다
무중단 배포, 안전한 rollback, 애플리케이션을 조용히 질식시키는 장시간의 테이블 잠금 없이 수행하는 migration도 production-grade 시스템에 포함됩니다. 이런 능력은 코드의 속성인 동시에 조직의 역량입니다. 깨끗한 아키텍처 다이어그램이나 잘 정리된 코드만으로는 만들어지지 않으며, 시간과 인프라 운영 경험이 필요합니다.
글쓴이가 정의하는 battle-tested의 문자 그대로의 의미는 시스템이 production에서 실제로 실패했고, 누군가 그 문제를 고쳤으며, 그 특정한 장애 때문에 postmortem, 모니터, regression test 가운데 하나 이상이 남아 있다는 것입니다. 아무리 잘 작성된 새 시스템이라도 이런 “흉터”가 아직 축적되지 않았다면 battle-tested하다고 부를 수 없습니다. 이를 자동으로 생성하는 버튼도 없습니다.
Resilient와 Battle-Tested는 서로 다른 축입니다
두 개념을 “먼저 resilient하게 만들고, 시간이 지나면 battle-tested가 된다”는 순서로 이해하는 것도 정확하지 않습니다. 수년 동안 production에서 실제 사용자와 부하, 장애를 겪은 코드는 battle-tested일 수 있지만 resilient하지 않을 수 있습니다. 예를 들어 외부 API가 중단됐을 때 fault tolerance를 설계하지 않았다면, 오랜 운영 이력이 있어도 시스템 전체가 함께 무너질 수 있습니다. 실제로 살아남은 이력과 처음부터 장애를 견디도록 설계한 계획은 같은 것이 아닙니다.
반대로 새로 배포한 시스템이 timeout, retry, circuit breaker, 캐시 기반 fallback을 갖추고 있다면 resilient할 수는 있지만 battle-tested하지는 않습니다. 이는 훌륭한 fault-tolerant 아키텍처일 수 있지만, 아직 실제 트래픽을 겪지 않은 설계상의 가설입니다. 잘 작성된 가설일 수는 있어도 운영 이력 자체는 아닙니다. 하나는 설계이고 다른 하나는 역사이므로, 한쪽을 가졌다는 이유로 다른 쪽의 용어를 빌려 사용할 수는 없습니다. 이상적으로는 두 가지를 모두 갖춰야 합니다.
결국 눈에 보이는 애플리케이션 구축은 작업의 20%에 가깝고, 나머지 80%는 판단의 영역이라고 글은 말합니다. 어떤 설계 선택이 8개월 뒤 조용히 시스템을 썩게 만들지 파악하고, 무엇을 만들지 않기로 결정하며, 여러 사람이 손댄 시스템이 왜 그런 모양이 됐는지 읽고, 운영 장애에 대응하는 능력은 작동하는 prototype만으로 드러나지 않습니다. 이런 능력은 시간, 실제 사용자, 실제 실패, 그리고 그 실패를 반영한 수정이 쌓인 뒤에야 나타납니다.
따라서 잘 설계되고 테스트된 애플리케이션을 “well-engineered” 또는 “early users를 받을 준비가 됐다”고 설명하는 것은 충분히 타당합니다. 다만 battle-tested라는 표현은 실제 전투를 요구합니다. 그 말이 의미를 유지하려면, 주말 프로젝트와 수년간의 장애 및 수정 과정을 거친 시스템을 같은 수준의 production-grade로 부르지 않아야 합니다.
원문: dev.to / 번역·요약: Trawling