Devin.KR

요청과 응답 계약

65분 안팎

학습 목표

DNS·TCP·TLS·HTTP의 역할과 메서드·상태 코드를 연결합니다.

개념

왜 요청 계약부터 정하나요

동아리 담당자가 도서 목록을 웹에서 보고 싶어 합니다. 앞 모듈의 CLI는 같은 컴퓨터의 파일을 직접 읽지만 웹 클라이언트는 파일 경로나 Java 객체를 알 수 없습니다. 서버가 받아들일 요청과 돌려줄 응답을 정해야 담당자가 새 도서를 추가한 뒤 제대로 저장되었는지 판단할 수 있습니다. 이번 계약은 GET /books, GET /books/{id}, POST /books입니다. 목록의 빈 상태와 없는 도서 한 권의 상태를 구별하는 것이 출발점입니다.

이름에서 요청까지의 역할을 나눕니다

주소가 https://club.example/books라면 호스트 이름을 연결 대상 주소로 해석하는 역할은 DNS가 맡습니다. 일반적인 HTTP/1.1·HTTP/2 HTTPS 요청에서는 TCP로 연결하고 TLS로 전송 내용을 암호화하며 인증서를 검증한 뒤 HTTP 메시지를 주고받습니다. HTTP/3는 QUIC를 사용하므로 모든 HTTP가 TCP 위에 있다는 설명은 맞지 않습니다. 이 레슨의 로컬 http://127.0.0.1 주소는 DNS 이름 해석과 TLS 인증서 검증을 실습하지 않습니다.

DNS 해석에 실패한 경우에는 이 앱의 컨트롤러가 응답한 404가 없습니다. TCP 연결 거부는 해당 주소와 포트에 연결하지 못했다는 뜻이며 GET 경로가 틀렸다는 직접 증거는 아닙니다. 인증서 검증 오류도 HTTP 상태와 구분합니다. HTTP 404를 받았다면 어떤 HTTP 서버가 응답했다는 증거는 생겼지만 그 서버가 의도한 앱인지까지 확인한 것은 아닙니다. 실패 단계와 응답을 분리해 기록하면 조사할 위치가 좁아집니다.

메서드는 행위의 의미를 전달합니다

GET /books는 현재 목록을 조회합니다. GET /books/7은 ID 7을 조회합니다. GET을 호출할 때마다 도서를 만드는 구현은 조회의 안전한 의미를 어깁니다. 로그가 남는다는 이유로 GET의 조회 의미가 바뀌는 것은 아닙니다. POST /books는 요청 본문을 처리해 새 도서를 등록하도록 정합니다. 같은 POST를 두 번 보낼 때 두 권이 만들어질 수 있으며 이번 단계에는 재시도 중복을 막는 요청 키가 없습니다.

경로의 books는 목록 자원이고 끝의 정수는 저장소 키입니다. 앞 모듈에서 ID를 Book 내부 필드 대신 저장소 키로 둔 선택을 유지합니다. 제목은 바뀔 수 있고 같은 제목이 여러 권일 수 있으므로 제목을 상세 경로의 식별자로 쓰지 않습니다. 목록 크기가 1이라고 그 도서 ID가 1이라는 뜻도 아닙니다. 기존 파일에는 7이나 10 같은 키가 들어 있을 수 있습니다.

상태·헤더·본문을 함께 읽습니다

빈 파일의 GET /books는 200과 JSON 배열 []입니다. 목록 자원이 존재하되 결과가 비어 있다는 의미입니다. GET /books/99에서 키 99가 없다면 404입니다. 새 도서를 만든 POST는 201로 응답하고 Location 헤더에 /books/새ID를 넣으며 본문에 id, title, borrowed를 제공합니다. 201은 새 자원 생성 성공이고 Location은 이어 조회할 경로입니다. 상태 숫자만 보고 저장된 제목이 올바르다고 판단하지 않습니다.

Content-Type은 메시지 본문의 형식을 설명합니다. JSON 문자열처럼 보이는 본문을 보내도 요청 Content-Type이 다른 값이면 서버가 JSON으로 읽는 계약을 만족하지 못합니다. 이번 실습에서 요청에 application/json을 지정하는 이유입니다. 응답의 Content-Type은 서버가 보내는 본문의 형식이고 요청 헤더와 별도로 봅니다. Accept는 클라이언트가 선호하는 응답 형식이므로 요청 본문 형식을 지정하는 Content-Type의 대체물이 아닙니다.

curl을 관찰 도구로 씁니다

request.sh는 기본 URL과 list, create, detail, missing 동작을 받습니다. -sS는 진행 표시를 줄이면서 전송 오류는 보여 주고 -i는 응답 헤더와 본문을 함께 보여 줍니다. -w의 http_code는 응답 상태를 마지막 STATUS 행으로 기록합니다. --data는 본문을 전송하며 이 예제에서는 POST를 선택합니다. 제목을 JSON에 문자열로 이어 붙이지 않고 Python json.dumps로 직렬화해 따옴표와 역슬래시가 있는 제목도 JSON 문법을 지키게 합니다.

-i와 -I는 다릅니다. -I는 HEAD 요청을 하므로 GET의 배열 본문을 읽는 관찰에 맞지 않습니다. -f를 넣지 않은 curl은 404나 400 응답을 정상적으로 수신해 종료 상태 0을 반환할 수 있습니다. 셸의 성공과 HTTP 업무 성공은 별개입니다. 이 레슨은 실패 응답의 본문도 관찰하므로 -f를 넣지 않고 check.py가 상태 숫자를 직접 비교합니다. 배포 성공 여부를 판단하는 스크립트에서는 별도의 상태 검사 정책이 필요합니다.

실습의 정상·경계·실패 경로

모의 API는 빈 목록, 유효 제목 추가, 추가한 도서 상세 조회, 없는 ID, 공백 제목이라는 다섯 요청을 검증합니다. create의 상태와 Location, 응답 JSON이 모두 맞아야 통과합니다. 공백 제목을 거부한 뒤 목록이 한 권인지를 추가로 확인해 실패 요청이 상태를 바꾸지 않았는지도 봅니다. starter에서는 create의 Content-Type 요청 헤더가 빠져 일부 검사가 실패합니다. 헤더를 수정하고 서버 fixture나 기대값은 바꾸지 않습니다.

check.py는 자체 Python HTTP 서버를 127.0.0.1의 임의 포트에서 열어 충돌 가능성을 줄입니다. 실제 Spring 저장소나 운영 서비스를 호출하지 않습니다. 모의 서버의 오류 code는 관찰용 fixture이고 다음 레슨의 Spring 오류 본문을 보장하지 않습니다. 완료나 테스트 실패 뒤에는 finally에서 서버를 shutdown하고 스레드를 join한 뒤 소켓을 닫습니다. 서버 프로세스를 따로 남기는 실행 구조가 아닙니다.

오류 메시지를 증거로 읽습니다

curl의 Failed to connect가 보이면 URL의 포트와 서버 실행 여부부터 확인합니다. HTTP 415가 보이면 전송은 이뤄졌으므로 요청 Content-Type과 서버의 허용 형식을 비교합니다. 400이 보이면 JSON 문법과 필수 title을 각각 확인합니다. Location 누락은 생성 상태가 맞아도 다음 상세 조회를 자동화하기 어렵다는 뜻입니다. response 본문만 비교해서 이 헤더 오류를 놓치지 않습니다.

현재 작성 샌드박스에서는 로컬 포트 bind가 Operation not permitted로 거부되었습니다. 따라서 이 실습은 external 검증 대기이며 따라하기에 실행 결과를 만들어 적지 않습니다. 외부 환경에서는 bash check.sh로 starter 일부 실패와 solution 다섯 검사 통과를 확인합니다. 포트 열기가 가능한 본인 컴퓨터에서 같은 명령으로 수행할 수 있습니다. 더 많은 curl 옵션은 더 읽기에서 확인하고 이번에는 도서 요청 계약에 필요한 관찰만 사용합니다.

완료 기준을 설명합니다

동료에게 빈 배열과 없는 상세가 다른 이유, Content-Type을 어디에 넣는지, curl 종료 상태와 HTTP 상태를 왜 따로 보는지 설명합니다. 요청 기록에는 메서드·경로·헤더·본문과 응답 상태·헤더·본문을 함께 남깁니다. Date와 임의 포트 같은 실행마다 달라지는 값은 계약 비교에서 제외합니다. 다음 레슨에서는 같은 조회 계약을 이전 파일 저장소와 Spring 빈에 연결합니다. [200]이라는 숫자를 암기하는 대신 실제 요청이 어떤 결과를 뜻하는지 판단하는 것이 이번 목표입니다.

기술 확인 참고: RFC 9110 HTTP 의미입니다. 본문의 도서 예제는 이 트랙의 저장소 계약으로 작성했습니다.

따라하기

검사 범위와 도구 준비

starter를 풀고 README와 check.py를 읽습니다. bash·curl·Python 3와 로컬 포트 bind가 가능한 환경을 준비합니다. 아래 문법 검사는 요청 결과를 증명하지 않습니다.

bash -n request.sh check.sh

초기 실패를 재현합니다

starter에서 실행합니다. JSON 요청 Content-Type이 빠져 create의 201 기대와 실제 상태가 다르고 상세도 따라 실패합니다. 환경에서 bind가 거부되면 external 검증 대기로 기록합니다.

bash check.sh

요청 헤더를 보완합니다

request.sh의 create curl 호출에 아래 헤더 인자를 추가합니다. 요청 본문은 기존 JSON 직렬화 코드로 유지합니다.

-H 'Content-Type: application/json'

다섯 사례를 다시 확인합니다

같은 명령으로 빈 목록 200·생성 201과 Location·상세 200·없는 ID 404·공백 제목 400을 확인합니다. 마지막 실패 수가 0이고 서버 스레드가 정상 정리되는지 봅니다. 실행하지 못한 출력은 기록하지 않습니다.

bash check.sh

확인 문제

실습

request.sh의 JSON POST에 필요한 요청 헤더를 보완합니다. bash check.sh는 로컬 임의 포트 fixture를 열고 빈 목록·201과 Location·상세·404·공백 제목 400을 검증합니다. 실패 요청 뒤 목록 불변도 확인하며 finally에서 서버를 정리합니다. 샌드박스 포트 bind가 거부되어 외부 실행 검증이 필요합니다. 원시 응답의 상태·헤더·본문을 구분해 기록합니다.

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

실행 명령

bash check.sh

기대 결과

외부 검증: 5개 요청 검사 실패 0, 종료 상태 0, 서버 스레드 종료입니다.

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

더 읽기

면접 질문

  • 빈 목록의 200과 없는 도서 상세의 404는 어떻게 다른가요?
  • Content-Type과 Accept의 역할은 어떻게 다른가요?