Devin.KR

SLI의 분모와 실패 정의

75분 안팎

학습 목표

측정 범위·관측 기간·제외 조건을 명시합니다.

개념

숫자보다 측정 계약이 먼저입니다

안내판 배포 뒤 성공률이 100%라는 보고를 받았습니다. 조회 요청이 아니라 헬스 체크만 집계했다면 사용자가 겪은 실패를 설명하지 못합니다. SLI는 관심 있는 서비스 동작을 수치로 관측하는 지표입니다. 이번 레슨에서는 안내판 조회가 성공했는지 측정합니다. 비율을 구하기 전에 누가 어디에서 무엇을 세었는지 정해야 합니다. 계산 함수가 맞아도 측정 대상이 다르면 잘못된 운영 결정을 내립니다. 신입 담당자는 지표 이름 옆에 분자·분모·관측 지점·시간 경계를 붙여 보고할 수 있어야 합니다.

이번 모듈의 실행 환경

브라우저 실습은 Python 3 표준 입력으로 JSON 객체를 받고 표준 출력 한 줄을 채점합니다. 외부 계정과 서비스 접속은 없습니다. JSON의 true와 false는 Python으로 읽으면 True와 False입니다. 빈 표준 입력은 빈 데이터로 취급하지만 문법이 깨진 입력은 INVALID를 출력합니다. 로컬 zip은 압축을 푼 루트에서 bash check.sh로 검사하며 Python 3.8 이상을 사용합니다. 미션은 앞 모듈 전체를 포함하여 JDK 17·Spring Boot 3.1.5·Maven wrapper와 Docker가 필요합니다. Docker 통합은 외부 실행 검증 대기이며 교육용 자료와 자기 프로젝트만 대상으로 합니다.

사용자 흐름을 좁힙니다

사용자는 목록을 열어 안내문을 읽으려 합니다. 따라서 이번 측정 대상은 method가 GET이고 route가 /notices인 요청입니다. /health와 관리자 작업은 이 흐름의 분모에 포함하지 않습니다. 이 선택은 장애 발생 뒤 불리한 요청을 버리는 규칙이 아니라 측정 전 정한 범위입니다. 조회 기능 안에서 일어난 401·500·timeout은 모두 실패로 셉니다. 인증 오류를 제외하는 별도 계약을 만들려면 어떤 사용자와 요청을 보장하려는지 사전에 설명하고 정책 버전을 바꿔야 합니다. 이번 과제에서 상태별 사후 제외는 허용하지 않습니다.

분모와 분자를 따로 셉니다

분모 total은 시간·경로·메서드 조건을 만족한 요청 수입니다. 분자 good은 그중 상태가 200부터 299이고 timeout이 false인 요청 수입니다. bad는 total에서 good을 뺀 값입니다. 이 예제는 응답 본문의 내용 정확성을 검증하지 않는 가용성 지표입니다. 200 응답에 엉뚱한 내용이 있어도 good으로 분류될 수 있다는 한계를 적습니다. 별도의 본문 계약 검사가 필요하다면 측정 필드를 확장해야 합니다. 성공률과 응답 지연 목표를 하나의 이름에 섞지 않고 어떤 조건을 추가했는지 드러냅니다.

시간 구간의 끝을 정합니다

브라우저 입력의 start와 end는 정수 시간 번호이며 start 이상 end 미만인 반열린 구간을 사용합니다. 0부터 10이라는 창에 at=0은 들어가고 at=10은 다음 창으로 넘어갑니다. 이 규칙은 인접한 두 창에 경계 요청을 중복 집계하는 실수를 막습니다. 실제 관측에서는 UTC 시각과 시간 동기화·수집 지연 정책도 필요합니다. 교육 입력의 정수는 시간대 변환을 배우기 위한 날짜가 아니라 구간 포함 판정을 연습하는 값입니다. end가 start보다 크지 않으면 계산 가능한 측정 구간이 아니므로 INVALID입니다.

관측 지점이 바뀌면 값도 달라집니다

m08의 windows.json에는 클라이언트가 본 status와 timeout이 있습니다. 서버 로그는 필터가 기록한 결과이며 클라이언트 관측과 다를 수 있습니다. 서버가 결국 200을 남겨도 사용자가 그 전에 timeout을 만났다면 이 레슨의 계약에서는 실패입니다. 요청 로그와 span은 그 차이를 조사할 자료이지 클라이언트 실패를 지우는 근거가 아닙니다. 브라우저 예제의 status=200, timeout=true를 일부러 테스트합니다. 클라이언트와 서버 자료를 합칠 때는 관측 지점을 표시하고 같은 요청을 두 번 분모에 더하지 않습니다.

누락과 제외를 구별합니다

제외는 /health처럼 계약 밖 동작을 세지 않는 결정입니다. 누락은 계약 안 요청이 관측 자료에 남지 않은 문제입니다. 장애 때 로그 수집기가 멈춰 실패 요청만 빠지면 기록된 성공률이 좋아 보일 수 있습니다. 분모 0은 사용자가 모두 만족했다는 의미가 아닙니다. 요청이 없었는지 수집이 끊겼는지 추가 자료를 확인해야 합니다. 이번 프로그램은 NO_DATA를 내보내 판단을 보류합니다. 정책에는 원천 자료·수집 경계·누락 감시 방법을 남기고 보고서에는 자료가 비어 있는 구간을 표시합니다.

중복 ID를 숨기지 않습니다

같은 request_id를 두 번 넣으면 요청 건수를 늘리거나 성공을 중복 계산할 위험이 있습니다. 브라우저 입력은 ID가 비어 있지 않은 문자열이며 전체 events 안에서 중복되지 않아야 합니다. 중복이면 INVALID를 출력하고 재수집·병합 절차를 확인합니다. 실제 요청 ID는 유일성이 항상 보장되지 않을 수 있어 시각·클라이언트 표본 번호 같은 별도 키가 필요합니다. 이 교육 계약은 임의로 마지막 행을 선택하지 않는 연습입니다. 경로 밖 이벤트라도 스키마와 ID 중복은 검사하여 잘못된 원천 자료를 조용히 넘기지 않습니다.

형식 오류는 실패율과 다릅니다

status는 100부터 599인 정수이고 timeout은 JSON 불리언입니다. 문자열 "200"이나 문자열 "false"를 자동 변환하지 않습니다. Python에서 bool은 int의 하위 타입이므로 isinstance만 쓰면 true가 숫자로 들어갈 수 있습니다. type(value) is int로 이번 정수 계약을 검사합니다. at도 정수이고 route와 method는 문자열입니다. INVALID는 사용자 요청이 실패했다는 상태가 아니라 지표 계산 입력이 계약을 어겼다는 뜻입니다. 원천 오류를 HTTP 실패 한 건으로 바꾸면 운영 통계와 수집 품질 문제가 섞입니다.

표시 반올림으로 판정하지 않습니다

good을 total로 나눈 뒤 100을 곱하여 소수 둘째 자리까지 출력합니다. 표시 문자열 99.00은 관측 비율을 읽기 쉽게 만든 결과이며 원래 건수를 대체하지 않습니다. total과 good을 함께 출력하면 다른 사람이 계산을 검산할 수 있습니다. 표시가 같아 보이는 두 비율도 정밀한 값은 다를 수 있습니다. 다음 레슨에서 목표 충족 여부는 정수 건수와 목표 단위를 이용해 비교합니다. 반올림된 화면 값만 보고 배포를 승인하는 습관을 피하고 원천 수치와 정책 버전을 같이 보관합니다.

따라하기에서 함수를 분리합니다

먼저 경로·메서드 선택을 확인하고 시간 포함 조건을 확인합니다. 이후 성공 조건과 비율 표시를 따로 시험합니다. 함수는 판단 결과를 돌려주고 print는 마지막에 한 번 호출합니다. 디버그 문장을 표준 출력에 추가하면 채점 결과가 달라지므로 필요하면 표준 오류를 이용합니다. KeyError는 필요한 필드가 없는 입력인지 살피고 JSONDecodeError는 JSON 문법 문제인지 확인합니다. 이번 실습은 두 경우 INVALID를 출력하게 하여 입력 거부를 일관되게 만듭니다. 올바른 오류 처리는 실패 기록을 없애는 것과 다릅니다.

완료 기준은 문장과 테스트입니다

측정 계약을 설명한 뒤 정상·빈 자료·기간 경계·timeout·형식 오류·중복 ID 사례를 통과합니다. 실습 테스트의 기대값을 바꾸어 성공으로 만들지 않습니다. 결과를 읽을 때는 "관측 구간에 해당하는 안내판 조회 3건 중 2건 성공"처럼 분모를 말합니다. 총 이벤트 수와 조회 요청 수를 혼동하지 않는지 동료에게 검토받습니다. 함수 문법과 반환값의 자세한 내용은 더 읽기의 파이썬 함수 장으로 연결합니다. 여기서 얻을 산출물은 복사한 문법 요약이 아니라 실제로 실행되는 측정 정책 구현입니다.

따라하기

측정 대상 선택

먼저 상태와 무관하게 측정 범위를 고릅니다.

rows=[('GET','/notices'),('GET','/health'),('POST','/notices')]
for method,route in rows:
    print(method,route,method=='GET' and route=='/notices')

실행 결과

GET /notices True
GET /health False
POST /notices False

기간의 양끝 검사

0 이상 10 미만의 포함 여부를 실행하여 확인합니다.

for at in [0,9,10]:
    print(at,0 <= at < 10)

실행 결과

0 True
9 True
10 False

성공과 timeout 분리

세 요청의 상태와 timeout으로 분자와 분모를 계산합니다.

rows=[(200,False),(500,False),(200,True)]
good=sum(200 <= status < 300 and not timeout for status,timeout in rows)
print(len(rows),good,f'{100*good/len(rows):.2f}')

실행 결과

3 1 33.33

미측정 표시

빈 구간을 정상 판정과 구분합니다.

total=0
print('NO_DATA' if total==0 else 'MEASURED')

실행 결과

NO_DATA

확인 문제

실습

입력은 start·end 정수와 events 배열입니다. 각 이벤트는 id 문자열, at 정수, method·route 문자열, status 정수(100~599), timeout 불리언입니다. start 이상 end 미만의 GET /notices만 분모입니다. 200~299이고 timeout=false이면 성공입니다. total good 성공률(소수 둘째 자리) 순서로 출력합니다. 분모 0과 빈 표준 입력은 0 0 NO_DATA, 형식 오류·중복 ID·잘못된 구간은 INVALID입니다. 입력 자료를 수정하지 않고 계산 함수를 완성합니다.

모범 답안
import sys,json

def calc(d):
    if not isinstance(d,dict) or type(d.get('start')) is not int or type(d.get('end')) is not int or d['start']>=d['end'] or not isinstance(d.get('events'),list):
        raise ValueError()
    ids=set();total=good=0
    for r in d['events']:
        if not isinstance(r,dict) or not isinstance(r.get('id'),str) or not r['id'] or r['id'] in ids:
            raise ValueError()
        ids.add(r['id'])
        if type(r.get('at')) is not int or type(r.get('status')) is not int or not 100<=r['status']<=599 or type(r.get('timeout')) is not bool or not isinstance(r.get('route'),str) or not isinstance(r.get('method'),str):
            raise ValueError()
        if d['start']<=r['at']<d['end'] and r['route']=='/notices' and r['method']=='GET':
            total+=1
            good+=200<=r['status']<300 and not r['timeout']
    return '0 0 NO_DATA' if not total else f'{total} {good} {100*good/total:.2f}'
try:
    raw=sys.stdin.read().strip()
    print(calc(json.loads(raw)) if raw else '0 0 NO_DATA')
except (ValueError,TypeError,KeyError):print('INVALID')

더 읽기

면접 질문

  • 안내판 조회 성공률의 분모와 실패 조건을 어떻게 정하나요?
  • 클라이언트 timeout과 서버 200을 함께 관측하면 어떻게 설명하나요?