Connection Refused and Timeout Are Completely Different News
한국어 원문으로 표시합니다.
한 줄 요약
"API 가 안 된다" 는 신고에서 가장 많은 정보를 담고 있는 것은 로그가 아니라 실패 메시지의 정확한 문구다.
왜 이게 필요했나
고객이 "API 가 죽었어요" 라고 말할 때, 그 문장 뒤에는 완전히 다른 다섯 가지 상황이 숨어 있을 수 있습니다. 그리고 어느 것인지는 실패 메시지 한 줄로 대부분 갈립니다.
Connection refused — 나쁜 소식이 아닙니다. 패킷이 목적지까지 갔다가 돌아왔다는 증거입니다. 라우팅, 방화벽, NAT 같은 경로상의 모든 관문을 이미 통과한 것이고, 남은 원인은 대상 호스트 한 대 안으로 좁혀집니다. 프로세스가 죽었거나, 다른 포트에서 듣고 있거나, 0.0.0.0 이 아니라 127.0.0.1 에만 바인딩돼 있습니다.
Connection timed out — 아무 정보가 없습니다. 아무도 대답하지 않았다는 뜻이고, 방화벽이 조용히 버렸을 수도 라우팅이 없을 수도 상대가 과부하일 수도 있습니다. 실무에서 쓰는 방화벽은 거의 언제나 조용히 버리는 정책이라, 방화벽이 막으면 refused 가 아니라 timeout 이 납니다. 그래서 refused 를 보고 방화벽을 뒤지는 것은 이미 통과한 관문을 다시 확인하는 일입니다.
Name or service not known — 대상 서버로 패킷이 한 개도 안 나갔습니다. 방화벽 로그에 아무것도 없는 것이 정상이고, 조사는 이름 해석 쪽으로 갑니다.
여기에 실패까지 걸린 시간을 함께 재면 확신이 생깁니다. 몇 밀리초 만에 끝났으면 왕복이 완료된 것(refused)이고, 2분 7초쯤에서 끊겼으면 커널이 SYN 재전송을 전부 소진한 것입니다. 앱 타임아웃을 30초로 설정했는데 실제로는 127초에 실패했다면, 그 설정이 적용되지 않고 있다는 신호입니다.
어떻게 동작하나
연결이 되고 나면 다음은 상태 코드입니다. 여기서 가장 자주 오진되는 것이 502 와 504 입니다.
502 는 응답을 못 받은 것이 아니라 유효하지 않은 응답을 받은 것입니다. 응답이 오긴 왔는데 프록시가 파싱하지 못한 경우도 502 입니다. 그래서 502 에 타임아웃을 늘리는 것은 아무 효과가 없습니다. 시간 안에 응답을 못 받은 경우는 504 입니다.
그리고 문서가 부실한 API 에 붙을 때 반드시 확인해야 할 것이 세 가지 더 있습니다.
페이지네이션의 끝을 어떻게 아는가. has_next 같은 명시적 플래그가 있으면 그것을 믿고, 없으면 빈 배열이 올 때까지 돕니다. 여기서 흔한 버그는 첫 페이지의 total 만 보고 페이지 수를 계산한 뒤 실제로는 마지막 페이지를 빠뜨리는 것입니다. 나눗셈의 나머지를 잊는 고전적 실수입니다.
일시적 실패를 어떻게 다루는가. 503 이나 네트워크 오류는 재시도로 넘어가는 경우가 많습니다. 다만 재시도해도 되는 것과 안 되는 것이 갈립니다. 조회는 몇 번을 반복해도 같지만, 생성 요청을 재시도하면 중복이 만들어집니다. 4xx 는 요청을 고치지 않으면 몇 번을 보내도 같은 결과이므로 재시도 대상이 아닙니다.
문서에 없는 결측이 얼마나 있는가. 이것이 실무에서 가장 자주 사람을 잡습니다. 명세에는 필수라고 적힌 필드가 실제 응답에서는 빈 문자열로 오는 경우가 흔하고, 그 값을 그대로 집계에 넣으면 조용히 틀린 숫자가 나옵니다.
현장에서 만나는 모습
그래서 새 API 에 붙을 때의 첫 작업은 코드를 쓰는 것이 아니라 전수 조사입니다. 모든 페이지를 한 번 돌면서 총 건수, 합계, 그리고 각 필드의 결측 건수를 세어 봅니다.
이 한 번의 조사가 이후 몇 주의 디버깅을 없앱니다. "총 60건 중 6건은 region 이 비어 있습니다. 이 건들을 어떻게 처리할까요" 라는 질문을 첫 주에 던지면, 나중에 지역별 매출 합계가 안 맞는다는 신고가 오지 않습니다.
남의 API 에 기대는 코드를 안전하게 만드는 것
전수 조사로 무엇을 상대하는지 알았다면, 그다음은 그 API 가 흔들려도 우리가 안 무너지게 만드는 일이다. 남의 API 는 우리가 고칠 수 없으므로 가정을 코드에 적어 두는 것이 유일한 방어다.
받은 것을 그대로 믿지 않는다. 명세에 필수라고 적힌 필드가 비어 오는 것을 이미 봤다. 그래서 읽는 지점에서 형과 범위를 확인하고, 어긋나면 그 건을 건너뛰되 건너뛴 수를 세어 남긴다. 조용히 버리면 나중에 합계가 안 맞는 이유를 찾지 못하고, 통째로 실패하면 한 건 때문에 전체가 멈춘다.
타임아웃을 반드시 건다. 기본값이 없거나 무한인 라이브러리가 많다. 타임아웃 없는 호출 하나가 워커를 영원히 붙잡으면, 앞에서 본 커넥션 풀 고갈이 그대로 재현된다. 연결까지의 시간과 응답까지의 시간을 따로 걸 수 있으면 나눠 건다.
재시도는 멱등한 것에만. 앞에서 말한 대로 조회는 몇 번을 해도 같지만 생성은 아니다. 그리고 재시도 간격에 무작위를 섞지 않으면, 상대가 잠깐 흔들렸다가 회복될 때 모든 클라이언트가 같은 순간에 몰려 다시 쓰러뜨린다.
응답을 그대로 저장해 둔다. 원본을 남겨 두면 파싱 규칙을 나중에 고쳤을 때 다시 돌릴 수 있다. 파싱한 결과만 저장하면 규칙이 틀렸다는 것을 발견한 시점에 이미 원본이 없다. 저장 비용보다 다시 받아 오는 비용이 대개 훨씬 크다.
마지막으로 상대가 바뀐 것을 알아채는 장치를 둔다. 응답의 필드 구성이나 건수가 평소와 크게 다르면 알리게 해 두면, API 가 조용히 바뀌었을 때 사용자보다 먼저 안다. 남의 API 는 예고 없이 바뀌고, 예고했더라도 그 메일은 대개 우리에게 오지 않는다.
다음 실습에서 할 것
문서가 한 페이지짜리 위키가 전부인 주문 API 에 붙어, 버전을 확인하고, 모든 페이지를 돌며 총 건수와 합계를 내고, 문서에 없는 결측을 세고, 불안정한 엔드포인트를 재시도로 통과합니다.