200 응답을 받았는데 화면은 잘못된 이유

배포 직후 curl -I로 확인한 주소가 200이면 웹 서버에 접속할 수 있다는 사실은 확인된다. 하지만 사용자가 받을 문서까지 확인한 것은 아니다. 요청한 경로가 옛 릴리스의 HTML을 가리키거나, 오류 안내를 200으로 내보내는 경우에도 헤더 검사만으로는 정상처럼 보일 수 있다. 이 글의 질문은 하나다. 배포 완료를 판단할 때 HEAD의 상태 코드만 확인해도 될까?

이를 구분하려고 Python 3 표준 라이브러리로 임시 HTTP 서버를 만들었다. 운영 사이트를 고장내거나 설정을 바꾸지 않고 127.0.0.1의 자동 배정 포트에 네 경로를 열었다. 정상 본문, 다른 릴리스의 본문, HEAD와 GET을 다르게 처리하는 합성 분기, 실제 404를 각각 만들고 같은 경로에 두 메서드를 요청했다. 메서드별 차이는 실험용으로 의도적으로 구현한 것이며 실제 운영 장애를 발견했다는 뜻이 아니다.

같은 네 경로에 HEAD와 GET을 요청한 결과

직접 관찰한 결과 · 2026-10-04
경로 조건HEAD 상태HEAD 본문 바이트GET 상태GET 본문 바이트GET 예상 문자열
정상 본문200020041있음
다른 릴리스 본문200020036없음
의도적으로 다른 메서드 처리200050324없음
없는 경로404040422없음

정상 경로에서는 GET 본문에 RELEASE-EXPECTED-v2가 있었다. 다른 릴리스 경로도 HEAD와 GET 모두 200이었지만 기대한 문자열은 없었다. 따라서 GET으로 바꾸는 것만으로도 부족했다. HTTP 상태가 정상인지와 요청한 내용이 맞는지를 별도로 판단해야 했다. 이때 release 문자열은 합성 실험의 식별자이지 서비스에 반드시 노출해야 하는 운영 정보가 아니다.

의도적으로 분기한 경로는 HEAD 200, GET 503이었다. 실제 서버도 그렇다고 일반화할 수는 없지만, HEAD가 처리되는 코드 경로와 사용자의 GET이 처리되는 코드 경로를 확인해야 하는 이유는 보여 준다. 없는 경로는 두 요청 모두 404였으므로 HEAD가 언제나 잘못된 검사라는 결론도 아니다.

Content-Length가 있어도 HEAD 본문을 읽을 수는 없다

MDN은 HEAD를 GET과 같은 헤더를 요청하되 응답 본문을 받지 않는 메서드로 설명한다. Content-Length가 있다면 대응하는 GET 표현의 길이를 나타내며, HEAD 응답에 그만큼의 본문이 들어왔다는 뜻이 아니다. 이번 실험에서도 네 HEAD 응답의 실제 본문은 모두 0바이트였다. 본문 문자열 검사를 HEAD에 붙이면 정상 페이지에서도 원하는 글자를 찾을 수 없다.

이번 서버는 길이를 계산하기 쉬운 짧은 합성 HTML만 썼다. 압축, 스트리밍, CDN에서의 응답 헤더 생략, 인증에 따른 문서 변경은 구현하지 않았다. 이 표에서 본문 길이가 같다는 이유만으로 다른 환경의 문서가 동일하다고 판단할 수 없다. 비교할 때는 실제 GET 본문과 서비스에 의미 있는 조건을 함께 봐야 한다.

배포 확인을 세 가지 질문으로 나누기

확인할 질문이번 실험에서 쓴 확인판단할 수 없는 범위
주소가 응답하는가HEAD 상태문서 내용과 기능 정상 여부
GET 요청이 성공하는가GET 상태가 200인지 확인다른 HTML이 200으로 나온 경우
기대한 페이지인가GET 본문의 고유 문자열 확인로그인·저장·검색 등 다른 기능 전체

실제 서비스에서는 고정된 진단 문자열 대신 해당 페이지의 제목이나 본문 표식처럼 변경을 관리할 수 있는 값을 선택할 수 있다. 아무 페이지에나 있는 사이트 이름 하나만 검사하면 오류 페이지도 통과한다. 홈 확인용으로 고른 문자열과 글 상세 확인용 문자열을 분리하고, 리다이렉트가 의도한 주소에서 끝났는지도 기록하는 편이 판단하기 쉽다. 동적인 문서는 날짜나 랜덤 값처럼 매번 바뀌는 항목을 기준으로 삼지 않는 것이 좋다.

curl로 공개 페이지의 상태와 본문을 함께 확인하기

아래는 주소만 조회하는 별도 예시다. TARGET_URL을 본인이 확인할 공개 페이지로 바꾸고 EXPECTED_TEXT에는 그 문서에만 있는 제목 일부를 넣는다. 인증 헤더·쿠키를 넣지 않는다. curl의 --fail은 HTTP 오류 응답을 실패로 처리하고 -L은 리다이렉트를 따른다. 이 예시는 본문 검사 방법을 설명하기 위한 것이며 위 실험의 측정에 curl을 사용했다는 뜻은 아니다.

공개 문서의 GET 응답 검사 · Bash, curl, Python 3
TARGET_URL='https://example.com/'
EXPECTED_TEXT='Example Domain'
CHECK_BODY=$(mktemp)
trap 'rm -f "$CHECK_BODY"' EXIT
curl --fail --silent --show-error --location --max-time 15 \
  --output "$CHECK_BODY" \
  --write-out 'status=%{http_code} final_url=%{url_effective}\n' "$TARGET_URL" || exit 1
python3 - "$CHECK_BODY" "$EXPECTED_TEXT" <<'CHECK'
from pathlib import Path
import sys
body = Path(sys.argv[1]).read_text(encoding='utf-8')
if sys.argv[2] not in body:
    raise SystemExit('Expected page content was not found')
print('Expected content found')
CHECK

명령이 성공해도 모든 동작이 정상이라는 보장은 없다. 정적 문서의 접속·본문 확인과 검색·로그인·결제 같은 기능 검사는 범위가 다르다. 공개된 작은 페이지를 적은 횟수로 확인하고 결과가 필요한 범위만 점검해야 한다. 이번 실험에서는 응답 시간의 우열, 운영 부하, 외부 연결의 안정성을 측정하지 않았다.

실험을 직접 재현할 때

아래 원자료 링크의 JSON에는 네 경로의 HEAD·GET 상태와 본문이 들어 있다. 함께 제공한 Python 스크립트는 표준 라이브러리만 사용한다. 빈 출력 경로를 지정해 실행하면 임시 서버를 만든 뒤 요청 결과를 저장하고 자신이 만든 서버만 종료한다. 이미 존재하는 결과 파일은 덮어쓰지 않는다. 운영 URL이나 운영 설정 파일을 인자로 받지 않으므로 이번 비교를 재현하기 위해 실서비스를 수정할 필요가 없다.

실험 환경과 원자료

관찰 시각: 2026-10-04T07:04:37.461472+00:00
os: Linux · python: 3.10.12 · server: Python ThreadingHTTPServer · binding: 127.0.0.1:자동 배정 포트

임시 루프백 서버의 네 경로에 HEAD와 GET을 각각 요청하고 상태·본문 바이트 수·예상 릴리스 문자열을 비교했다. 잘못된 본문과 메서드별 분기는 실험을 위해 의도적으로 구현했다.

확인하지 않은 범위: 합성 서버에서의 기능 비교이며 운영 장애·CDN·TLS·부하·성능 측정이 아니다. 실제 서비스가 HEAD와 GET을 다르게 구현했다는 증거가 아니다.

Python 3 표준 라이브러리만 사용합니다. 루프백의 자동 배정 포트에서 실험하며 관리자 권한과 외부 네트워크는 필요하지 않습니다. 자신이 만든 서버만 종료합니다. 기존 원자료 파일을 덮어쓰지 않으며 운영 서버 설정이나 서비스에는 접근하지 않습니다.

python3 lab-head-get-health-check.py --output new-observations.json

참고한 공식 문서

원고 수정: 2026-10-04 · 정정 요청

이 글의 보완 기록

  • · HEAD·GET 네 경로 격리 실험을 실행하고 원자료와 재현 스크립트를 대조해 최초 발행했다.