이름에서 HTTP 응답까지
100분 안팎
학습 목표
DNS·IP·TCP·TLS·HTTP와 응답 유무를 연결합니다.
개념
서버가 안 된다는 말을 쪼갭니다
안내판에 연결되지 않는다는 보고에는 이름을 못 찾은 경우, 연결이 거절된 경우, 인증서를 신뢰하지 못한 경우, 응답이 늦은 경우, HTTP 500을 받은 경우가 섞여 있습니다. 해결 순서를 정하려면 마지막으로 성공한 경계부터 찾습니다. DNS로 이름을 주소로 바꾸고, IP와 포트로 TCP 연결을 만들고, HTTPS라면 TLS를 거쳐 HTTP 요청·응답을 교환합니다. 실제 시스템은 프록시·캐시·연결 재사용 때문에 더 복잡할 수 있지만 이 레슨의 새 연결 fixture에서는 이 순서로 실패 위치를 좁힙니다.
주소와 포트를 함께 읽습니다
http://127.0.0.1:18080/health에서 http는 방식, 127.0.0.1은 호스트, 18080은 포트, /health는 경로입니다. 127.0.0.1은 현재 요청하는 컴퓨터 자신을 뜻하므로 다른 컴퓨터나 컨테이너에서 같은 주소가 같은 앱을 가리키지는 않습니다. 포트는 같은 컴퓨터 안에서 어느 listen 지점에 연결할지 정합니다. 경로는 연결 후 HTTP 서버가 요청을 나누는 기준입니다. 포트가 다르면 경로가 맞아도 접속이 안 되고 경로가 틀리면 연결 성공 후 404를 받을 수 있습니다.
DNS와 fixture 범위를 구분합니다
실제 DNS는 도메인 이름의 주소 정보를 조회하는 체계입니다. 이 실습에서는 시스템 DNS나 hosts 파일을 바꾸지 않습니다. check.py의 resolve가 notice.test만 127.0.0.1로 바꾸며 다른 이름에는 socket.gaierror를 발생시킵니다. 이는 실제 DNS 패킷 교환이나 캐시를 검사하는 것이 아니라 이름 조회 경계에 성공·실패를 주입하는 fixture입니다. 따라서 notice.test를 일반 브라우저에 입력해 실습 서버가 자동으로 열릴 것이라고 기대하지 않습니다. 운영 DNS 전파나 TTL 문제를 이 결과로 판단하지 않습니다.
이름 조회 실패 뒤에서 멈춥니다
이름을 주소로 바꾸지 못하면 그 이름을 대상으로 TCP 연결을 시도할 근거가 없습니다. curl에서는 Could not resolve host 계열 오류를 볼 수 있습니다. 호스트 철자, 개인 환경의 resolver 설정, 이름 조회 결과를 먼저 확인합니다. 앱 코드를 수정하거나 HTTP 라우트를 추가해도 이름 조회 자체는 고쳐지지 않습니다. 반대로 IP로 성공하고 이름으로만 실패하면 주소 결과와 이름을 사용하는 경로를 비교할 가설이 생깁니다. IP 접속 성공이 TLS 이름 검증까지 성공했다는 뜻은 아닙니다.
연결 거절과 시간 초과를 나눕니다
이름이 맞아도 대상 포트가 listen하지 않으면 연결이 거절될 수 있습니다. 실습은 임의 포트를 bind만 한 소켓으로 예약하여 다른 앱이 차지하지 않게 하고 listen하지 않은 상태를 만듭니다. connect 분류는 이 연결 시도 실패를 가리킵니다. 방화벽이 패킷을 버리거나 네트워크가 불안정하면 연결 단계에서도 시간 초과가 생길 수 있습니다. 시간 초과는 제한 시간 내 완료하지 못했다는 증거이지 특정 원인의 이름이 아닙니다. 그래서 먼저 주소·포트·연결과 응답 중 어디서 기다렸는지 추가로 봅니다.
TLS가 확인하는 두 조건을 읽습니다
HTTPS에서는 TLS를 사용하여 통신을 보호하고 인증서로 서버 신원을 확인합니다. 신뢰할 수 있는 인증 경로와 요청한 이름에 맞는 인증서는 별개의 조건입니다. 실습은 임시 자체서명 인증서를 만들며 이름은 notice.test입니다. 기본 신뢰 저장소는 이 교육용 인증서를 신뢰하지 않아 실패합니다. 이 인증서를 cafile로 지정한 실습 전용 컨텍스트는 성공하지만 wrong.test로 요청한 검사는 이름 불일치로 실패합니다. 이름 검증을 끄는 방식으로 문제를 해결하지 않습니다.
실습 TLS 프록시가 하는 일을 봅니다
Python TLS 프록시는 루프백에서 TLS 연결을 받아 한 번의 GET을 뒤의 로컬 HTTP fixture로 전달합니다. 평문 HTTP 서버와 TLS 서버는 서로 다른 임의 포트를 사용합니다. 신뢰한 인증서로 TLS를 완료한 뒤 /health의 200을 받아 전체 경계를 확인합니다. 실제 운영 프록시가 필요한 제한·부하 관리·접근 통제를 구현한 예제는 아닙니다. 외부 서비스로 요청을 보내지 않고 임시 인증서와 키도 검사가 끝나면 임시 폴더에서 정리합니다. 교육용 키를 운영 인증서로 재사용하지 않습니다.
HTTP 오류도 응답입니다
HTTP 500은 응답 상태 코드이므로 HTTP 응답을 받았다는 증거입니다. 이 요청이 거친 모든 서비스가 건강하다는 뜻은 아니며 프록시가 만든 500일 수도 있습니다. 실습에서는 /error가 의도적으로 500을 반환합니다. HTTP 404도 경로 처리 결과라 연결 실패와 구분합니다. curl은 HTTP 500을 받더라도 기본 설정에서는 명령 종료 0일 수 있습니다. HTTP 오류를 비영 종료로 다루려면 --fail 계열 옵션을 별도로 선택합니다. HTTP 상태와 셸 종료 상태를 같은 숫자 체계로 섞지 않습니다.
기다린 구간과 제한을 적습니다
/slow는 250ms 기다리고 probe의 클라이언트 제한은 50ms입니다. 이 요청은 timeout으로 분류합니다. 제한 시간은 임의의 운영 권장 수치가 아니라 실패를 빠르게 재현하는 fixture 값입니다. 서버의 요청 처리 자체는 계속되어 나중에 쓰기를 시도할 수 있습니다. 실제 클라이언트의 timeout이 연결 제한인지 전체 요청 제한인지도 도구별로 확인합니다. curl의 --connect-timeout은 연결 단계에, --max-time은 전체 작업 시간에 관련된 옵션입니다. 제한을 늘리기 전에 사용자 기대와 병목 증거를 검토합니다.
분류 함수를 작성합니다
diagnose.py의 classify는 stage가 dns, connect, tls, timeout이면 해당 실패를 우선 반환합니다. 응답을 받은 경우 상태 200 이상 400 미만은 교육용 ok, 400 이상 600 미만은 http, 상태가 없거나 나머지 값은 unknown입니다. 여기서 3xx가 ok인 것은 응답을 받았다는 교육 계약이며 최종 사용자 요청 성공을 정의하는 지표가 아닙니다. 실제 리다이렉트 완료를 확인하려면 별도 검사가 필요합니다. 함수가 모든 상태를 ok로 반환하면 이름 실패와 인증서 실패를 숨겨 check.sh에서 실패합니다.
실행 전 도구와 제약을 확인합니다
개인 Linux나 macOS에 Bash, Python 3.8 이상, OpenSSL 1.1.1 이상의 req -addext 기능이 필요합니다. check.sh는 도구 존재를 확인하고 인증서 생성 실패는 환경 오류로 안내합니다. 샌드박스에서 PermissionError: Operation not permitted가 bind 위치에 발생하면 코드의 실패 분류를 고치는 대신 포트가 허용된 개인 환경에서 실행합니다. 이 레슨 실습은 verify: external로 표시되어 그 실행까지 검증 대기입니다. 실행하지 못한 HTTP·TLS 결과는 예제 출력 칸에 채우지 않습니다.
완료 증거를 남깁니다
bash check.sh는 이름 실패, 닫힌 포트, 느린 응답, HTTP 500, 정상 HTTP, 미신뢰 인증서, 정상 TLS 프록시, 이름 불일치, 상태 없음, 399와 400 경계를 검사합니다. 모든 항목 PASS와 RESULT: 0 failure(s), 종료 0이 solution의 완료 기준입니다. PASS 줄은 실제 관측을 교육용 category로 정규화한 값이며 curl 원문을 흉내 낸 것이 아닙니다. 서버는 shutdown과 server_close로 닫고 스레드는 join으로 기다립니다. 네 가지 실패의 관측과 다음 확인을 미션 diagnosis.json에 옮겨 적습니다.
외부의 정답 대신 자신의 증거를 읽습니다
인터넷의 특정 사이트가 열리는지로 개인 안내판의 건강을 판단하지 않습니다. 같은 요청의 이름, 주소, 포트, TLS 이름, HTTP 상태를 순서대로 적습니다. DNS와 TLS의 세부 원리는 더 읽기의 서재로 보내고 여기서는 각 경계가 성공했는지와 다음으로 무엇을 볼지 설명합니다. 공식 오류 정의는 curl 오류 문서에서 확인할 수 있으며 OS·버전별 문구는 다를 수 있습니다. 오류 문자열 하나를 모든 환경의 절대 기준으로 삼지 않고 단계와 상태를 함께 기록합니다.
사실 확인 참고: curl 공식 오류 정의에서 이 레슨에 사용한 기능의 공식 정의를 확인할 수 있습니다.
TLS 구현의 신뢰 저장소와 이름 확인 기능은 Python ssl 공식 문서에서 더 확인합니다.
따라하기
응답 없는 실패와 HTTP 상태 구분
미리 제공한 관측을 분류하는 연습입니다. 네트워크 통신 결과를 얻는 코드는 아니며 None은 HTTP 상태를 받지 못했다는 표식입니다.
observations = [('dns', None), ('response', 500), ('response', None)]
for stage, status in observations:
if stage == 'dns': print('dns')
elif status is None: print('unknown')
elif 400 <= status < 600: print('http')실행 결과
dns http unknown
분류 구현과 로컬 fixture 실행
포트 허용 환경에서 starter를 실행해 실패를 읽고 diagnose.py를 고칩니다. 정상 solution은 11항목 PASS, 실패 0을 보여야 합니다. 이 샌드박스는 bind가 차단되어 결과 출력은 비워 둡니다. README의 OpenSSL 전제를 먼저 확인합니다.
bash check.shTLS 신뢰와 이름 조건 비교
solution zip에서 검사합니다. untrusted certificate와 hostname mismatch는 tls, trusted TLS proxy는 ok여야 합니다. check.py의 기본 컨텍스트, cafile 지정 컨텍스트, server_hostname=wrong.test를 찾아 조건의 차이를 설명합니다. 조회 fixture는 실제 DNS 서버 검사가 아니라는 점도 기록합니다. 출력은 외부 검증 뒤에 확보합니다.
python3 -B check.py개인 서비스의 요청 경계 확인
앞 레슨 앱이 실행 중인 개인 환경에서만 요청합니다. 상태 줄의 500과 셸 종료 상태를 각각 확인합니다. --fail을 추가했을 때의 차이도 비교합니다. 요청 ID와 시각으로 서버 로그를 연결합니다. 서버를 실행할 수 없는 이 샌드박스의 출력은 비웁니다.
curl --connect-timeout 2 --max-time 3 -i http://127.0.0.1:18080/fixture/error확인 문제
실습
diagnose.py의 classify를 구현하고 포트를 허용한 개인 환경에서 bash check.sh를 실행합니다. 이름 조회는 메모리 매핑 fixture이며 TCP와 TLS는 실제 루프백 통신입니다. DNS 실패·닫힌 포트·시간 초과·500·정상 HTTP·미신뢰/정상/이름 불일치 TLS와 상태 경계를 검사합니다. 시스템 DNS·hosts·인증서 저장소는 변경하지 않습니다. 현재 샌드박스는 bind를 차단하여 실행 검증 대기입니다.
실행 명령
bash check.sh
기대 결과
실제 루프백 TCP·TLS 프록시와 경계 11항목 PASS, RESULT: 0 failure(s), 종료 0
모범 답안
모범 답안 내려받기더 읽기
면접 질문
- HTTPS 요청이 실패했을 때 DNS, TCP, TLS, HTTP 중 어느 단계인지 어떻게 구분하겠습니까?
- 인증서를 신뢰해도 TLS 연결이 실패할 수 있는 이유는 무엇인가요?