코드에서 네트워크 호출은 한 줄이지만, 실제로는 독립적으로 실패하는 두 시스템 사이의 경계다. 요청이 느릴 수 있고, 응답이 사라질 수 있으며, 응답은 없어도 원격 작업은 완료될 수 있다. 이 경계를 성공과 실패 두 값으로만 다루면 운영 장애가 주문 중복, 대기열 증가, 재시도 폭주로 확산된다.
원문: Beyond Happy Path Engineering: the Network
네트워크의 해피 패스
일반적인 checkout은 결제 서비스에 요청을 보내고, 응답을 받은 뒤 주문을 완료한다. 네트워크가 빠르고 요청이 정확히 한 번 전달되며 응답도 항상 돌아온다면 이 흐름은 간단하다.
하지만 실제 네트워크는 느려지고, 연결이 끊기고, 서버가 과부하되며, 응답이 유실된다. 더 까다로운 경우는 timeout이 발생했는데 결제 서비스에서는 이미 결제가 승인된 상황이다. 호출자는 실패로 보지만 원격 시스템에는 성공이 남는다.
timeout은 취소가 아니다
timeout은 caller가 기다릴 수 있는 유용한 시간의 한도다. 원격 작업을 되돌리거나 서버에서 실행 중인 작업을 중단시키는 마법의 신호가 아니다. 따라서 timeout 뒤에는 최소한 두 가지를 구분해야 한다.
- 원격 작업이 확실히 실패했는가?
- 결과를 알 수 없을 뿐, 작업은 계속되거나 완료됐을 수 있는가?
결과가 불명확한 결제를 즉시 실패로 기록하면 사용자가 다시 결제해 중복 청구가 발생할 수 있다. 반대로 무조건 성공으로 기록해도 위험하다. payment_pending, payment_unknown 같은 상태와 조회·재조정 경로를 두어 불확실성을 데이터 모델에 반영해야 한다.
재시도는 부하를 증폭시킨다
재시도는 일시적인 오류를 회복시키지만, 장애 중인 의존성에 작업을 더 보낼 수도 있다. 호출자 1,000개가 동시에 재시도하면 작은 지연이 재시도 폭주로 바뀐다.
안전한 재시도에는 다음 조건이 필요하다.
- 전체 요청 deadline 안에서 실행한다.
- 횟수와 총 대기 시간을 제한한다.
- exponential backoff로 간격을 늘린다.
- jitter로 동시 재시도를 분산한다.
- 요청이 재시도 가능한 오류인지 구분한다.
특히 결제나 주문처럼 부작용이 있는 요청은 재시도 전에 멱등성 계약을 마련해야 한다. 같은 idempotency key를 가진 요청은 서버가 동일한 논리적 작업으로 인식하고, 이미 처리한 결과를 돌려주거나 중복 실행을 차단해야 한다.
장애가 전파되지 않게 한다
네트워크 경계의 문제는 호출자 전체로 퍼질 수 있다. 느린 결제 서비스가 checkout worker를 점유하고, 대기열을 키우고, 결국 다른 기능까지 느리게 만드는 식이다.
이를 막는 대표적인 장치는 다음과 같다.
- circuit breaker: 실패가 누적된 의존성으로 요청을 계속 보내지 않고 잠시 차단한다.
- load shedding: 시스템이 포화되기 전에 우선순위가 낮은 요청을 거절한다.
- bulkhead: 결제·재고·관리자 기능의 worker와 connection pool을 분리한다.
- cancellation: 사용자가 더 이상 기다리지 않을 때 불필요한 후속 작업을 줄인다.
- graceful degradation: 영수증 메일·추천·분석처럼 핵심 흐름에 필요 없는 기능을 지연하거나 생략한다.
이 장치들은 실패를 없애지 않는다. 대신 한 의존성의 문제가 전체 서비스의 실패로 번지는 속도와 범위를 제한한다.
중요한 작업과 부가 작업을 분리한다
checkout 성공을 위해 반드시 필요한 작업과 나중에 처리해도 되는 작업을 같은 요청 경로에 넣으면 장애 지점이 늘어난다. 결제 승인과 재고 확정은 핵심 경로에 둘 수 있지만, 영수증 메일·분석 이벤트·추천 업데이트는 큐로 넘기는 편이 낫다.
그 결과 사용자는 핵심 결과를 더 빨리 받고, 부가 기능의 지연이 주문 전체를 실패시키지 않는다. 다만 “나중에 처리한다”는 말에는 재처리, 중복 방지, 실패 알림, 보존 기간 같은 운영 계약이 따라야 한다.
관찰해야 할 것은 경계다
평균 latency 하나만으로는 네트워크 건강 상태를 알기 어렵다. p95와 p99 같은 tail latency, timeout 수, 오류 수, 재시도 수와 재시도 성공률, queue 대기, connection pool 포화, circuit breaker 상태, idempotency replay를 함께 봐야 한다.
특히 호출자와 의존 서비스가 각각 건강해 보여도 둘 사이의 경계가 느리거나 불안정할 수 있다. 따라서 메트릭과 로그에는 요청의 deadline, attempt 수, idempotency key, 최종 상태, timeout 이후 재조정 여부가 남아야 한다. 사용자에게는 숨긴 graceful degradation도 운영자에게는 분명히 보여야 한다.
실무 설계 원칙
네트워크 호출을 추가할 때 다음을 먼저 결정하면 된다.
- 호출자에게 허용된 대기 예산은 얼마인가?
- timeout 뒤 원격 작업이 진행될 수 있는가?
- 재시도해도 안전한가? 안전하다면 요청의 정체성은 무엇인가?
- 결과가 불명확할 때 어떤 상태를 보여주고 어떻게 재조정하는가?
- 이 의존성이 느려질 때 어떤 작업을 줄이고 어떤 기능을 보존하는가?
이 질문에 대한 답이 코드의 timeout 값이나 retry loop보다 먼저 존재해야 한다. 그래야 네트워크의 불확실성이 데이터 모델과 운영 절차에 반영된다.
결론 및 시사점
한 줄 결론: 네트워크를 신뢰하는 시스템은 응답이 항상 온다고 가정하는 시스템이 아니라, 느림·유실·중복·불명확성을 상태와 계약으로 처리하는 시스템이다.