요청이 시간 초과되면 서버 작업도 멈출까?

반응형

주문 버튼을 눌렀는데 화면에 시간 초과 오류가 뜬다. 다시 누르면 될까? 화면이 답을 받지 못했다는 사실과 주문이 만들어졌는지는 다른 문제다. 서버가 요청을 받기 전에 연결이 끊겼을 수도 있고, 주문을 저장한 뒤 응답만 돌아오지 않았을 수도 있다.

시간 초과(timeout)는 기다리는 쪽이 정한 시간이 지났다는 뜻이다. 상대가 작업을 하지 않았다는 확인서는 아니다. 앞서 주소창에서 첫 화면까지 어떤 일이 일어나는지 따라갔다면, 이번에는 요청을 보낸 뒤 답이 오지 않는 구간을 살펴보자. 아래 주문과 시간은 동작을 설명하기 위한 예다.

같은 오류 화면 뒤에 세 가지 상태가 있다

클라이언트(client)는 서버에 요청을 보내는 프로그램이다. 여기서는 브라우저가 클라이언트다. 브라우저가 POST /orders 요청을 보낸 뒤 2초 동안 답을 기다린다고 하자. POST는 서버에 작업을 요청할 때 사용하는 하이퍼텍스트 전송 프로토콜(Hypertext Transfer Protocol, HTTP)의 요청 방법이다.

0.0초  브라우저: 주문 생성 요청 전송
2.0초  브라우저: 응답을 못 받아 기다리기 중단

이 두 줄만으로는 서버 상태를 알 수 없다. 요청이 서버에 닿지 않았다면 주문은 없다. 서버가 요청을 받아 처리 중이라면 2초 뒤에 주문이 생길 수도 있다. 서버가 이미 주문을 저장했는데 돌아오는 응답이 끊겼다면 주문은 존재한다. 브라우저에는 셋 다 시간 초과로 보일 수 있다.

마지막 경우를 시간으로 펼치면 이렇다. 서버가 1.7초에 주문 101을 저장했고 1.8초에 응답을 보냈지만, 응답이 브라우저에 닿기 전에 연결이 끊겼다. 브라우저는 2초에 기다리기를 끝낸다. 사용자가 본 오류 시각이 서버의 저장 시각보다 늦더라도 두 시각은 서로 다른 컴퓨터에서 관찰한 것이다. 기록의 시계가 정확히 같다고 가정하지 말고 주문 번호와 요청 식별자로 사건을 연결해야 한다.

서버가 주문을 데이터베이스(database)에 저장하고 변경을 확정하는 일을 커밋(commit)이라고 한다. 커밋이 끝난 뒤 브라우저 연결이 끊어져도 저장된 주문이 저절로 사라지지 않는다. 반대로 브라우저가 오류를 봤다고 반드시 커밋이 일어났다고 생각해서도 안 된다.

2초는 어디서부터 세는가

연결에 300밀리초, 서버 앞 대기열에 400밀리초, 데이터베이스 조회에 900밀리초, 응답을 보내는 데 200밀리초가 들었다면 합계는 1.8초다. 화면에 보여 주고 정리하는 시간까지 포함하면 브라우저의 2초 제한에 가까워진다. 각 단계에 2초씩 새로 주는 것과는 다르다.

전체 시간 예산: 2,000ms
연결:             300ms  → 남은 시간 1,700ms
서버 앞 대기:      400ms  → 남은 시간 1,300ms
데이터베이스:      900ms  → 남은 시간   400ms
응답 전송:         200ms  → 남은 시간   200ms

처리 시한(deadline)은 이 요청을 언제까지 기다릴지 정한 시점이다. 첫 서버가 다른 서버를 부를 때는 이미 쓴 700밀리초를 빼고 남은 시간만 넘겨야 한다. 매 서버가 새로 2초를 잡으면 브라우저가 기다리기를 끝낸 뒤에도 뒤쪽 서버는 계속 일할 수 있다.

시간 제한도 하나만 있는 것은 아니다. 연결을 맺을 때의 제한, 첫 응답을 기다리는 제한, 응답을 읽는 제한, 요청 전체의 제한이 다를 수 있다. “2초 만에 실패했다”는 말만으로는 어느 단계에서 시간이 다 썼는지 알 수 없다. 연결조차 못 했는지, 서버가 응답을 만들다가 늦었는지를 구분해야 한다.

예를 들어 서버 앞 대기열에서 1.5초를 쓴 뒤 데이터베이스 호출에 새로 2초를 허용하면 전체 요청은 3.5초 이상 이어질 수 있다. 브라우저의 2초 제한과 맞지 않는다. 서버가 하류로 넘길 때 남은 시간을 0.5초로 줄이거나, 이미 그 안에 끝날 가능성이 없다면 호출 자체를 시작하지 않는 선택이 필요하다. 짧은 제한을 무작정 모든 단계에 복사하는 것도 답은 아니다. 정상적인 요청이 끝나는 데 필요한 시간을 관찰해 전체 시한과 단계별 시한을 정해야 한다.

브라우저가 취소해도 이미 끝난 변경은 남는다

사용자가 화면을 닫거나 브라우저가 요청을 취소하면 서버에 더는 결과가 필요 없다는 신호를 보낼 수 있다. 서버가 그 신호를 받아 아직 시작하지 않은 비싼 계산을 멈추면 자원을 아낀다. 그러나 취소 신호가 늦게 도착하거나 서버 프로그램이 확인하지 않으면 작업이 계속될 수 있다.

특히 주문 저장이 끝난 뒤에는 취소 신호로 과거의 커밋을 없앨 수 없다. 주문 취소가 필요하다면 이미 만든 주문을 찾아 업무 규칙에 따라 상태를 바꾸는 별도 요청이 필요하다. 외부 결제 요청이나 이메일 발송도 브라우저 창을 닫는다고 되돌아가지 않는다.

프로그램끼리 원격 기능을 호출할 때 쓰는 gRPC 문서에서도 클라이언트가 기다리기를 그만두는 것과 서버 프로그램의 실제 작업 중단을 구분한다. 이 글의 브라우저 주문 예가 gRPC 방식으로 구현됐다는 뜻은 아니다. 취소 신호를 받은 서버 코드가 진행 중인 일을 정리해야 한다는 원칙을 확인하기 위한 자료다.

서버 코드가 취소를 확인하기 전에 데이터베이스 커밋을 마쳤다면 주문은 이미 있다. 취소를 확인하고 데이터베이스 변경 전에 중단했다면 주문은 없다. 이 경계를 알 수 없으면 브라우저 화면에도 성공 또는 실패를 확정해서 표시할 수 없다. 오래 걸리는 조회처럼 중간에 멈춰도 되는 작업과, 주문 상태 변경처럼 이미 끝난 결과를 확인해야 하는 작업은 취소 뒤 처리도 달라진다.

다시 누르면 주문이 두 개 생길 수도 있다

첫 요청이 저장된 뒤 응답만 사라졌다고 하자. 사용자가 같은 주문 버튼을 다시 누르면 새 요청이 서버에 간다. 서버가 두 요청을 서로 다른 주문으로 받아들이면 주문이 두 건 생긴다.

첫 요청: 주문 101 저장 → 응답 유실 → 화면에는 시간 초과
두 번째 요청: 같은 내용을 다시 전송 → 주문 102 저장

화면에서 버튼을 잠깐 비활성화하는 방법은 연속 클릭을 줄일 수 있다. 하지만 연결이 끊긴 뒤 앱을 다시 열거나 다른 기기에서 시도하는 경우까지 막지는 못한다. 서버가 같은 논리적 주문을 알아봐야 한다. 멱등성 키(idempotency key)는 한 주문 시도의 재전송에 같은 식별자를 붙여 서버가 이전 결과를 찾도록 하는 값이다. 멱등성 키가 중복 주문을 막는 과정은 앞선 글에서 따로 다뤘다.

같은 키를 보냈더라도 서버가 첫 요청을 처리 중일 수 있다. 두 번째 요청을 새 주문으로 만들지 않으면서 “처리 중”이라고 알려 주고, 잠시 뒤 같은 키의 결과를 조회하게 할 수 있다. 이미 성공했다면 처음 만든 주문 101을 돌려준다. 첫 요청이 실제로 시작되지 않았다면 그 키로 처리를 시작한다. 어떤 상태에서 다시 시도해도 새 주문을 만들지 않는 규칙이 있어야 한다.

다만 모든 실패를 기계적으로 재시도하면 안 된다. 서버가 이미 바쁠 때 여러 브라우저가 즉시 다시 보내면 대기열은 더 길어진다. 요청이 조회인지 변경인지, 같은 식별자로 결과를 확인할 수 있는지, 남은 시간에 한 번 더 시도할 여유가 있는지를 보고 결정해야 한다. 이미 만든 주문을 조회할 방법이 있다면 상태부터 확인하는 편이 안전하다.

실제로는 무엇을 확인해야 할까

브라우저의 네트워크 화면에서는 연결이 실패했는지, 요청을 보낸 뒤 응답을 기다리다가 끝났는지 볼 수 있다. 서버에는 요청 식별자, 접수 시각, 처리 시작과 완료 시각, 주문 번호를 남겨 두면 같은 요청의 경로를 맞춰 보기 쉽다. 브라우저 기록에 시간 초과가 있고 서버 기록에는 같은 식별자의 커밋이 있다면 결과가 사라진 위치는 커밋 이후다.

서버 기록이 보이지 않는다고 곧바로 “서버에 안 왔다”고 결론 낼 수도 없다. 요청이 앞단에서 막혔거나, 기록을 남기기 전에 프로세스가 종료됐거나, 로그 수집 자체가 실패했을 수 있다. 앞단 기록과 데이터베이스에 실제 주문이 있는지까지 확인해야 한다.

진단할 때는 최소한 네 시각을 따로 본다. 브라우저가 요청을 보낸 시각, 앞단이 받은 시각, 서버가 변경을 확정한 시각, 브라우저가 기다리기를 끝낸 시각이다. 사이에 기록이 비어 있다면 그 구간이 조사 대상이다. 서버의 완료 기록만 보고 브라우저가 성공 응답을 봤다고 판단할 수 없고, 브라우저의 시간 초과 기록만 보고 서버 변경이 없었다고 판단할 수도 없다.

화면에는 “실패했으니 다시 누르세요”보다 “처리 결과를 확인하고 있습니다”가 맞는 경우가 있다. 일정 시간이 지나도 결과를 확인하지 못했다면 미확인 상태를 그대로 보여 주고, 같은 주문을 안전하게 조회하거나 다시 시도할 수 있는 경로를 제공해야 한다. 시간 초과는 화면이 아는 범위를 말할 뿐, 서버에서 벌어진 일의 최종 판정이 아니다.

참고 자료

반응형
KEEP READING
카테고리 전체 보기 →

댓글