요청 기록으로 실패 분류
100분 안팎
학습 목표
연결 실패·시간 초과·HTTP 오류를 증거로 분류합니다.
개념
한 요청의 양쪽 기록을 맞춥니다
사용자가 안내판을 못 봤다는 보고를 받으면 서버의 마지막 오류만 찾기보다 해당 요청이 무엇을 했는지부터 확인합니다. 클라이언트 결과와 서버 로그는 서로 다른 위치의 관측입니다. 요청 ID는 그 둘을 이어 주는 표식입니다. ID가 같다고 해서 실패 원인이 자동으로 밝혀지는 것은 아니므로 시각, 경로, 상태, 지연을 함께 확인합니다. 이 레슨은 여러 줄의 요청 결과를 원인 단계별 건수로 분류하고, 어떤 경우에 서버 로그가 없거나 뒤늦게 남는지 판단하는 훈련입니다.
제공된 서비스의 로그 필드를 읽습니다
RequestLogFilter는 request_id, method, path, status, duration_ms를 한 줄로 남깁니다. 응답 헤더 X-Request-ID에도 같은 ID를 넣습니다. 입력 ID가 영문·숫자·하이픈 1~64자 규칙을 만족하면 재사용하고 그렇지 않으면 새 UUID를 만듭니다. 공백이나 줄바꿈을 그대로 로그에 넣으면 기록의 구조가 깨질 수 있어 입력을 제한합니다. ID는 인증 정보나 권한을 의미하지 않습니다. 학습용으로 전달한 lesson-01이 로그에 있어도 그 요청을 신뢰할 사용자인지 판단할 근거는 아닙니다.
기록할 것과 빼야 할 것을 정합니다
서비스는 요청 경로를 기록하지만 쿼리 문자열과 Authorization 헤더는 기록하지 않습니다. 비밀이 흔히 들어가는 부분을 무심코 저장하지 않기 위해서입니다. 이 API의 경로는 고정된 실습 경로이지만 다른 시스템의 경로에 개인정보가 들어 있다면 경로도 정규화하거나 숨길 필요가 있습니다. 제공 테스트는 FAKE_ONLY라는 가짜 값을 사용하여 해당 값이 로그에 없는지 확인합니다. 실제 비밀을 테스트 fixture에 넣지 않습니다. 로그 파일 접근 권한과 보관 기간의 정책은 뒤 권한·관측 모듈에서 확장합니다.
HTTP 상태와 명령 종료를 분리합니다
HTTP 500을 받은 요청에는 응답이 있고 HTTP 단계의 실패로 분류합니다. 셸 명령의 비영 종료와 HTTP 상태를 혼동하지 않습니다. curl은 HTTP 오류를 별도 옵션 없이 받으면 명령 종료가 성공일 수 있습니다. 반대로 명령이 DNS나 TLS 단계에서 실패했다면 HTTP 상태를 받지 못할 수 있습니다. 이 브라우저 실습의 상태 0은 실제 HTTP 응답 코드가 아니라 응답 없음이라는 입력용 표식입니다. 해당 숫자를 서버에서 반환한 정상 상태로 해석하지 않습니다.
시간 초과에서 서버 로그가 늦을 수 있습니다
클라이언트가 정해진 시간에 포기해도 서버는 작업을 계속할 수 있습니다. 제공 /fixture/slow의 경우 클라이언트는 50ms에 시간 초과가 나고 서버는 300ms 후 응답을 만들 수 있습니다. 따라서 클라이언트 timeout과 서버 200 로그가 함께 있어도 기록이 모순이라고 단정하지 않습니다. 어느 시점에 관측했는지, 요청 ID가 같은지, 프록시나 재시도 요청이 섞이지 않았는지 확인합니다. 이번 실습은 timeout 오류 필드를 우선하여 사용자가 제때 결과를 받지 못했다는 사실을 보존합니다.
서버 로그 없음의 범위를 해석합니다
이름 조회나 연결 실패는 애플리케이션까지 요청이 오지 않아 서버 요청 로그가 없을 수 있습니다. 그러나 로그 없음만으로 DNS 실패를 결론 내리지 않습니다. 다른 인스턴스를 보고 있거나 로그 경로가 다르거나 수집이 늦었을 수도 있습니다. 클라이언트가 제공한 error 필드와 HTTP 응답 유무를 먼저 비교합니다. 실습 입력은 이미 관측을 정규화해 제공하므로 문자열로 원인을 추측하지 않습니다. 실제 현장에서는 정규화하기 전 원문과 도구 옵션도 보관하여 분류 근거를 재검토할 수 있게 합니다.
입력 형식을 하나로 정합니다
각 비어 있지 않은 줄은 request_id status error 세 칸입니다. 예를 들면 r1 200 -는 응답을 받은 정상 사례이고 r2 0 dns는 이름 조회 실패입니다. status는 정수이며 error는 -, dns, connect, tls, timeout 중 하나입니다. 빈 줄은 건너뜁니다. ID는 이 연습에서는 한 칸짜리 표식이고 중복 ID도 줄 하나당 요청 결과 한 건으로 셉니다. 따라서 재시도 자료에서 같은 ID를 합쳐 버리지 않습니다. 요청 건수인지 사용자 수인지 구분하는 습관이 뒤 SLI 설계에도 이어집니다.
분류의 우선순위를 정합니다
error가 dns, connect, tls, timeout이면 status보다 error를 우선합니다. r3 200 timeout도 timeout으로 셉니다. error가 -인 경우 200 이상 400 미만은 ok, 400 이상 600 미만은 http입니다. 0처럼 응답을 확정할 수 없는 상태와 100·600처럼 이 계약 밖의 상태는 unknown입니다. 3xx를 ok로 묶는 것은 교육용 응답 도달 분류이며 최종 화면 성공을 보장하는 규칙은 아닙니다. 입력 형식이 깨진 줄도 unknown 한 건으로 셉니다. 오류 자료를 조용히 버리지 않도록 정한 계약입니다.
형식이 깨졌을 때 동작을 정합니다
세 칸이 아니거나 status가 정수가 아니거나 error가 허용 목록에 없으면 unknown입니다. 예를 들어 broken, r1 nope -, r2 200 mystery는 각각 unknown 한 건입니다. 정상 데이터만 받을 것이라 기대하고 모든 줄을 즉시 int로 바꾸면 ValueError에서 프로그램이 멈춥니다. 먼저 split 결과의 길이를 확인하고 변환을 try/except로 보호합니다. 예외를 모두 숨기기보다 이처럼 예상한 형식 오류만 처리합니다. 표준 출력에는 채점 결과만 남기며 설명 문장이나 디버그 값을 섞지 않습니다.
Python의 필요한 문법을 준비합니다
import sys 후 sys.stdin을 반복하면 표준 입력을 한 줄씩 읽습니다. line.split()은 공백과 탭을 기준으로 칸을 나누며 빈 줄은 빈 목록이 됩니다. 딕셔너리는 분류 이름을 키로 두고 건수를 저장합니다. counts[name] += 1은 해당 건수를 하나 올립니다. Python 딕셔너리는 삽입 순서를 유지하지만, 출력 계약을 명시하기 위해 order 목록으로 순서를 고정합니다. 이번 과제는 정규표현식이 없어도 풀 수 있습니다. 형식 경계를 먼저 확인하는 짧은 함수로 분류와 집계를 나누면 실패 테스트의 원인을 좁히기 쉽습니다.
출력 계약과 빈 입력을 확인합니다
출력은 ok, dns, connect, tls, timeout, http, unknown 순서로 이름과 건수를 한 줄씩 씁니다. 입력이 없으면 모든 건수 0인 일곱 줄을 출력합니다. 399는 ok, 400과 599는 http, 600은 unknown이라는 경계를 검사합니다. 같은 요청 ID 두 번과 공백 줄도 포함합니다. 이 방식은 입력 건수와 분류 합계를 비교하기 쉽고 빈 시간대에도 자료가 없는 것과 프로그램이 실행되지 않은 것을 구분할 출발점이 됩니다. 여기서는 한 줄의 결과를 한 건으로 계산한다는 계약을 유지합니다.
테스트 실패를 읽고 수정합니다
첫 시작 코드는 모든 줄을 ok로 세므로 DNS와 HTTP 오류 fixture에서 기대 결과가 다릅니다. 테스트를 하나씩 대조하여 입력의 어떤 칸이 우선인지 확인합니다. 형식 오류 사례에서 traceback이 보이면 칸 수나 정수 변환 검사가 빠졌는지 먼저 봅니다. 출력 숫자는 맞는데 채점이 실패하면 정해진 순서와 불필요한 문장을 확인합니다. 모범 답안은 같은 계약을 구현하며 외부 네트워크 호출을 하지 않습니다. 로그 분석의 파일 읽기·정규표현식 확장은 더 읽기의 서재 장에서 이어 갑니다.
미션 기록과 연결합니다
이 집계는 실패 단계의 분포를 보여 주지만 근본 원인을 확정하지는 않습니다. HTTP 500 두 건이 같은 배포 문제인지 알려면 요청 ID와 서버 로그를 다시 연결해야 합니다. 미션에서는 실제 /health·/notices 응답과 요청 ID 로그를 확인하고 DNS·connect·timeout·HTTP 오류 네 가지의 관측·다음 단계도 기록합니다. 분류 건수와 개별 증거를 함께 제출하면 다음 작업자가 재현할 수 있습니다. 빠르게 결론을 내는 것보다 증거가 어느 경계까지 설명하는지를 명확히 말하는 것이 이 레슨의 완료 기준입니다.
따라하기
필드와 빈 줄 읽기
split과 빈 줄 건너뛰기를 먼저 익힙니다. status와 error를 판단하기 전에 세 칸인지 확인합니다.
for line in ['r1 200 -', '', 'r2 0 dns']:
fields = line.split()
if fields:
print(len(fields), fields[0])실행 결과
3 r1 3 r2
정수 변환 오류를 분리
예상한 형식 오류만 잡습니다. 전체 프로그램의 모든 예외를 숨기지 않습니다.
for text in ['200', 'nope']:
try:
print(int(text))
except ValueError:
print('unknown')실행 결과
200 unknown
분류 우선순위 확인
서버가 뒤늦게 200을 기록했어도 클라이언트 제한 내 결과를 못 받은 관측은 timeout으로 보존합니다. 완성 함수에는 unknown 경계도 넣습니다.
status, error = 200, 'timeout'
category = error if error != '-' else ('ok' if 200 <= status < 400 else 'http')
print(category)실행 결과
timeout
고정 순서로 빈 건수 출력
이 출력은 빈 입력의 계약입니다. 브라우저 실습에서 혼합 결과·상태 경계·중복 ID·형식 오류까지 8개 테스트를 통과시킵니다.
order = ['ok', 'dns', 'connect', 'tls', 'timeout', 'http', 'unknown']
counts = dict.fromkeys(order, 0)
for name in order:
print(name, counts[name])실행 결과
ok 0 dns 0 connect 0 tls 0 timeout 0 http 0 unknown 0
확인 문제
실습
각 줄 request_id status error를 분류하여 ok/dns/connect/tls/timeout/http/unknown 순으로 건수를 출력합니다. 빈 줄은 제외합니다. 세 칸이 아니거나 status 정수 변환 실패 또는 미지원 error면 unknown 한 건입니다. error가 dns/connect/tls/timeout이면 상태보다 우선합니다. error=-에서는 200~399는 ok, 400~599는 http, 나머지는 unknown입니다. 상태 0은 응답 없음 표식입니다. 같은 ID도 각 줄을 셉니다. 빈 입력은 0인 일곱 줄입니다. 디버그 문장은 출력하지 않습니다.
모범 답안
import sys
order = ['ok', 'dns', 'connect', 'tls', 'timeout', 'http', 'unknown']
counts = dict.fromkeys(order, 0)
for line in sys.stdin:
parts = line.split()
if not parts:
continue
category = 'unknown'
if len(parts) == 3:
request_id, text, error = parts
try:
status = int(text)
except ValueError:
status = None
if status is not None and error in ['-', 'dns', 'connect', 'tls', 'timeout']:
if error != '-':
category = error
elif 200 <= status < 400:
category = 'ok'
elif 400 <= status < 600:
category = 'http'
counts[category] += 1
for name in order:
print(name, counts[name])
더 읽기
면접 질문
- 클라이언트 시간 초과와 같은 요청 ID의 서버 200 로그가 함께 있을 때 어떻게 조사하겠습니까?
- 요청 로그에서 비밀을 제외하면서 장애를 추적하려면 어떤 필드를 남기겠습니까?