Devin.KR

요청과 응답 관찰

160분 안팎

학습 목표

준비된 요청 기록의 메서드·경로·상태·본문을 읽습니다.

개념

신청이 안 된다는 말에서 비교 가능한 기록을 만듭니다

“어제는 됐는데 오늘은 안 됩니다”라는 설명만으로는 개발자가 같은 요청을 찾기 어렵습니다. 행사 선택이 빠졌는지, 존재하지 않는 신청을 조회했는지, 저장 시도가 실패했는지 서로 다른 후보가 남습니다. 이번 레슨에서는 제공된 요청 여섯 개의 메서드, 경로, 응답 상태, 본문, 저장 행 수를 비교합니다. 호출 코드를 새로 구현하는 대신 관찰 기록을 정확히 남겨 기술 담당자가 확인할 수 있는 질문으로 바꾸는 연습입니다.

메서드와 경로를 함께 읽습니다

POST /applications는 이번 계약에서 신청을 만드는 요청이고 GET /applications/A001은 이미 만들어진 신청 하나를 읽는 요청입니다. 메서드만 쓰면 어떤 자원을 다루는지 알 수 없고 경로만 쓰면 생성인지 조회인지 구별하기 어렵습니다. 요청 본문은 보낼 행사 값처럼 입력 데이터이며 응답 본문은 신청 ID나 오류 코드처럼 처리 결과입니다. 요청 eventId가 EV01이었다는 사실과 응답 status가 PENDING이었다는 사실을 서로 다른 열에 기록합니다.

HTTP 상태는 응답 전체의 처리 의미를 알리는 숫자이고 본문의 status는 우리 업무의 신청 상태입니다. R01은 HTTP 201과 업무 상태 PENDING을 동시에 가집니다. 새 신청은 만들어졌지만 행사 승인은 대기라는 뜻입니다. ‘상태가 성공이니 승인’이라고 요약하지 않습니다. 오류 본문의 error.code=INVALID_EVENT는 화면과 지원팀이 같은 오류 종류를 구별하게 하는 약속입니다. 사람이 읽는 문구가 바뀌어도 오류 코드와 필드 의미를 유지할지 계약으로 논의합니다.

헤더는 본문과 역할이 다릅니다

모의 응답의 Content-Type은 application/json이며 본문 구조를 JSON으로 읽는다는 뜻입니다. X-Request-ID는 R번호를 돌려줍니다. 신청 요청에는 같은 제출 의도를 재생할 때 사용할 키 K01도 있습니다. 이 키는 모의 API의 key 인자로 전달되며 실제 HTTP 구현에서는 Idempotency-Key 헤더로 대응시킬 제안입니다. 헤더 이름이 있다는 것과 실제 네트워크에 그 헤더가 전송됐다는 것은 다릅니다. 이 패키지는 HTTP 형태의 요청·응답 객체를 모델링하고 소켓을 열지 않습니다.

bash replay.sh는 Python 모의 API에 요청을 순서대로 전달하고 한 줄씩 결과를 출력합니다. 같은 실행 안에서는 메모리 저장소를 공유하므로 첫 신청 이후 조회와 재전송을 비교할 수 있습니다. 스크립트를 다시 실행하면 빈 저장소에서 시작합니다. 따라서 계속 실행해 행이 누적되는 운영 환경을 재현하지 않습니다. DNS, TLS, 브라우저 네트워크 탭, 실제 DB 영속성도 이 출력으로 확인할 수 없습니다. 관찰 범위를 ‘모의 요청 재생’이라고 적어 과장하지 않습니다.

행 수와 요청 전후 차이를 함께 봅니다

R01 뒤 rows=1이면 모의 저장소에 신청 한 행이 있다는 뜻입니다. R02는 같은 신청을 읽고 rows=1을 유지합니다. 조회에서 한 행을 반환했다는 것과 한 행을 새로 만들었다는 것은 다릅니다. 미션에는 rowDelta를 쓰며 R01은 1, R02는 0입니다. 여섯 요청에서 rows가 계속 1이어도 모든 요청이 저장 성공이라는 뜻은 아닙니다. 각 요청 바로 전과 바로 후의 저장 결과를 비교해야 새 기록 여부를 구별할 수 있습니다.

R03은 eventId가 빈 문자열이라 HTTP 422와 INVALID_EVENT를 반환하며 행을 늘리지 않습니다. R04는 유효한 행사와 새 키를 보내지만 fail=True로 모의 저장 실패를 주입해서 503과 STORE_UNAVAILABLE을 반환합니다. 이 실패는 저장 전에 발생하도록 작성되어 행을 늘리지 않습니다. 실제 서비스의 모든 503이 저장되지 않았음을 의미한다고 일반화하지 않습니다. 모의 코드가 만든 실패 위치를 알고 있을 때만 이번 요청의 행 변화 0을 기록합니다.

재전송을 새로운 신청과 구분합니다

R05는 R01과 같은 행사와 같은 키 K01을 다시 보냅니다. 응답은 HTTP 200이고 applicationId=A001을 그대로 돌려주며 rows=1입니다. R05는 다른 요청 ID지만 같은 신청 의도입니다. 이를 신규 신청 두 건으로 집계하면 중복 방지 동작을 실패로 오해할 수 있습니다. 신청 수를 셀 때 요청 횟수와 생성된 신청 ID 수를 나누어 기록합니다. 같은 키로 다른 내용을 보내는 충돌은 뒤 계약에서 409로 정의하며 이번 여섯 줄에는 그 충돌 요청이 없습니다.

R06은 A999 조회로 404와 NOT_FOUND를 반환합니다. A001의 저장이 실패했다는 증거가 아니라 이번 조회 대상이 모의 저장소에 없다는 결과입니다. 요청 경로의 신청 ID를 먼저 읽고 앞 생성 응답의 ID와 비교합니다. 지원 기록에 A001과 A999를 섞으면 잘못된 연결로 저장 문제를 만들 수 있습니다. 조회 실패의 다음 질문은 번호 출처와 경로를 확인하는 것입니다. 인증과 접근 범위는 이 모형에 없으므로 실제 제품에서는 추가 확인이 필요합니다.

시작본의 빈 관찰을 채웁니다

observations.json에는 R01과 R02가 완성되어 있습니다. R03부터 R06까지 method, path, status, rows, result를 채웁니다. status와 rows는 숫자로 쓰고 result는 정상 응답이면 PENDING, 오류이면 error.code 값을 씁니다. 출력의 JSON 안에서 status를 읽었다고 해서 observations의 HTTP status에 PENDING을 넣지 않습니다. 키 이름은 같아도 위치가 다르므로 어느 객체의 값을 기록했는지 먼저 찾습니다. 원본 요청과 응답을 모두 보여 주는 mock_api.py도 비교 자료입니다.

python3 check.py는 여섯 관찰을 다시 재생한 값과 비교합니다. PASS R01과 PASS R02는 이미 채운 관찰의 일치이며 FAIL R03은 아직 입력 오류 기록이 다르다는 뜻입니다. 검사기를 수정해 실패를 없애지 않고 해당 요청을 출력에서 찾아 값과 타입을 고칩니다. SyntaxError나 JSON 형식 오류가 보이면 쉼표, 따옴표, 중괄호를 확인합니다. 파일이 없다는 안내는 현재 폴더와 observations.json 이름부터 확인합니다. 네 기록을 완성하면 실패 수가 0이 됩니다.

기록에서 개발자 질문을 도출합니다

각 사례 아래에 확인된 결과와 다음 확인을 한 문장씩 적습니다. R04는 ‘모의 저장 전 실패에서 행 추가 없음’이 확인이고 ‘실제 저장 실패에서도 성공 응답이 나가지 않는가’는 개발자 질문입니다. R05는 같은 키의 순차 재생만 확인했으므로 동시에 두 요청이 오면 한 신청만 생성되는지는 미확인입니다. 내부 파일 경로나 예외 전문을 사용자 안내에 붙이기보다 오류 코드와 요청 ID로 내부 조사와 사용자 설명을 연결합니다.

기획자가 모든 헤더를 외울 필요는 없습니다. 요청을 찾는 데 필요한 식별자, 입력을 해석하는 데 필요한 형식, 결과를 구별하는 상태와 본문을 읽을 수 있으면 협업 기록을 시작할 수 있습니다. 동료가 같은 명령을 실행해 여섯 관찰을 다시 얻는지 확인하고, 운영 기록이라는 표현은 사용하지 않습니다. 모의 결과를 출발점으로 실제 제품에서 더 관찰해야 할 항목을 표시하는 것이 이 실습의 완료 기준입니다.

따라하기

시작 관찰을 확인합니다

로컬 관찰 starter를 푼 루트에서 실행합니다. 완성된 두 사례만 PASS이고 네 사례는 FAIL인지 확인합니다. 오류가 없도록 검사기를 수정하지 않습니다.

python3 check.py

실행 결과

PASS R01: 요청·응답·저장 행 비교
PASS R02: 요청·응답·저장 행 비교
FAIL R03: 요청·응답·저장 행 비교
FAIL R04: 요청·응답·저장 행 비교
FAIL R05: 요청·응답·저장 행 비교
FAIL R06: 요청·응답·저장 행 비교
검사 실패: 4

여섯 요청을 재생합니다

같은 폴더에서 실행합니다. 아래 출력은 실제 모의 재생 결과입니다. 본문의 status와 줄의 HTTP 숫자를 다른 열로 기록합니다.

bash replay.sh

실행 결과

R01 POST /applications 201 {"applicationId":"A001","eventId":"EV01","status":"PENDING"} rows=1
R02 GET /applications/A001 200 {"applicationId":"A001","eventId":"EV01","status":"PENDING"} rows=1
R03 POST /applications 422 {"error":{"code":"INVALID_EVENT","field":"eventId"}} rows=1
R04 POST /applications 503 {"error":{"code":"STORE_UNAVAILABLE"}} rows=1
R05 POST /applications 200 {"applicationId":"A001","eventId":"EV01","status":"PENDING"} rows=1
R06 GET /applications/A999 404 {"error":{"code":"NOT_FOUND"}} rows=1

빈 네 기록을 채웁니다

observations.json의 R03~R06에 method, path, status, rows, result를 채웁니다. status와 rows는 숫자로 씁니다. 재생 코드는 요청 입력과 응답 헤더도 담으므로 mock_api.py와 비교합니다.

수정한 관찰을 확인합니다

python3 check.py를 다시 실행해 여섯 PASS와 실패 0을 확인합니다. 아래 명령은 solution에서 직접 실행한 결과입니다. 성공 후에도 실제 HTTP 전송이나 운영 DB를 검증한 것으로 표시하지 않습니다.

python3 check.py

실행 결과

PASS R01: 요청·응답·저장 행 비교
PASS R02: 요청·응답·저장 행 비교
PASS R03: 요청·응답·저장 행 비교
PASS R04: 요청·응답·저장 행 비교
PASS R05: 요청·응답·저장 행 비교
PASS R06: 요청·응답·저장 행 비교
검사 실패: 0

확인 문제

실습

starter를 풀고 bash replay.sh를 실행합니다. observations.json의 R03~R06 빈 객체를 method·path·status·rows·result로 채웁니다. R01·R02는 그대로 둡니다. python3 check.py로 여섯 사례를 확인합니다. 숫자 상태는 HTTP 응답, result는 업무 상태 또는 오류 코드입니다. 네 오류·중복·없는 자원 사례를 구별하고 모의 결과를 운영 기록이라고 표현하지 않습니다.

시작 코드·테스트 내려받기

실행 명령

python3 check.py

기대 결과

여섯 요청 PASS, 검사 실패: 0, 종료 코드 0

모범 답안모범 답안 내려받기

더 읽기

면접 질문

  • 화면에서 저장 버튼을 눌렀을 때의 데이터 흐름을 설명해 주시면 됩니다.