요청·응답과 오류
70분 안팎
학습 목표
설정 조회 요청에서 알 수 없는 키와 정상 응답을 판정합니다.
개념
조회 성공과 오류를 같은 응답 틀에 넣습니다
속도 상한을 조회하는 관제 프로그램은 값만 받으면 그 값이 유효한 설정인지 알기 어렵습니다. 없는 키를 0으로 돌려주면 실제 상한 0과 키 오류가 같아집니다. 이번 레슨은 요청을 검사하고 정상 응답, 알 수 없는 키, 잘못된 요청을 서로 다른 결과로 만듭니다. 목표는 성공 경로뿐 아니라 오류를 받은 클라이언트도 다음 행동을 결정할 수 있게 작성하는 것입니다.
설정 저장소는 max_speed_mps=0.5, frame_id=map, tick_s=0.05 세 항목으로 고정합니다. 첫 값의 단위는 m/s, 마지막 값의 단위는 s입니다. 브라우저 과제는 이 표를 조회하는 순수 함수이며 파일 변경이나 원격 호출이 없습니다. ROS 파라미터 서버와 이름 해석까지 구현한 과제가 아니라 설정 조회의 요청·응답 계약 연습입니다.
입력은 JSON 한 행에 요청 하나입니다
요청은 id와 key를 가진 JSON 객체입니다. 둘 다 비어 있지 않은 문자열이어야 하며 앞뒤 공백을 자동으로 제거하지 않습니다. id=r1, key=tick_s는 정상 요청이고 id=true는 형식 오류입니다. 추가 필드는 무시합니다. null, 배열, 숫자처럼 객체가 아닌 최상위 값도 BAD_REQUEST로 반환합니다. 빈 입력 파일은 요청이 없으므로 아무것도 출력하지 않습니다.
각 행을 따로 읽으므로 행 두 개를 보내면 응답도 두 행입니다. 빈 행은 요청이 없는 파일과 다릅니다. 빈 행의 JSON 파싱은 실패하여 BAD_JSON을 출력합니다. 하나의 JSON을 여러 줄에 나누어 쓰는 입력은 이 프로토콜에 맞지 않습니다. 채점할 때는 줄바꿈이 요청의 경계를 결정한다는 점을 먼저 확인합니다.
검사 순서로 첫 오류를 정합니다
첫째 json.loads로 행을 읽습니다. 실패하면 BAD_JSON이며 id는 null로 둡니다. 둘째 객체인지, id와 key가 올바른 문자열인지 확인합니다. 이 단계가 실패하면 BAD_REQUEST입니다. 파싱된 객체에 id가 있으면 그 값을 응답에 보존하여 잘못 보낸 식별자를 추적하게 합니다. 셋째 유효한 key가 저장소에 있는지 확인합니다. 없으면 UNKNOWN_KEY입니다.
UNKNOWN_KEY는 요청 형식이 정상일 때만 나옵니다. id가 없고 key가 speed인 요청을 UNKNOWN_KEY로 먼저 판정하면 호출자가 빠진 id를 고치지 못합니다. 코드 순서가 곧 오류 우선순위이므로 테스트에도 두 오류가 같이 있는 사례를 넣습니다. 예외를 전부 숨기고 빈 딕셔너리를 출력하는 방식은 어떤 경계에서 실패했는지 잃게 합니다.
응답 필드의 의미를 고정합니다
응답은 id, ok, code, value 네 필드입니다. 성공이면 ok=true, code=OK이고 value에 설정값을 넣습니다. 나머지 경우는 ok=false와 해당 code를 넣고 value=null로 둡니다. 0이나 빈 문자열을 오류 표시로 쓰지 않습니다. JSON 출력에서 Python의 True·None을 직접 문자열로 찍으면 true·null과 달라지므로 json.dumps를 사용합니다.
문자열 frame_id 응답과 숫자 tick_s 응답은 value의 타입이 다릅니다. 조회 결과에 float 변환을 일괄 적용하면 map을 숫자로 바꾸다 실패합니다. 설정마다 의미와 타입을 유지하고 표시 단계에서 필요할 때 형식을 바꿉니다. 소수점 자릿수를 맞춘 화면 표시와 JSON의 숫자 표현을 같은 요구사항으로 혼동하지 않습니다.
상관관계를 값의 정확성보다 먼저 확인합니다
id는 여러 조회의 응답을 연결하는 표식입니다. r1과 r2를 보내 두 응답의 순서가 바뀌어도 각 id로 매칭할 수 있어야 합니다. 브라우저 모델은 순서대로 처리하지만 설계는 응답 위치에만 의존하지 않습니다. 조회에 응답 id가 다르면 값이 맞아 보여도 그 요청의 답으로 사용하지 않습니다. 작업 목표 id와 서비스 요청 id는 용도가 달라 이름 영역도 구분합니다.
조회는 읽기만 하므로 같은 요청을 다시 읽어도 설정을 바꾸지 않습니다. 그러나 이 성질을 모든 서비스로 일반화하면 안 됩니다. 구동기 초기화나 설정 변경은 부작용이 있을 수 있어 대기 기한을 넘긴 뒤 재시도할 때 중복 적용을 고려해야 합니다. 이번 과제의 id는 상관관계용이며 중복 실행 방지를 보장하는 영속 저장소는 아닙니다.
대기 기한과 업무 오류를 다르게 읽습니다
실제 통신에서 클라이언트가 기다리다 기한을 넘기면 요청이 서버에 도착하지 않았다고 단정할 수 없습니다. 서버가 처리했으나 응답이 늦을 수도 있습니다. 호출 시각·기한·서버 로그를 비교하고 부작용 여부에 맞는 재시도 정책을 정합니다. 이번 동기 함수에는 네트워크 대기가 없어서 TIMEOUT 응답을 임의로 출력하지 않습니다.
BAD_JSON은 문자열 입력 경계, BAD_REQUEST는 필드 계약 경계, UNKNOWN_KEY는 업무 조회 경계에 해당합니다. 세 오류가 어디서 생겼는지 알면 수정 대상도 좁아집니다. frame_id를 frame으로 보낸 경우에는 JSON 문법이 아니라 key 이름을 고칩니다. 코드만 보고 서버 재시작부터 시도하는 습관을 줄입니다.
작은 테스트로 클라이언트 해석을 검증합니다
정상 숫자 조회와 문자열 조회를 각각 넣습니다. 이어 없는 키, 누락 id, 불리언 id, 깨진 JSON, 빈 입력, 여러 요청을 확인합니다. 기대 출력은 키 정렬과 공백 제거를 적용한 JSON 행입니다. 이 고정 형식은 사람이 보기 위한 화면이 아니라 입력·출력 테스트를 명확히 하기 위한 결정입니다. 실습 프롬프트의 형식을 그대로 따릅니다.
starter는 모든 요청을 UNKNOWN_KEY로 반환하여 일부 오류 사례만 맞습니다. respond 함수를 완성하고 파싱 오류 분기를 분리합니다. 정상 조회까지 실패한다면 저장소 확인보다 앞의 형식 검사부터 읽습니다. SyntaxError는 업무 오류 응답이 아니라 프로그램이 시작하지 못했다는 뜻이므로 들여쓰기와 따옴표를 먼저 고칩니다.
보관과 표시를 분리해 확장합니다
설정 응답을 받은 뒤 값의 범위를 검증하고 화면에 “속도 상한 0.5 m/s”로 표시하는 것은 클라이언트 책임입니다. 서버는 value=0.5를 반환하고 단위는 계약표에서 정의합니다. 이후 동적 설정을 추가하면 설정 변경 버전과 조회 시점을 붙일 수 있지만 이번 과제에서는 고정 저장소를 유지해 오류 처리에 집중합니다.
실제 ROS 파라미터 읽기·변경과 선언 규칙은 서재 장으로 연결합니다. 이 연습에서 만든 사용자 정의 응답 틀을 ROS 내장 인터페이스의 필드 이름이라고 설명하지 않습니다. 후속 구현에서는 사용할 타입 정의를 직접 확인하고 로컬의 id·code가 어디에 대응하는지 결정해야 합니다.
완료 기준을 리뷰합니다
동료가 없는 키를 보내면 null과 UNKNOWN_KEY로 판단할 수 있는지, 성공값이 문자열이어도 보존되는지 확인합니다. 응답 하나만 맞는 것으로 끝내지 않고 같은 입력에서 항상 같은 한 행이 출력되는지 비교합니다. 디버그 문장이 표준 출력에 섞이면 올바른 응답도 채점에서 틀릴 수 있으므로 필요하면 표준 오류를 사용합니다.
최종 제출에는 오류별 입력 예와 응답, 클라이언트의 다음 행동을 적습니다. BAD_REQUEST에서는 호출자 필드를 고치고, UNKNOWN_KEY에서는 허용 키를 확인하며, 실제 무응답에서는 통신 증거를 찾습니다. 짧은 조회를 액션으로 만들지 않아도 이런 책임을 분명히 할 수 있다는 점을 설명하면 레슨 목표를 충족합니다.
따라하기
응답을 JSON으로 출력합니다
문자열과 null이 실제 JSON 표기로 변환되는지 확인합니다.
import json
r={"id":"r1","ok":False,"code":"UNKNOWN_KEY","value":None}
print(json.dumps(r,sort_keys=True,separators=(",",":")))실행 결과
{"code":"UNKNOWN_KEY","id":"r1","ok":false,"value":null}
필드 검사를 키 조회보다 앞에 둡니다
같은 키라도 요청 id의 형식에 따라 결과가 달라지는지 비교합니다.
for r in ({"key":"speed"},{"id":"r1","key":"speed"}):
code="BAD_REQUEST" if type(r.get("id")) is not str else "UNKNOWN_KEY"
print(code)실행 결과
BAD_REQUEST UNKNOWN_KEY
저장소의 타입을 보존합니다
세 설정을 그대로 조회합니다. 문자열 설정에 float 변환을 적용하지 않습니다.
import json
config={"max_speed_mps":0.5,"frame_id":"map","tick_s":0.05}
for key,value in config.items():
print(key+"="+json.dumps(value))실행 결과
max_speed_mps=0.5 frame_id="map" tick_s=0.05
브라우저에서 경계 입력을 확인합니다
아래 실습의 respond TODO를 구현합니다. 정상·없는 키·불리언 id·누락 id·깨진 JSON을 순서대로 넣고 각 code와 value를 비교합니다. 여러 행에서도 응답 id와 행 수가 유지되는지 확인합니다.
확인 문제
실습
JSONL 요청을 행별로 판정합니다. id와 key는 공백 제거 없이 비어 있지 않은 문자열이며 추가 필드는 무시합니다. 저장소는 max_speed_mps=0.5, frame_id="map", tick_s=0.05입니다. 비표준 NaN·Infinity 토큰도 JSON 오류입니다. JSON 오류→BAD_JSON(id=null), 객체·id·key 오류→BAD_REQUEST(객체면 원래 id, 없으면 null), 모르는 key→UNKNOWN_KEY 순서입니다. 응답 필드는 id, ok, code, value이며 오류면 ok=false와 value=null입니다. 성공이면 OK와 원래 타입의 value입니다. json.dumps의 sort_keys=True, separators=(",",":")로 한 행을 출력합니다. 빈 파일은 출력 없음, 빈 행은 BAD_JSON입니다.
모범 답안
import sys, json
CONFIG={"max_speed_mps":0.5,"frame_id":"map","tick_s":0.05}
def respond(r):
rid=r.get("id") if isinstance(r,dict) else None
if not isinstance(r,dict) or type(rid) is not str or not rid or type(r.get("key")) is not str or not r["key"]:
return dict(id=rid,ok=False,code="BAD_REQUEST",value=None)
key=r["key"]
return dict(id=rid,ok=key in CONFIG,code="OK" if key in CONFIG else "UNKNOWN_KEY",value=CONFIG.get(key))
for line in sys.stdin:
try: response=respond(json.loads(line, parse_constant=lambda value: (_ for _ in ()).throw(ValueError(value))))
except (json.JSONDecodeError, ValueError): response=dict(id=None,ok=False,code="BAD_JSON",value=None)
print(json.dumps(response,ensure_ascii=False,sort_keys=True,separators=(",",":")))
더 읽기
면접 질문
- ROS 2 토픽과 서비스, 액션의 선택 기준을 설명해 주시면 됩니다.