Devin.KR

요청·응답과 오류 위치

110분 안팎

학습 목표

연결 실패·시간 초과·HTTP 오류를 구분합니다.

개념

응답이 없다는 사실과 거절 응답을 구별합니다

가입 API 테스트가 실패했다는 한 줄만으로는 개발자가 무엇을 조사해야 할지 알기 어렵습니다. 서버에 연결하지 못했는지, 연결 후 정해진 시간에 응답하지 않았는지, 정상적으로 받은 응답이 입력을 거절했는지를 나눕니다. 이번 목표는 요청의 구성 요소를 읽고 curl의 종료 코드와 HTTP 상태를 서로 다른 증거로 기록하는 것입니다. 이후 자동 검증에서 환경 실패를 제품의 권한 거절로 오해하지 않는 출발점입니다.

앞 모듈의 앱은 가입 결과를 HTTP 200과 result 필드로 표현했습니다. 이번 제공 API의 추가 합의는 api/contract.json에 있습니다. 성공 가입은 201, 잘못된 입력은 400, 중복 이메일은 409입니다. 과거 evidence를 새 코드에 맞춰 고치지 않습니다. 같은 서비스 도메인이라도 교육용 제공 버전의 계약이 확장되었으므로 버전과 관찰 시점을 함께 기록합니다. 200이라는 숫자 자체가 모든 업무의 성공 기준인 것은 아닙니다.

실습 환경과 실행 책임을 확인합니다

이 모듈은 JDK 17, Spring Boot 3.1.5, Node.js 20 이상, curl, Python 3을 사용합니다. 로컬 zip을 풀고 README와 check.sh가 있는 루트에서 명령을 실행합니다. Java용 Maven wrapper가 포함되어 있어 시스템 Maven 설치는 필요하지 않습니다. 처음 의존성을 준비할 때 ./mvnw test를 실행하고 캐시가 준비되면 bash check.sh로 오프라인 검사를 수행합니다. 다운로드 실패는 제품 계약 실패와 구분합니다.

실습은 합성 이메일과 임시 H2 DB를 사용하며 운영 설정을 읽지 않습니다. JUnit이 127.0.0.1의 임시 포트를 선택하고 Spring context를 정상 종료합니다. 별도 서버 실행이나 프로세스 종료 명령을 입력하지 않습니다. 제공 네트워크 검사는 작성 환경의 포트 열기 제한으로 실행 확인이 대기 중입니다. 외부 검증자는 bash check.sh로 curl 요청과 쿠키 전달을 확인합니다. 이 레슨의 실제 출력은 네트워크를 사용하지 않는 분류 함수 실행에서만 제시합니다.

브라우저 레슨은 실행 창의 표준 입력에 JSON 배열을 넣고 표준 출력으로 정해진 문자열을 반환합니다. 로컬 파일이나 실제 fetch 요청을 브라우저 채점기에 넣지 않습니다. 각 레슨 실습의 종류와 제출 파일을 먼저 읽습니다. 미션은 앞 모듈 solution을 포함하고 api-app을 추가한 프로젝트입니다. 이전 checks와 문서의 보존 여부도 확인하며 실제 화면·동시 요청·세션 만료는 별도 범위로 남깁니다.

요청을 주소·방법·헤더·본문으로 읽습니다

GET /practice/profile?email=a%40example.test는 회원 프로필 조회입니다. URL은 어느 자원에 접근하는지, 메서드는 어떤 작업인지 나타냅니다. 쿼리 값은 encodeURIComponent로 인코딩하면 주소에서 특별한 의미를 가진 문자와 데이터 문자를 구별할 수 있습니다. POST /practice/signup은 가입, POST /practice/login은 로그인, PATCH /practice/profile은 본인 닉네임 변경입니다. 이번 API의 메서드 합의이며 다른 제품의 계약은 문서로 따로 확인합니다.

JSON 본문을 보낼 때 Content-Type: application/json과 JSON 문자열을 같이 보냅니다. curl의 -H는 헤더, -d는 데이터를 지정합니다. -d만 보내면 JSON 미디어 타입이 자동 보장되지 않습니다. Unsupported Media Type과 415가 나오면 입력 필드 규칙을 조사하기 전에 헤더와 본문 형식을 확인합니다. JSON 따옴표가 깨진 경우는 본문을 읽는 단계의 문제이므로 이메일 중복 거절과 같은 업무 오류로 분류하지 않습니다.

응답에서는 상태, Content-Type, Set-Cookie, JSON 본문을 순서대로 봅니다. 헤더 이름의 대소문자는 의미 차이를 만들지 않지만 값은 계약대로 해석합니다. HTTP 상태 401은 이 앱에서 인증이 필요함을 뜻하고 403은 인증된 사용자가 타인의 자원에 접근하여 거절됨을 뜻합니다. 두 상태의 처리 순서는 인증 여부 다음 소유자 일치입니다. 로그인에 성공한 세션과 조회하려는 회원을 각각 기록해야 재현할 수 있습니다.

curl의 두 결과를 나란히 기록합니다

curl -s --max-time 3 -o /dev/null -w '%{http_code}'는 본문을 버리고 상태만 표시합니다. -s는 진행 표시를 숨기며 --max-time은 전체 작업의 시간 상한입니다. shell의 $?는 직전 명령의 종료 코드입니다. 이 값은 HTTP 상태 코드와 단위부터 다릅니다. 401을 받은 curl은 기본 옵션에서 종료 코드 0일 수 있습니다. 서버가 응답했다는 전송 결과와 앱이 접근을 거절했다는 업무 결과를 한 칸에 합치지 않습니다.

종료 코드 7은 연결 실패, 28은 시간 상한 도달입니다. 응답 상태를 확보하지 못하면 -w의 http_code는 000으로 보일 수 있는데 실제 HTTP 상태 0이라는 뜻이 아닙니다. 연결 실패는 잘못된 주소·포트나 서버 미기동 등 원인을 추가 조사합니다. 시간 초과 역시 제품이 느린지 환경이 막혔는지 이 한 번의 결과로 확정하지 않습니다. 테스트 ID, 시간 제한, 호출 대상과 함께 관찰 사실을 남깁니다.

-f 또는 --fail을 사용하면 400 이상 HTTP 응답이 종료 코드 22로 바뀔 수 있습니다. 이번 probe는 -f를 쓰지 않고 상태를 별도 확인합니다. 다른 스크립트의 옵션을 그대로 가져와 22를 연결 실패라고 적지 않습니다. set -e를 쓰는 shell은 실패 명령 뒤에 바로 종료할 수 있어서 probe는 의도한 연결 실패와 시간 초과를 수집할 구간만 set +e로 두고 곧 다시 켭니다. 오류 무시 구간을 전체 스크립트로 넓히지 않습니다.

로그인 쿠키의 전달 여부를 증거로 남깁니다

로그인 응답의 Set-Cookie는 이후 요청에 보낼 세션 식별값을 알려 줍니다. curl -c cookies는 받은 쿠키를 파일에 쓰고 -b cookies는 다음 요청에 해당 파일의 쿠키를 보냅니다. 로그인 요청이 200이어도 쿠키를 전달하지 않은 조회는 401로 거절될 수 있습니다. 이는 로그인 실패로 바로 단정할 수 없으며 먼저 다음 요청의 쿠키 전달 조건을 확인합니다. 쿠키 값 자체는 보고서에 넣지 않습니다.

probe.sh는 임시 폴더에 헤더와 쿠키를 담고 종료 시 정상 삭제합니다. Set-Cookie가 있는지와 JSON Content-Type인지 확인한 후 본인 프로필을 조회합니다. 실제 쿠키 문자열 대신 present라는 증거만 남기므로 세션 식별값을 노출하지 않습니다. 사용자 입력 비밀번호를 curl -v 출력과 함께 공유하지 않습니다. 옵션의 자세한 기능은 더 읽기와 curl 공식 문서에서 확인하되 이번 제출은 분류 함수의 정확성과 요청 조건 설명에 집중합니다.

오류 분류 함수를 완성합니다

http/classify.sh의 classify는 종료 코드와 상태 두 값을 받습니다. 종료 코드가 0이면 HTTP 상태 400 이상을 HTTP_ERROR, 나머지를 RESPONSE로 구별합니다. 7은 CONNECT_FAILED, 28은 TIMEOUT, 다른 종료 코드는 TRANSPORT_ERROR입니다. RESPONSE는 업무 성공을 보장하지 않는 이름입니다. 성공 판정과 본문 의미는 다음 레슨의 책임입니다. 문자열 000을 상태 비교에 넣기 전에 종료 코드를 먼저 분기합니다.

starter는 모든 결과를 RESPONSE로 반환하므로 연결 실패 분류 검사에서 실패합니다. [ 조건 ]의 종료 코드가 1이면 조건 불일치이며 syntax error는 shell 문법 문제입니다. 같은 실패에도 원인이 다릅니다. 함수를 완성한 뒤 실제 probe의 종료 코드와 상태 조건까지 확인합니다. 외부 실행 결과에는 접속·시간 초과·미인증·로그인 헤더·쿠키 있는 본인 조회가 각각 들어 있어야 합니다. 연결 실패를 성공으로 바꾸는 수정은 해결이 아닙니다.

curl 공식 옵션·종료 코드 문서에서 실습의 옵션과 API 정의를 확인할 수 있습니다.

따라하기

전송 결과 분류를 실행합니다

파일 없이 실행할 수 있는 분류 함수 예제입니다. 아래 출력을 종료 코드와 상태의 두 열로 읽습니다.

classify() {
 local code="$1" status="$2"
 case "$code" in
  0) if [ "$status" -ge 400 ]; then echo HTTP_ERROR; else echo RESPONSE; fi ;;
  7) echo CONNECT_FAILED ;;
  28) echo TIMEOUT ;;
  *) echo TRANSPORT_ERROR ;;
 esac
}
for pair in "0 401" "7 000" "28 000" "0 201"; do
 read -r exit_code status <<< "$pair"
 printf "%s %s " "$exit_code" "$status"
 classify "$exit_code" "$status"
done

실행 결과

0 401 HTTP_ERROR
7 000 CONNECT_FAILED
28 000 TIMEOUT
0 201 RESPONSE

제공 요청 스크립트를 읽습니다

qa-http-contract 압축을 푼 루트에서 다음 명령으로 제공 요청을 읽습니다. --max-time, -w, 종료 코드 수집 구간을 찾고 401과 연결 실패가 다른 분기로 가는지 확인합니다. 파일 읽기는 네트워크를 사용하지 않습니다.

cat http/probe.sh

로그인과 쿠키를 연결합니다

probe.sh의 로그인 헤더와 쿠키 옵션을 따로 찾습니다. -c는 저장, -b는 전달입니다. 변수 base·work는 스크립트와 JUnit이 준비하므로 개별 요청을 복사해 따로 실행하지 않습니다. 외부 전체 실행에서는 Set-Cookie 존재와 쿠키 있는 본인 조회 상태 200을 확인하며 식별값은 공개하지 않습니다.

python3 -c "from pathlib import Path; print(''.join(line for line in Path('http/probe.sh').read_text().splitlines(True) if 'cookie' in line.lower() or 'content-type' in line.lower()))"

분류 함수를 수정하고 검사합니다

압축을 푼 qa-http-contract의 http/classify.sh를 첫 단계의 조건에 맞게 수정합니다. 외부 실행자는 다음 명령으로 전체 probe를 확인합니다. 종료 코드 0과 CLASSIFY PASS: 7 cases, 로그인 헤더·쿠키 있는 본인 조회를 확인해야 합니다. 작성 환경에서는 네트워크 검증 대기입니다.

bash check.sh

확인 문제

실습

http/classify.sh를 완성합니다. 연결 실패·시간 초과·HTTP 거절·정상 응답·기타 전송 오류를 분리하고 제공 probe의 실제 요청과 쿠키 조건도 확인합니다. 서버와 쿠키 임시 파일 정리는 테스트가 담당합니다. 이 네트워크 실습은 작성 샌드박스의 포트 제한으로 외부 실행 검증 대기입니다.

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

실행 명령

bash check.sh

기대 결과

JUnit 1개 통과, CLASSIFY PASS: 7 cases와 쿠키 전달 확인; 외부 검증 대기

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

더 읽기

면접 질문

  • API 응답 코드가 성공이어도 테스트가 실패할 수 있는 상황을 설명해 주시면 됩니다.