백엔드 프로덕션 시스템이 실제로 실패하는 방법
Source: Dev.to
(번역을 진행하려면 번역하고자 하는 원문 텍스트를 제공해 주세요.)
Introduction
프로덕션에 있는 시스템은 사고를 겪는 경우가 많으며, 그 빈도는 시스템마다 다릅니다. 대부분의 경우, 프로덕션에서 문제가 발생했을 때 코드는 작성된 대로 정확히 동작하고 있습니다. 문제는 프로덕션 환경이 사전에 완전히 시뮬레이션할 수 없는 조건들을 도입한다는 점입니다. 이 글에서는 이러한 실패가 실제로 어떻게 발생하는지 살펴보고, 이를 세 가지 패턴으로 구분한 뒤, 이러한 패턴이 왜 위험한지 언급하며, 배울 수 있는 교훈에 대해 간략히 다루겠습니다.
프로덕션 시스템이 실패하는 이유는 코드가 나쁘기 때문이 아니라, 현실이 항상 일관되지 않기 때문입니다.
전제 조건
진행하기 전에, 이 글이 다음 대상에게 해당한다는 점을 알려드립니다:
- 백엔드 엔지니어
- 운영 중인 시스템을 담당하는 사람들
- 대시보드가 “녹색”이라고 표시되지만 사용자들은 불만을 토로하는 모든 사람
Source: …
Failure Patterns
Failure Pattern #1: Cascading Failures
Cascading failures occur when one service in a system becomes slow or fails, which in turn affects how other parts of the system that depend on the service behave. Cascading failures can even arise from small user actions, for example, retries.
Example
I once worked on a project where a cascading failure occurred. Certain DB queries created bottlenecks due to their complexity and the growing size of the data. To make matters worse, the connection pool had reached its maximum number of slots, so further database calls could not be processed. This resulted in two likely scenarios:
- The user’s request was abruptly cancelled and they tried again.
- The request lingered in the system, waiting for an open connection to execute the query.
The second option became a cascading failure: the system tried to process more than it should at a given time while also handling regular incoming requests. This led to longer wait times; even simple tasks like logging in took a long time. In some cases, the CPU maxed out and the entire system became unresponsive, putting the system in a slow state.
What happens in the background?
Each request runs on a thread for its lifetime. The thread pool (i.e., the allocation of threads for processes) is limited; when requests pile up, they consume the available threads, leaving new requests stuck in a waiting state. What actually gets exhausted is rarely “the server” itself—it’s thread pools, DB connections, or queue workers.
Why is this dangerous?
- Slowness is contagious. A slow component or service will affect the delivery of other services, presenting a broken system to users.
- Partial health illusion. A system can look healthy in isolation while failing as a whole due to cascading failures. Depending on the design, some services may continue to operate, but their dependence on the affected service causes the entire system to fail.
Lessons learned
- Timeouts – Ensure long‑running requests or batch jobs are terminated after a reasonable period. Timeouts can be applied to known bottlenecks, such as external provider calls or heavy database queries.
- Circuit breakers – Route traffic away from failing services or dependencies to healthy alternatives. For example, if a third‑party payment provider is down, a circuit breaker can switch to another provider until the primary one recovers.
Failure Pattern #2: Partial Failures
Partial failures occur when only a part of a system fails while other parts continue to function, leading to incomplete or inconsistent results. They are subtle but can be very expensive.
Example
I worked on a payment system where users could initiate charges. One user attempted to charge their card but received no response in time, so they retried. The payment service experienced a brief downtime and could not fully process the request, but it still accepted incoming requests into a queue. When the service recovered, it processed each request as a unique transaction, unaware that the second request was a retry, resulting in a double charge.
From the user’s perspective, retrying is reasonable. From the system’s perspective, each request looked unique, so duplicates were created. Nothing was technically a bug; every step made sense in isolation.
Why is this dangerous?
Partial failures put systems in an “in‑between” state:
- User view: “It didn’t work.”
- System view: “Part of it did work.”
This creates a divergent truth where some operations succeed, others fail, and the system and user disagree about the outcome.
Lessons learned
- Idempotency – Design operations to be idempotent so that retries do not cause side‑effects such as double charges.
- Transactional guarantees – Use atomic transactions or two‑phase commits where appropriate to ensure that either all steps succeed or none do.
Visibility & monitoring – 대시보드와 알림에 부분 실패 상태를 표시하여 운영자가 불일치가 커지기 전에 조치를 취할 수 있도록 합니다.
Failure Pattern #3: [Placeholder for Third Pattern]
(원본 내용은 세 번째 패턴에 대한 설명이 끝나기 전에 종료되었습니다. 해당 설명, 위험성 및 교훈이 제공될 경우 여기에 삽입하십시오.)
추가 실패 패턴
실패 패턴 #2: 중복 요청
중복 요청은 피할 수 없습니다; 사용자가 페이지를 새로 고치거나, 클라이언트 앱이 요청을 다시 보내거나, 다른 시스템이 자동으로 재시도할 수 있습니다. 백엔드는 다음 시나리오를 가정해야 합니다:
“이 요청이 한 번 이상 전송될 수 있다”
그리고 이를 적절히 처리해야 합니다. 시스템은 요청 식별자를 사용해 멱등성을 달성할 수 있으며, 동일한 소스로부터의 재시도를 같은 요청으로 취급하고 다음 중 하나를 선택해 중복을 방지합니다:
- 첫 번째 요청의 결과를 반환하거나,
- 이전 요청을 버리고 최신 요청을 처리한다 (시스템마다 다름).
실패 패턴 #3: 무음 실패
무음 실패는 가장 치명적입니다. 왜냐하면 눈치채기 가장 어렵기 때문입니다. 백그라운드 작업이 조용히 실패하거나 보고서가 생성되지 않을 수 있습니다. 처음에는 모든 것이 정상으로 보이다가, 며칠 뒤에 누군가 불일치를 발견하게 됩니다.
무음 실패가 반드시 시스템이 다운됐다는 뜻은 아닙니다; 보통 다음 어느 하나라도 발생할 때 일어납니다:
- 시스템이 계속 운영 중이다
- 요청이 성공한 것처럼 보인다
- 알림이 발생하지 않는다
- 대시보드가 “정상”으로 보인다
…하지만 비즈니스 결과는 잘못되었습니다.
본질적으로 무음 실패는 오류 신호가 정확성을 관찰하는 레이어에 전달되지 않는 실패 모드입니다. 캐시 쓰기 실패나 이벤트가 발행됐지만 소비되지 않는 것과 같은 아주 단순한 작업도 무음 실패의 징후가 될 수 있습니다.
왜 위험한가?
무음 실패가 발생하면 시스템 사용자는 즉시 알아차리지 못하고, 팀은 모든 것이 정상이라고 가정합니다. 이는 문제가 누적되어 비즈니스에 영향을 미칠 때까지 이어집니다. 예시:
- 결제 없이 생성된 주문
- 송장이 발송되지 않은 결제
- 소비되지 않은 이벤트
이때 백엔드에는 “역사적 손상”이 남게 됩니다. 많은 경우 버그를 수정해도 새로운 데이터는 정상이지만, 기존 데이터는 여전히 잘못된 상태로 남습니다. 팀은 다음과 같은 기술을 사용해야 합니다:
- 데이터 백‑필링
- 이벤트 재처리
- 일회성 마이그레이션 스크립트
교훈
- 관찰 가능성 – 모든 백엔드 시스템에 필수적입니다. 잘못 구현되면 사실상 쓸모가 없습니다. 적절한 관찰 가능성은 시스템이 올바르게 동작하고 있는지를 알려줍니다.
- 로깅 – 연관된 엔티티(
orderID,transactionReferenceID등), 실패 원인, 다음 단계 등을 기록해야 합니다. 좋은 로그는 알림, 흐름 추적, 무음 실패의 빠른 탐지를 가능하게 합니다. - 메트릭 – 무음 실패를 감지하는 데 핵심입니다. 무음 오류는 요청이 성공했을 때 발생하므로,
orders_count_total,events_published_total,completed_payments_total,abandoned_payments_total와 같은 도메인 메트릭이 유용합니다. 이를 활용해 관계를 검증하거나 알림을 발생시킵니다.- 예시:
abandoned_payments_total이 임계값을 초과하거나orders_count_total과completed_payments_total이 크게 차이날 경우 알림을 발생시킵니다.
- 예시:
- 알림 – 실행 가능할 때만 의미가 있습니다. “오류율이 증가했다”는 알림은 잡음에 불과하며, 조치를 취하기에 충분한 정보를 제공하지 못합니다. 실행 가능한 알림은 무엇이 깨졌는지, 어디를 확인해야 하는지, 왜 중요한지를 알려줘야 합니다.
요약: 알림이 다음에 해야 할 일을 알려주지 않으면 잡음에 불과합니다.
또한 “작동한다”는 “정확하다”는 의미가 아니라는 점을 이해하는 것이 중요합니다, 특히 무음 실패를 다룰 때는 더욱 그렇습니다. 백엔드 시스템은 가용성, 처리량, 복원력을 최적화하면서 동시에 정확성도 최적화해야 합니다.
결론
프로덕션 장애는 드물게 “잘못된 코드”에서 비롯됩니다. 실제 환경 조건—리소스 제한, 네트워크 파티션, 서드파티 장애, 인간 행동—이 테스트 환경에서 완전히 재현될 수 없기 때문에 발생합니다. 세 가지 실패 패턴, 왜 위험한지, 그리고 해당 교훈(타임아웃, 회로 차단기, 멱등성 등)을 적용하는 방법을 이해하면 시스템 복원력을 크게 향상시킬 수 있습니다.
결론
프로덕션 장애는 알림이 울릴 때 시작되는 것이 아니라 가정이 검증되지 않을 때 시작됩니다. 목표는 무결점이 아니라 볼 수 있고, 이해할 수 있으며, 복구할 수 있는 장애입니다. 프로덕션 시스템은 기본적으로 크게 실패하지 않고 조용히 실패합니다—우리가 설계하지 않는 한.
엔지니어로서 우리는:
- 시스템을 구축할 때 장애 패턴을 고려한다.
- 프로덕션 이슈는 불가피하다는 것을 받아들인다.
- 시스템을 장기적으로 견고하게 유지할 수 있는 방식으로 대응한다.