Devin.KR

요청과 응답의 구조

55분 안팎

학습 목표

메서드·경로·상태 코드·헤더를 구분합니다.

개념

응답 숫자를 업무 결과로 읽기

자료 보관 앱에 메모를 등록했는데 화면이 비어 있다는 문의를 받았습니다. 서버가 실행 중이라는 사실만으로 등록 성공을 설명할 수는 없습니다. 어떤 메서드로 어느 경로에 어떤 데이터를 보냈고 서버가 어떤 상태와 본문을 돌려주었는지 확인해야 합니다. 이번 레슨은 생성과 조회를 실제 루프백 HTTP 요청으로 재현하여 통신 성공과 기능 성공을 구분합니다.

모듈의 로컬 실습은 Node.js 표준 모듈과 Python 3를 사용합니다. 각 ZIP은 별도 폴더에 풀고 그 루트에서 안내 명령을 실행합니다. 본인 소유의 127.0.0.1 앱과 합성 자료만 다루며 운영 주소로 바꾸지 않습니다. 데모는 빈 포트를 자동 배정하고 유한한 요청 뒤 서버를 닫습니다. Docker가 필요한 누적 미션 전체 검사는 외부 검증 대상으로 남기고 새 기능만 local-check.sh로 확인합니다.

요청의 네 부분 구분하기

메서드는 수행하려는 동작의 의미입니다. 이 앱의 GET /documents/1은 자료 조회이고 POST /documents는 새 자료 생성입니다. 경로가 같아도 메서드가 다르면 다른 처리로 이어집니다. GET은 자료 변경을 요청하는 수단으로 설계하지 않습니다. 다만 접근 로그 같은 부수 효과가 생길 수 있으므로 서버가 아무 일도 하지 않는다는 뜻으로 설명하지 않습니다.

경로는 서버 안의 대상을 나타냅니다. 주소의 호스트와 포트는 어느 리스너로 연결할지를 정하고 /documents/1은 그 연결에서 요청할 자료를 정합니다. URL 뒤의 쿼리는 추가 조건이며 헤더나 본문과 별개입니다. 현재 앱은 pathname으로 경로를 선택합니다. ?token=값 같은 문자열은 로그에서 제외하며 실제 토큰을 실습에 넣지 않습니다.

헤더는 메시지 해석을 돕는 필드입니다. Content-Type: application/json은 보내는 내용의 표현 형식을 알려 줍니다. 이 예제의 응답에도 JSON 형식을 명시합니다. 헤더 이름의 대소문자로 다른 필드라고 판단하지 않습니다. Node의 응답 헤더 객체에서는 소문자 키로 값을 조회합니다. 헤더의 선언만으로 내용이 유효한 JSON임이 증명되지는 않습니다.

본문은 이번 생성 요청의 title과 content를 담습니다. 문자열을 따옴표로 감싸고 객체 키도 큰따옴표로 써야 JSON 파싱이 가능합니다. { 하나만 보내면 문법 오류입니다. 빈 제목을 보낸 경우는 JSON 파싱에는 성공했지만 앱 요구를 만족하지 못한 경우입니다. 두 실패가 모두 400이어도 error 값과 테스트 이름을 구분하여 원인을 읽습니다.

상태와 본문을 함께 대조하기

성공적으로 자료를 만들면 201과 id가 든 본문을 받습니다. 그 id로 다시 조회하면 200과 같은 자료가 나와야 합니다. 201만 확인하면 반환한 id가 실제 조회에 연결되는지 놓칩니다. 테스트는 생성 본문과 조회 본문을 비교하며 고정 소유자 demo-alice와 메모리 저장이라는 기존 모델을 유지합니다. 아직 로그인과 자료 소유자 인가는 구현하지 않았습니다.

존재하지 않는 /documents/999는 404입니다. 이것은 HTTP 메시지를 받지 못했다는 뜻이 아니라 앱이 해당 자료를 찾지 못했다는 응답입니다. 400은 잘못된 JSON 또는 자료 입력을 처리할 수 없다는 결과입니다. 이번 앱에서 요청 본문이 4096바이트를 넘으면 413을 반환합니다. 바이트 한도와 문자열 글자 수가 같다고 가정하지 않으며 테스트는 ASCII 입력으로 경계를 분명히 합니다.

상태 코드 계열은 진단의 시작점이며 모든 실패의 세부 원인을 말해 주지는 않습니다. 4xx라고 공격으로 단정하거나 2xx라고 보안이 검증됐다고 판단하지 않습니다. 특히 현재 라우터는 지원하지 않는 경로·메서드를 404로 처리합니다. 실무 API의 405 정책과 이 학습 앱의 구현을 구분합니다. HTTP 상태의 의미는 RFC 9110 상태 코드 절에서 확인할 수 있습니다.

실제 소켓 관찰과 함수 시험

앞 모듈의 createApp 테스트는 함수에 메서드와 경로를 전달했습니다. 이번 client.cjs는 http.request로 TCP 소켓을 통해 요청합니다. 함수 테스트가 성공해도 리스너 바인딩, JSON 파싱, 응답 헤더는 별도 오류가 생길 수 있습니다. 테스트는 127.0.0.1로만 연결하며 서버 주소의 실제 배정 포트를 읽어 사용합니다. 포트 번호를 운영 포트로 바꾸지 않습니다.

withServer는 테스트를 위해 서버를 열고 작업이 끝나면 finally에서 닫습니다. 테스트가 실패해도 정상 정리 경로가 실행됩니다. 요청에는 시간 제한을 두어 응답이 없는 경우 계속 기다리지 않게 합니다. 상시 실행 명령을 다른 터미널에서 띄울 필요가 없습니다. 이 구조로 생성·조회·결측·형식 오류를 반복해도 남는 서버 프로세스 없이 관찰할 수 있습니다.

데모의 출력은 메서드, 경로, 상태, 응답의 x-request-id 순서입니다. 날짜와 임의 포트처럼 실행마다 달라지는 값은 이 요약에 넣지 않습니다. ID는 데모의 주입 함수로 demo-1부터 만들며 실제 기본 구현은 임의 UUID를 사용합니다. 고정 출력의 재현성과 실제 요청 식별 정책을 나누면 예제를 검토하면서도 ID 충돌 방지의 필요성을 놓치지 않습니다.

실패한 검사에서 고칠 위치 찾기

starter는 자료 생성과 조회 코드는 있지만 응답의 ID를 missing으로 고정합니다. bash check.sh에서 expected demo 또는 r 계열 ID와 actual missing의 차이를 읽습니다. server.cjs의 응답 헤더와 로그 이벤트에 서버가 요청 시작 때 만든 requestId를 연결합니다. 테스트 기대값을 missing으로 바꾸면 관찰 계약을 없애는 것이므로 구현을 고칩니다.

ECONNREFUSED는 해당 주소와 포트에서 연결을 받지 못했다는 단서입니다. JSON의 error: not_found와 구분합니다. EADDRINUSE는 요청 내용 오류가 아니라 리스닝 포트 충돌입니다. 제공 데모는 포트 0을 사용하여 충돌 가능성을 줄입니다. SyntaxError는 자바스크립트 문법 오류인지 JSON 파싱 오류인지 발생 위치를 확인하고, 이미 400으로 처리한 입력을 환경 실패로 기록하지 않습니다.

curl로 상태를 조사할 때 응답 상태와 셸 종료 코드는 별도로 봅니다. 일반 호출은 404 메시지를 정상 수신해 종료 코드가 0일 수 있습니다. 자동 점검은 상태를 직접 비교하거나 적절한 실패 옵션을 사용해야 합니다. 옵션 목록과 리다이렉트 세부 동작은 더 읽기로 보내며 여기서는 제공 클라이언트의 status와 body를 직접 비교하는 방법에 집중합니다.

작은 재현 자료 남기기

실습을 마치면 POST의 최소 입력, 반환 id, 같은 id의 GET 결과, 없는 id의 결과를 네 줄로 정리합니다. 민감한 본문 전체를 복사할 필요는 없습니다. 상태와 ID만으로 재현 순서를 연결하고 합성 자료라는 표시를 붙입니다. HTTP 평문으로 루프백을 호출했으므로 HTTPS 신뢰 검증이나 외부 방화벽 점검을 완료했다는 문장은 쓰지 않습니다.

합격 기준은 check.sh 종료 코드 0과 두 HTTP 테스트 묶음 통과입니다. 첫 묶음은 여섯 응답의 상태·헤더·로그 일치를, 둘째는 병렬 요청 여덟 개의 ID 차이와 로그의 최소 필드를 확인합니다. 각 테스트 안의 비교 개수와 테스트 묶음 개수는 다른 수치입니다. 통과 요약만 제출하기보다 실패했던 ID 연결과 수정 후 동일 요청 대조 근거를 설명합니다.

따라하기

URL의 연결 대상과 경로 나누기

합성 URL을 분해할 뿐 네트워크 연결은 하지 않습니다.

from urllib.parse import urlsplit
u = urlsplit('http://127.0.0.1:8765/documents/1?view=brief')
print(u.scheme, u.hostname, u.port)
print(u.path)
print(u.query)

실행 결과

http 127.0.0.1 8765
/documents/1
view=brief

최소 JSON 입력 확인

본문의 자료 조건을 파싱 후 확인합니다.

import json
for text in ['{"title":"합성 메모","content":"demo"}', '{"title":" ","content":"demo"}']:
    item = json.loads(text)
    print('valid_title=' + str(bool(item['title'].strip())))

실행 결과

valid_title=True
valid_title=False

실제 HTTP 네 요청 관찰

solution ZIP 루트에서 실행합니다. 데모는 요청이 끝나면 리스너를 닫습니다. 출력의 마지막 값은 응답 헤더의 ID입니다. 샌드박스의 listen EPERM으로 이 단계는 실행 검증 대기이며 출력은 비워 두었습니다. 밖에서 정상 종료와 201·200·404·400, 네 ID 연결을 확인합니다.

node http-demo.cjs

starter 수정과 전체 비교

starter의 server.cjs에서 missing 두 곳을 요청별 requestId에 연결하고 실행합니다. 2개 테스트 묶음 통과와 종료 코드 0을 확인합니다. 상태와 식별자가 함께 맞아야 완료입니다.

bash check.sh

확인 문제

실습

server.cjs의 고정 missing 값을 요청 시작 때 만든 requestId로 바꿉니다. 응답 헤더와 로그 이벤트에 같은 값을 연결합니다. 기존 생성·조회 함수와 테스트 기대값은 유지합니다. 성공·자료 없음·형식 오류·본문 한도·병렬 요청 및 로그 최소화 검사를 모두 통과합니다.

루프백 listen 권한이 없는 샌드박스에서는 external 대기입니다. node --test handler.test.cjs로 메모리 스트림의 요청 처리 검사는 실행할 수 있지만 실제 HTTP 소켓 통과로 기록하지 않습니다.

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

실행 명령

bash check.sh

기대 결과

2개 HTTP 테스트 묶음 통과, 종료 코드 0

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

더 읽기

면접 질문

  • HTTP 응답을 받았다는 사실과 자료 생성 성공을 어떻게 구분합니까?