타임아웃은 클라이언트 설정이 아니라 제품의 결정입니다
모든 실패에 같은 재시도 규칙을 붙이지 말고, 사용자가 기다릴 수 있는 시간과 실패 뒤에 알 수 있는 사실을 기준으로 타임아웃과 복구 정책을 설계합니다.
출처 및 AI 안내: 이 글은 제미니의 개발실무 유튜브를 기반으로 작성되었습니다.
gpt-5.6-sol모델을 사용해 생성·편집했습니다.
외부 시스템에서 타임아웃이 나면 익숙한 수단부터 떠올립니다. 다시 호출하거나, 실패 건을 저장하거나, 스케줄러를 돌리거나, 나중에 처리할 이벤트를 발행합니다. 모두 실무에서 쓰는 방법입니다. 하지만 방법의 이름만으로 정책이 되지는 않습니다.
결정은 호출 체인의 가장 끝, 결과를 기다리는 사람에게서 시작해야 합니다.
웹이나 앱이 주문 서버를 호출하고, 주문 서버는 결제 시스템을 호출한다고 해봅시다. 결제 시스템 뒤에는 카드사나 은행, 또 다른 원천사가 있을 수 있습니다. 연결마다 커넥션 타임아웃과 리드 타임아웃이 있습니다. 결제 시스템은 원천사가 최대 55초 걸리는 것을 고려해 60초를 허용했을 수 있습니다. 주문 서버가 그 값을 그대로 복사해도 더 중요한 질문은 남습니다. 그동안 고객 화면에는 무엇을 보여줄까요?
사용자 흐름이 빠진 타임아웃 값은 한 프로세스가 언제 기다리기를 포기하는지만 알려줍니다.
계층마다 기다림의 예산을 정합니다
어떤 제품은 결제 화면을 61초 동안 유지할 수 있습니다. 다른 제품은 30초만 기다린 뒤 처리 중 화면으로 넘기고 결과를 폴링할 수 있습니다. 앱은 15초 후 로딩 표시를 멈추고 결과를 푸시로 알려주겠다고 안내할 수도 있습니다. 동기 클라이언트가 떠난 뒤에도 서버 처리는 계속될 수 있습니다.
선택에 따라 백엔드 설계도 달라집니다. 처리 중 화면을 제공하려면 클라이언트가 조회할 상태가 필요합니다. 알림으로 끝내려면 완료되지 않은 작업을 안전하게 저장하고 최종 결과를 전달할 방법이 있어야 합니다. 동기 흐름이라면 네트워크와 애플리케이션 처리 여유를 포함해 클라이언트의 기다림 예산 안에서 전체 호출이 끝나야 합니다.
따라서 타임아웃 값은 제공하려는 사용자 경험에서 출발한 뒤 각 하위 시스템의 한계와 맞춰야 합니다. 라이브러리 기본값을 그대로 옮겨 적는 설정이 아닙니다.
각 계층의 책임도 분명해야 합니다. 클라이언트는 30초만 기다리고 주문 서버는 61초를 기다리는 구성이 의도일 수 있습니다. 사용자를 한 화면에 묶어두지는 않지만 서버는 결제 결과를 더 기다립니다. 이때 API는 모든 요청을 즉시 성공 또는 실패로만 꾸미지 말고 처리 중을 실제 상태로 표현해야 합니다.
커넥션 타임아웃과 리드 타임아웃은 다릅니다
커넥션 타임아웃은 정해진 시간 안에 연결하지 못했다는 뜻입니다. 어떤 시스템에서는 커넥션 타임아웃을 1초처럼 짧게 두고 몇 번만 재시도하는 전략이 가능합니다. 그래도 횟수에는 상한이 있어야 합니다. 네트워크가 끊겼거나 대상 호스트가 내려간 동안 모든 요청 스레드를 끝없이 붙잡을 수는 없습니다.
리드 타임아웃은 더 불편합니다. 연결은 됐고 요청이 상대 애플리케이션에 도착했을 수 있습니다. 응답을 받기 전에 호출자가 기다리기를 끝냈습니다. 원격 작업이 실행됐는지는 알 수 없습니다.
부작용이 있는 호출에서는 이 차이가 결정적입니다. 조회 요청은 재시도해도 괜찮을 수 있습니다. 결제 요청의 응답을 못 받았다는 이유로 다시 결제하면 고객에게 이중 청구될 수 있습니다. 결제 시스템이 동일 거래 방어 로직을 제공한다고 해도 주문 서버는 그 계약을 이해해야 합니다. 있다고 가정해서는 안 됩니다.
결과를 모르는 결제라면 안정적인 거래 식별자로 상태를 조회하는 편을 선호합니다. 즉시 확인할 수 없다면 처리 중으로 남겨두고 나중에 정합성을 맞춥니다. 스케줄러, 이벤트, 배치, 폴링, 알림을 조합할 수 있습니다. 필요한 동작이 먼저고 구현 수단은 그다음입니다.
복구가 장애를 더 키우지 않게 합니다
재시도는 일시적이고 반복해도 안전한 실패에 유용합니다. 모든 예외에 붙이면 위험합니다.
실무 정책이라면 적어도 다음 상황을 구분해야 합니다.
- 연결 자체가 만들어지지 않았다.
- 원격 시스템이 요청을 명확하게 거절했다.
- 요청은 처리됐을 수 있지만 응답을 받지 못했다.
- 같은 작업을 반복해도 안전하다.
- 상태를 바꾸는 작업이며 현재 결과를 알 수 없다.
전체 시간 예산과 최대 시도 횟수도 정해야 합니다. 그렇지 않으면 하위 계층이 재시도하는 동안 상위 호출자도 다시 요청해 장애 시점의 트래픽이 곱해집니다. 느려진 결제 시스템이 주문 서버의 스레드나 커넥션까지 소진시키면 외부 장애가 우리 장애가 됩니다.
실패 건을 저장하는 것만으로도 부족합니다. 여기서 “실패”가 무엇인지 알아야 합니다. 명확한 결제 거절은 종료 상태입니다. 결과를 모르는 작업은 조회하거나 확인해야 합니다. 안전하게 반복할 수 있는 작업만 재시도 큐에 넣을 수 있습니다. 셋을 하나의 범용 실패 테이블에 섞으면 스케줄러가 확인만 해야 할 작업을 다시 실행할 수 있습니다.
사용자 안내도 같은 기준으로 정해야 합니다. 시스템이 기다리기를 포기했을 뿐인데 “결제에 실패했습니다”라고 말하면 틀린 안내입니다. “결제 결과를 확인하고 있습니다”는 답답하지만 정직하고, 제품이 복구할 수 있는 상태를 남깁니다.
지금의 문제에서 시작합니다
실무의 정답을 “재시도를 쓴다”거나 “스케줄러를 쓴다”는 한 문장으로 만들 수 없습니다. 실제 시스템에서는 재시도, 실패 건 저장, 이벤트, 배치, 폴링, 서버 푸시, 앱 알림, 문자 전송을 상황에 따라 조합합니다.
쓸모 있는 질문은 더 좁습니다. 지금 어떤 실패가 발생했고, 실패한 뒤 무엇을 알 수 있으며, 마지막 사용자는 어떤 경험을 받아야 할까요?
그 답에서 각 계층의 기다림 예산을 정합니다. 회복 가능성이 높고 반복해도 안전한 실패만 재시도합니다. 결과를 모르는 부작용은 다시 실행하기보다 조회하고 정합성을 맞춥니다. 동기 요청이 끝내지 못한 일을 백그라운드에서 마칠 수 있도록 상태를 저장하고, 처리 중 상태를 클라이언트에도 보여줍니다.
타임아웃은 아무 일도 일어나지 않았다는 증거가 아닙니다. 한 호출자가 더는 기다리지 않기로 했다는 뜻일 뿐입니다. 이 두 사실을 분리하는 데서 좋은 장애 대응이 시작됩니다.