취업·면접 · 실무
개발자 실무 면접 - 질문의 의도와 모범 답안
네트워크와 웹 질문 - 주소창에 입력하면 생기는 일
URL 입력부터 화면까지 흐름(DNS·TCP·TLS·HTTP), TCP 와 UDP, HTTP 메서드·상태 코드·멱등성, 쿠키·세션·토큰, HTTPS 가 지키는 것, CORS, 꼬리 질문, 완성 코드는 원시 HTTP 요청 문자열을 파싱해 메서드·경로·헤더를 보여 주고 상태 코드별 의미와 재시도 가능 여부를 표로 출력한다
개발자KR · 원고 갱신
이 장에서 배우는 것
중고 거래 서비스의 상품 상세 주소를 열었는데 화면이 늦게 나타난다. 면접관이 “주소창에 주소를 입력한 뒤 어떤 일이 일어나는가”라고 묻는다. 이 질문은 통신 용어를 순서대로 외웠는지 확인하는 데 그치지 않는다. 이름 조회, 연결, 암호화, 요청과 응답을 구분하고, 어느 단계에서 문제가 생겼는지 설명할 수 있는지를 확인한다.
앞 장에서 프로세스와 스레드가 실행 자원을 사용하는 방식을 살펴보았다. 이번에는 브라우저와 서버 사이를 오가는 요청을 따라간다. 실제 인터넷에 접속하지 않고 요청 문자열을 해석하는 실험으로, 통신 규칙에 관한 답변을 눈에 보이는 결과와 연결한다.
- 주소 입력부터 화면 표시까지의 흐름을 단계별로 설명한다.
- TCP와 UDP의 보장 범위를 구분하고 서비스 요구에 맞춰 선택한다.
- 메서드, 상태 코드, 멱등성을 연결해 재시도 조건을 판단한다.
- 쿠키·세션·토큰의 역할과 HTTPS·CORS의 경계를 설명한다.
- 원시 HTTP 요청에서 메서드·경로·헤더를 추출하는 프로그램을 실행한다.
문제 상황
지원자는 가상의 중고 거래 서비스에서 상품 조회와 관심 상품 등록을 구현한 경험이 있다. 1차 기술 면접에서 면접관은 “상품 목록은 열리는데 관심 상품 등록만 간헐적으로 실패한다”라는 상황을 제시한다. 화면에는 실패 안내가 나타났지만 다시 누르면 등록 수가 늘어나기도 한다. 지원자가 곧바로 “서버가 느리니 재시도하면 된다”라고 답하면, 면접관은 응답을 받지 못한 것과 서버가 처리하지 않은 것이 같은지 되묻는다.
다른 상황도 이어진다. 별도 개발 서버에서 실행한 화면은 상품 API 응답을 읽지 못한다. 명령줄에서 같은 주소를 요청하면 응답이 온다. 이때 네트워크 단절, 로그인 실패, 브라우저의 교차 출처 제한을 구분해야 한다. 하나의 오류 화면만으로 원인을 단정하지 않고, 관찰한 사실과 아직 확인하지 못한 사실을 나누어 말하는 태도가 필요하다.
| 질문 | 의도 | 답변 핵심 | 꼬리 질문 |
|---|---|---|---|
| 주소 입력 후 화면은 어떻게 나타나는가 | 계층과 전체 흐름 이해 | 주소 해석, DNS, 연결, TLS, HTTP, 화면 구성 | 항상 새 연결을 만드는가 |
| TCP와 UDP는 무엇이 다른가 | 전송 특성과 선택 기준 | 바이트 스트림의 신뢰성, 데이터그램과 지연 요구 | UDP로 신뢰성을 구현할 수 있는가 |
| GET과 POST는 어떻게 구분하는가 | 메서드 의미 이해 | 조회 의미와 처리 요청 의미 | GET 요청에 본문을 넣어도 되는가 |
| 멱등성이란 무엇인가 | 실패 상황의 재시도 판단 | 같은 요청을 반복한 뒤 의도한 서버 효과 | 응답 코드도 같아야 하는가 |
| 401과 403은 어떻게 다른가 | 인증과 접근 거부 구분 | 유효한 인증 정보 부재와 요청 수행 거부 | 403이면 다시 로그인해야 하는가 |
| 쿠키·세션·토큰은 어떻게 다른가 | 상태 유지 수단 구분 | 브라우저 저장·전송, 서버 상태, 제시하는 증표 | 토큰도 쿠키에 넣을 수 있는가 |
| HTTPS가 보호하는 것은 무엇인가 | 보호 범위 이해 | 통신 기밀성·무결성·서버 인증 | HTTPS면 주소의 경로도 숨겨지는가 |
| CORS 오류는 왜 발생하는가 | 브라우저 정책 이해 | 출처에 따른 응답 공유 허용 여부 | 요청은 이미 처리되었을 수도 있는가 |
| 요청이 시간 초과되면 재시도하는가 | 부분 실패와 중복 처리 인식 | 처리 여부 불명, 메서드와 업무 중복 방지 확인 | 관심 상품 등록 중복은 어떻게 막는가 |
| 404와 503에 같은 대응을 하는가 | 오류 분류 능력 | 자원 식별 문제와 일시적 서비스 불가 구분 | 재시도 간격은 어떻게 정하는가 |
이 중 주소 입력 흐름, 재시도, 로그인 상태, CORS를 깊게 살펴본다. 답변은 결론을 먼저 말하고 근거를 붙인 뒤, 자신이 구현한 예시와 적용 범위를 덧붙이는 방식으로 구성한다. 아래 경험 문장은 설명용 사례다. 면접에서는 실제로 구현하거나 관찰한 범위로 바꾸어 말해야 한다.
주소 입력에서 화면까지: 단계를 구분해 설명하기
브라우저는 먼저 주소의 스킴, 호스트, 포트, 경로 등을 해석한다. 예를 들어 https://market.example/items/42?view=compact에서 스킴은 https, 호스트는 market.example, 경로는 /items/42다. 물음표 뒤는 질의 문자열이며 경로와 구분한다. 조각 식별자인 # 뒤의 값은 일반적인 HTTP 요청 대상에 포함하지 않는다.
도메인 이름 시스템(DNS)은 이름에 대응하는 주소 등의 정보를 조회하는 체계다. 브라우저나 운영체제 등에 쓸 수 있는 캐시가 있으면 매번 같은 조회를 반복하지 않는다. 또한 로컬 캐시에서 화면을 구성할 수 있다면 원격 서버 요청 자체가 필요하지 않을 수 있다. 따라서 “주소 입력마다 DNS부터 다시 수행한다”라는 설명은 범위를 지나치게 넓힌 것이다.
새 HTTPS 연결을 HTTP/1.1이나 HTTP/2로 만든다는 조건에서는 전송 제어 프로토콜(TCP) 연결을 맺고, 전송 계층 보안(TLS) 협상을 거쳐 HTTP 메시지를 주고받는다. TLS 과정에서는 인증서와 이름 등을 검증하고 통신에 사용할 보안 매개변수를 정한다. HTTP/3는 UDP 위의 QUIC을 사용하므로 모든 웹 통신을 TCP 연결 순서로 설명해서는 안 된다.
하이퍼텍스트 전송 프로토콜(HTTP) 응답을 받은 뒤에도 작업이 남는다. 브라우저는 HTML을 해석하고 필요한 스타일·스크립트·이미지 등을 추가로 가져온다. 문서와 스타일을 바탕으로 배치를 계산하고 그린다. 스크립트 실행과 추가 요청 때문에 일부 과정은 겹치거나 반복될 수 있다. 첫 HTML 응답을 받았다는 사실과 사용자가 화면을 조작할 수 있다는 사실은 같지 않다.
TCP는 순서 있는 바이트 스트림을 제공하며 손실 감지와 재전송, 흐름 제어와 혼잡 제어를 수행한다. 다만 연결이 끊어진 상황에서 데이터가 끝까지 전달된다고 보장하지 않으며, 전송 계층의 확인이 업무 처리를 완료했다는 뜻도 아니다. 보내는 쪽이 두 번 나누어 쓴 데이터가 받는 쪽에서도 두 번의 읽기로 나뉜다는 보장 역시 없다. 메시지 경계는 상위 규칙으로 구분해야 한다.
사용자 데이터그램 프로토콜(UDP)은 데이터그램 단위로 전송하며 프로토콜 자체가 도착·순서·중복 제거를 보장하지 않는다. 연결 설정이 간단하다는 이유만으로 항상 더 빠르다고 단정할 수는 없다. 실시간 음성처럼 늦게 도착한 데이터의 가치가 낮은 경우와 파일처럼 빠짐없는 전달이 필요한 경우의 요구가 다르다. 필요하면 상위 프로토콜이 신뢰성과 혼잡 제어를 구현할 수 있다. QUIC은 UDP 위에서 이런 기능을 제공하는 사례다.
깊게 보기: 주소를 입력하면 무슨 일이 일어나는가
| 수준 | 답변 예시 | 평가 근거 |
|---|---|---|
| 나쁜 답 | 인터넷으로 서버에 접속해서 화면을 받는다. | 단계와 관찰 지점을 구분하지 못한다. |
| 보통 답 | DNS로 주소를 찾고 TCP와 TLS 연결 후 HTTP 응답을 받는다. | 주요 순서는 알지만 전제와 화면 구성 과정이 빠졌다. |
| 좋은 답 | 새 HTTP/1.1 HTTPS 연결을 가정해 흐름을 설명하고, 캐시·연결 재사용·HTTP/3의 차이를 덧붙인다. | 기본 흐름을 유지하면서 적용 범위를 밝힌다. |
“새 HTTPS 연결을 사용하는 경우 주소 해석, 이름 조회, 연결과 보안 협상, HTTP 요청·응답, 화면 구성 순서로 설명할 수 있다. 각 단계가 담당하는 문제가 다르기 때문이다. 상품 상세 화면을 예로 들면 HTML을 받은 뒤에도 상품 이미지 요청이 이어질 수 있어 응답 시간과 화면 표시 시간을 나누어 보겠다. 다만 캐시나 기존 연결을 사용할 수 있고, HTTP/3는 연결 방식이 다르므로 모든 요청이 같은 순서를 새로 밟는 것은 아니다.”
면접관: “이미 연결이 있다면 어떻게 되는가?”
지원자: “재사용 가능한 연결이 있으면 연결 설정 비용을 줄일 수 있다. 다만 연결이 닫혔거나 다른 조건 때문에 재사용하지 못할 수 있으므로 실제 요청 기록에서 확인하겠다.”
면접관 반응: 기본 흐름을 설명하면서도 가정을 구분했으므로, 이제 느린 구간을 어떤 근거로 찾을지 이어서 물을 수 있다.
HTTPS가 제공하는 보호도 이 흐름에 붙여 설명하면 명확하다. 정상적인 인증서 검증을 전제로 통신 상대인 서버를 인증하고, TLS 구간에서 내용의 기밀성과 무결성을 보호한다. 클라이언트 인증서를 별도로 쓰지 않는 일반적인 구성에서 사용자 로그인까지 대신해 주지는 않는다. 중간에서 TLS를 종료하는 장비가 있다면 그 뒤의 통신 보호는 별도로 확인해야 한다.
경로·질의 문자열·헤더·본문은 TLS로 보호되는 HTTP 내용에 포함된다. 그러나 통신 상대의 IP 주소나 트래픽 크기·시점 같은 정보까지 모두 숨겨지지는 않는다. 이름 조회나 협상 방식에 따라 도메인 정보도 관찰될 수 있다. 서버에 저장된 데이터, 브라우저 내부의 악성 스크립트, 사용자가 접속한 사이트 자체의 신뢰성까지 HTTPS만으로 보장하지는 않는다.
메서드와 상태 코드: 재시도는 조건을 붙여 판단하기
메서드는 요청이 자원에 대해 어떤 의미를 갖는지 표현한다. GET은 자원 표현을 가져오는 데 사용하며 서버 상태 변경을 요청하는 용도로 설계하지 않는다. POST는 대상 자원이 요청 내용을 처리하도록 요청한다. PUT은 대상 자원의 상태를 요청 내용으로 생성하거나 대체하는 의미이며, DELETE는 대상 자원과 현재 기능의 연결을 제거하도록 요청한다. PATCH는 부분 변경을 표현하지만 변경 내용에 따라 멱등성이 달라진다.
안전한 메서드는 클라이언트가 서버 상태 변경을 요청하지 않는 메서드다. GET과 HEAD가 대표적이다. 접근 로그 기록 같은 부수 효과까지 없다는 뜻은 아니다. GET 요청 본문의 의미는 일반적으로 정의되어 있지 않아 다양한 중간 장치와 서버에서 일관되게 처리된다고 기대하기 어렵다. 조회 조건은 통상 질의 문자열로 표현하고, 별도 계약이 필요한 조회라면 그 조건을 명확히 정한다.
멱등성(idempotency)은 동일한 요청을 여러 번 수행했을 때 의도한 서버 효과가 한 번 수행했을 때와 같다는 성질이다. GET·PUT·DELETE는 메서드 의미상 멱등적이다. DELETE 첫 요청은 성공하고 두 번째는 404를 반환하더라도 대상이 제거된 결과는 같을 수 있다. 응답 내용이나 상태 코드가 매번 같아야 한다는 뜻이 아니다. 실제 서버 구현이 메서드 의미를 지키는지도 중요하다.
상태 코드는 응답의 결과를 분류한다. 200은 성공, 201은 자원 생성, 400은 잘못된 요청, 401은 대상 자원에 유효한 인증 정보가 없는 상태를 나타낸다. 403은 서버가 요청을 이해했지만 수행을 거부하는 경우이며 인증 외의 정책도 원인이 될 수 있다. 404는 대상 표현을 찾지 못했거나 존재 여부를 드러내지 않는 경우에 쓰일 수 있다. 429는 요청 빈도 제한, 503은 일시적인 서비스 불가를 나타낸다.
깊게 보기: 응답이 없으면 같은 요청을 다시 보내도 되는가
| 수준 | 답변 예시 | 평가 근거 |
|---|---|---|
| 나쁜 답 | 응답이 없으면 실패했으니 성공할 때까지 다시 보낸다. | 처리 완료 후 응답만 사라진 상황을 놓친다. |
| 보통 답 | 서버 오류일 때 몇 번 재시도한다. | 횟수는 제한하지만 중복 효과를 고려하지 않는다. |
| 좋은 답 | 처리 여부가 불명확하므로 메서드와 업무 중복 방지 조건을 확인하고 제한적으로 재시도한다. | 불확실성과 서버 효과를 함께 판단한다. |
“응답을 받지 못했다는 사실만으로 같은 요청을 바로 반복하지는 않겠다. 서버가 처리를 끝내고 응답만 전달하지 못했을 가능성이 있기 때문이다. 관심 상품 등록에서는 사용자와 상품의 조합을 기준으로 등록 결과가 하나가 되도록 만들거나, 같은 작업을 식별하는 키를 서버가 관리하도록 할 수 있다. 다만 키만 헤더에 붙이면 중복 방지가 완성되는 것은 아니다. 서버가 키와 요청 내용, 처리 결과를 일관되게 연결해야 하고, 재시도 횟수와 대기 시간도 제한해야 한다.”
면접관: “그러면 GET은 어떤 오류에서도 재시도하는가?”
지원자: “멱등성은 반복 효과에 관한 조건이다. 400처럼 요청을 고쳐야 하는 경우나 403처럼 정책상 거부된 경우를 그대로 반복해도 해결되지 않는다. 일시적 실패인지와 재시도 허용 조건을 함께 보겠다.”
면접관 반응: 메서드의 성질과 장애의 원인을 구분했으므로, 서버가 알려 주는 대기 시간을 어떻게 반영할지 확인한다.
429와 503에는 Retry-After 헤더가 올 수 있다. 값은 대기 초 또는 HTTP 날짜 형식이므로 두 형식을 구분해야 한다. 헤더가 있다고 해서 모든 메서드를 자동 재시도해도 되는 것은 아니다. 실무에서는 상한이 있는 대기 정책과 필요에 따른 무작위 지연을 적용해 요청이 한꺼번에 몰리는 일을 줄인다. 이번 실험은 네트워크 요청이나 실제 대기 없이, 상태 코드와 메서드에 따라 판단 문구가 달라지는 것만 확인한다.
로그인 상태와 브라우저 정책: 역할을 섞지 않기
HTTP의 각 요청은 그 자체로 해석할 수 있어야 한다. 이전에 로그인했다는 사실을 다음 요청에서도 활용하려면 이를 연결할 수단이 필요하다. 쿠키(cookie)는 브라우저가 저장하고 조건에 맞는 요청에 자동으로 붙이는 데이터다. 세션(session)은 사용자 상태를 여러 요청에 걸쳐 연결하는 개념이며, 흔히 서버가 상태를 저장하고 브라우저에는 세션 식별자를 전달하는 방식으로 구현한다. 토큰(token)은 접근 권한 등을 확인할 때 제시하는 증표다.
이 셋은 서로 배타적인 선택지가 아니다. 세션 식별자를 쿠키에 넣을 수 있고 접근 토큰을 쿠키에 넣을 수도 있다. 토큰에는 서버 조회가 필요한 불투명한 값도 있고, 서명된 클레임을 담은 형식도 있다. 서명은 변조 여부를 확인하기 위한 것이며 내용 암호화를 뜻하지 않는다. 토큰 기반이라는 이름만으로 서버 상태가 전혀 없거나 로그아웃 처리가 간단하다고 판단할 수 없다.
깊게 보기: 쿠키와 세션 중 무엇이 더 안전한가
| 수준 | 답변 예시 | 평가 근거 |
|---|---|---|
| 나쁜 답 | 쿠키는 위험하므로 세션만 쓰면 된다. | 저장·전송 수단과 상태 관리 방식을 혼동한다. |
| 보통 답 | 쿠키는 브라우저에, 세션은 서버에 저장한다. | 흔한 구성을 설명하지만 식별자 전달과 보안 조건이 빠졌다. |
| 좋은 답 | 서버 세션과 식별자 쿠키를 함께 쓰는 구성을 설명하고 만료·폐기·전송 조건을 검토한다. | 역할과 위험 발생 조건을 연결한다. |
“쿠키와 세션은 역할이 달라 둘 중 하나가 더 안전하다고 바로 비교하기 어렵다. 서버 세션을 쓰더라도 보통 브라우저가 식별자를 전달해야 하기 때문이다. 중고 거래 서비스의 로그인에서는 서버에 세션을 두고 식별자를 쿠키로 전달하는 구성을 선택할 수 있다. 쿠키에는 Secure와 HttpOnly, 용도에 맞는 SameSite 속성을 설정하고 만료와 서버 측 폐기를 관리하겠다. 다만 HttpOnly는 스크립트의 쿠키 읽기를 제한하는 속성이지 악성 스크립트의 모든 동작을 막는 수단은 아니다. 자동으로 전송되는 자격 증명에는 요청 위조 방어도 별도로 검토해야 한다.”
면접관: “접근 토큰을 쓰면 로그아웃할 때 브라우저에서 지우면 끝나는가?”
지원자: “브라우저에서 삭제하면 그 브라우저의 이후 사용은 줄일 수 있지만, 복사된 토큰의 유효성까지 사라지지는 않는다. 만료 시간과 갱신 자격 증명 폐기, 필요하면 서버 측 차단 정책을 함께 설계해야 한다.”
출처(origin)는 스킴·호스트·포트의 조합이다. 같은 호스트여도 포트가 다르면 출처가 다르다. 교차 출처 리소스 공유(CORS)는 브라우저가 다른 출처의 응답을 스크립트에 공유할 수 있는지 판단하는 체계다. API에 누가 접근할 수 있는지 결정하는 서버 인증·인가를 대신하지 않는다. 브라우저 외의 클라이언트는 이 정책에 따른 응답 읽기 제한을 같은 방식으로 받지 않는다.
깊게 보기: 명령줄에서는 되는데 브라우저에서는 왜 실패하는가
| 수준 | 답변 예시 | 평가 근거 |
|---|---|---|
| 나쁜 답 | 서버가 외부 요청을 전부 막아서 그렇다. | 브라우저 정책을 서버의 접근 통제로 오해한다. |
| 보통 답 | 서로 다른 출처라서 허용 헤더가 필요하다. | 핵심은 알지만 요청 전송과 응답 읽기를 구분하지 않는다. |
| 좋은 답 | 출처와 요청 조건을 확인하고 사전 요청 실패인지 실제 응답 공유 실패인지 나누어 본다. | 관찰 순서와 서버 처리 가능성을 함께 설명한다. |
“명령줄 요청 성공만으로 브라우저에서 응답을 읽을 수 있다고 판단할 수는 없다. 브라우저는 다른 출처의 응답 공유에 추가 조건을 적용하기 때문이다. 개발 화면에서 상품 API를 호출했다면 두 주소의 스킴·호스트·포트와 응답의 허용 출처를 먼저 확인하겠다. 사전 요청이 필요한 조건에서는 OPTIONS 응답을 확인한다. 다만 사전 요청 없이 실제 요청이 전송된 다음 응답 읽기만 제한될 수도 있으므로, 화면 오류만 보고 등록 작업이 수행되지 않았다고 단정하지 않겠다.”
면접관: “허용 출처를 별표로 바꾸면 해결되는가?”
지원자: “공개 응답이라면 선택지가 될 수 있지만, 자격 증명을 포함하는 요청의 응답 공유에는 별표를 사용할 수 없다. 허용할 출처를 검증해 명시하고 자격 증명 허용 조건도 맞춰야 한다. 요청의 출처를 검증 없이 그대로 반사하지 않겠다.”
JSON 본문을 보내거나 특정 사용자 정의 헤더를 사용하는 등 조건에 따라 브라우저는 실제 요청 전에 사전 요청을 보낸다. 사전 요청이 거절되면 실제 요청은 보내지 않는다. 반면 사전 요청이 필요 없는 요청도 있으므로 CORS 오류가 모두 같은 시점의 실패는 아니다. 또한 SameSite의 사이트 구분과 출처 구분은 같지 않다. 쿠키가 전송되는 조건과 응답을 읽을 수 있는 조건을 각각 확인해야 한다.
완성 코드
다음 내용을 main.py로 저장한다. 입력은 직접 만든 HTTP/1.1 요청 문자열 두 개다. 첫 요청에서 메서드·경로·헤더를 추출하고, GET과 POST에 대해 상태 코드별 재시도 판단을 출력한다. 두 번째 입력은 중복 헤더가 있는 요청이며, 조용히 덮어쓰지 않고 거부하는지 확인한다.
이 파서는 학습용으로 범위를 제한한다. 정확히 한 개의 본문 없는 요청, CRLF 줄바꿈, origin-form 요청 대상을 받는다. 헤더 중복은 종류와 관계없이 거부한다. 실제 HTTP에서는 필드에 따라 중복 처리 규칙이 다르다. 본문 길이 결정, 스트리밍, 프록시 처리, HTTP/2·HTTP/3의 메시지 표현을 구현한 서버용 파서가 아니다.
from urllib.parse import urlsplit
RAW_REQUEST = (
"GET /items/42?view=compact HTTP/1.1\r\n"
"Host: market.example\r\n"
"Accept: application/json\r\n"
"X-Request-Id: demo-001\r\n"
"\r\n"
)
STATUS_ROWS = (
(200, "성공", "skip"),
(201, "자원 생성", "skip"),
(400, "잘못된 요청", "fix"),
(401, "유효한 인증 정보 필요", "auth"),
(403, "수행 거부", "deny"),
(404, "대상 없음 또는 비공개", "check"),
(429, "요청 빈도 제한", "later"),
(503, "일시적 서비스 불가", "later"),
)
TOKEN_CHARS = frozenset(
"!#$%&'*+-.^_`|~"
"0123456789"
"abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
)
IDEMPOTENT_METHODS = frozenset(
{"GET", "HEAD", "PUT", "DELETE", "OPTIONS", "TRACE"}
)
def parse_request(raw):
if not raw.endswith("\r\n\r\n"):
raise ValueError("본문 없는 요청의 종료 구분자가 필요하다")
lines = raw[:-4].split("\r\n")
if any("\r" in line or "\n" in line for line in lines):
raise ValueError("CRLF 줄바꿈만 허용한다")
parts = lines[0].split(" ")
if len(parts) != 3:
raise ValueError("요청 줄은 세 항목이어야 한다")
method, target, version = parts
if not method or any(ch not in TOKEN_CHARS for ch in method):
raise ValueError("잘못된 메서드")
if version != "HTTP/1.1":
raise ValueError("HTTP/1.1만 지원한다")
if not target.startswith("/") or target.startswith("//"):
raise ValueError("origin-form 요청 대상만 지원한다")
if "#" in target or any(ord(ch) <= 32 or ord(ch) >= 127 for ch in target):
raise ValueError("잘못된 요청 대상")
url = urlsplit(target)
headers = {}
for line in lines[1:]:
name, separator, value = line.partition(":")
if not separator or not name:
raise ValueError("잘못된 헤더 형식")
if any(ch not in TOKEN_CHARS for ch in name):
raise ValueError("잘못된 헤더 이름")
if any((ord(ch) < 32 and ch != "\t") or ord(ch) == 127 for ch in value):
raise ValueError("잘못된 헤더 값")
key = name.lower()
if key in headers:
raise ValueError(f"중복 헤더: {key}")
headers[key] = value.strip(" \t")
if not headers.get("host"):
raise ValueError("Host 헤더가 필요하다")
if "content-length" in headers or "transfer-encoding" in headers:
raise ValueError("본문 프레이밍은 지원하지 않는다")
return method, url.path, url.query, headers
def retry_advice(method, rule):
fixed = {
"skip": "불필요",
"fix": "요청 수정",
"auth": "인증 확인",
"deny": "권한·정책 확인",
"check": "대상 확인",
}
if rule in fixed:
return fixed[rule]
if method in IDEMPOTENT_METHODS:
return "대기 후 제한 재시도"
return "중복 방지 확인 후 판단"
def main():
method, path, query, headers = parse_request(RAW_REQUEST)
print("[요청 해석]")
print(f"메서드: {method}")
print(f"경로: {path}")
print(f"질의: {query}")
print("헤더:")
for name, value in sorted(headers.items()):
print(f" {name}: {value}")
print("\n[상태 코드와 재시도 판단]")
print("코드 | 의미 | GET | POST")
for code, meaning, rule in STATUS_ROWS:
get_advice = retry_advice("GET", rule)
post_advice = retry_advice("POST", rule)
print(f"{code} | {meaning} | {get_advice} | {post_advice}")
print("\n[잘못된 입력]")
duplicate = RAW_REQUEST.replace(
"Accept: application/json\r\n",
"Host: another.example\r\n",
)
try:
parse_request(duplicate)
except ValueError as error:
print(f"거부: {error}")
if __name__ == "__main__":
main()
줄별 해설
urlsplit은 요청 대상에서 경로와 질의를 나누는 데 사용한다. 이 함수가 HTTP 요청 전체의 유효성을 검사해 주지는 않는다. 그래서 먼저 지원할 요청 대상 형식과 문자 범위를 검사한다. 이 예제는 비ASCII 문자를 그대로 받지 않으며, 필요한 문자는 적절히 인코딩된 요청 대상을 전제로 한다.
RAW_REQUEST의 각 줄은 CRLF로 끝난다. 마지막 빈 줄은 헤더 영역의 종료를 나타낸다. 코드에 적힌 \r\n은 실행 시 실제 제어 문자로 바뀐다. 이 실험에서는 본문이 없으므로 헤더 종료 뒤에 다른 데이터가 있으면 정상 요청으로 처리하지 않는다.
STATUS_ROWS는 상태 코드의 의미와 판단 분류를 묶은 고정 데이터다. later는 재시도가 허용되었다는 뜻이 아니라 대기와 반복 효과를 더 검토할 대상이라는 뜻이다. 상태 코드만 보고 업무 동작을 결정하지 않도록 판단을 다음 함수로 넘긴다.
TOKEN_CHARS는 메서드와 헤더 이름에 허용할 문자를 모은다. 메서드는 대소문자를 구분하므로 대문자로 강제 변환하지 않는다. 반면 헤더 이름은 대소문자를 구분하지 않으므로 저장할 때 소문자로 바꾼다. 헤더 값 전체를 소문자로 바꾸면 식별자나 자격 증명의 의미가 달라질 수 있다.
parse_request는 종료 구분자를 검사한 뒤 요청 줄을 세 항목으로 나눈다. 정확한 공백 하나를 구분자로 사용해 이 실험이 허용하는 형식을 좁힌다. 요청 대상의 #와 제어 문자를 거부하고, url.path와 url.query를 별도로 반환한다. 경로와 질의의 구분을 직접 확인하는 부분이다.
헤더 처리의 partition(":")은 첫 번째 콜론만 기준으로 나눈다. 값 내부에 콜론이 있어도 그대로 보존한다. 이름을 정규화한 뒤 중복을 검사하므로 Host와 host도 중복으로 판단한다. 중복 거부는 이 실험의 정책이며 모든 실제 헤더에 적용할 일반 규칙은 아니다.
Host 검사는 필수 필드의 존재와 비어 있지 않음을 확인하는 수준이다. 호스트 문법 전체를 검증하지 않는다. Content-Length와 Transfer-Encoding은 본문 경계를 다루는 필드이므로 지원 범위를 벗어났다는 오류로 처리한다. 지원하지 않는 입력을 우연히 해석하는 대신 범위를 드러내는 선택이다.
retry_advice는 성공이면 반복이 불필요하다고 표시하고, 요청·인증·정책 문제에는 먼저 확인할 대상을 출력한다. 429와 503에서는 메서드의 멱등성에 따라 문구를 나눈다. “대기 후 제한 재시도”에도 남은 기한, 서버 안내, 서비스 계약 확인이라는 전제가 있다. 실제 요청을 보내는 기능은 없다.
main은 헤더를 이름순으로 출력하고 고정된 상태 코드 순서를 사용한다. 난수·현재 시각·외부 입력·네트워크를 사용하지 않아 실행 결과가 일정하다. 마지막에는 Host가 두 번 나타나도록 입력을 바꾸고 예상한 예외만 잡는다. 정상 출력과 잘못된 입력 처리를 한 번에 확인할 수 있다.
실행 결과
Python 3.12 이상에서 다음 명령을 실행한다. 파일 실행 후 대기 없이 종료된다.
python3 main.py
예상 출력은 다음과 같다.
[요청 해석]
메서드: GET
경로: /items/42
질의: view=compact
헤더:
accept: application/json
host: market.example
x-request-id: demo-001
[상태 코드와 재시도 판단]
코드 | 의미 | GET | POST
200 | 성공 | 불필요 | 불필요
201 | 자원 생성 | 불필요 | 불필요
400 | 잘못된 요청 | 요청 수정 | 요청 수정
401 | 유효한 인증 정보 필요 | 인증 확인 | 인증 확인
403 | 수행 거부 | 권한·정책 확인 | 권한·정책 확인
404 | 대상 없음 또는 비공개 | 대상 확인 | 대상 확인
429 | 요청 빈도 제한 | 대기 후 제한 재시도 | 중복 방지 확인 후 판단
503 | 일시적 서비스 불가 | 대기 후 제한 재시도 | 중복 방지 확인 후 판단
[잘못된 입력]
거부: 중복 헤더: host
출력에서 GET과 POST가 달라지는 곳은 429와 503이다. 그렇다고 POST의 반복이 언제나 불가능한 것은 아니다. 서버가 업무상 중복 방지를 제공한다면 제한적으로 반복할 수 있다. 반대로 GET이라도 서버 구현이 조회 요청에 등록 효과를 넣었다면 메서드 의미를 어긴 구현부터 고쳐야 한다. 이 표는 자동 승인 목록이 아니라 면접 답변에 필요한 확인 순서를 보여 준다.
실무에서 자주 틀리는 것
헤더를 모든 콜론에서 나누기
다음은 잘못된 예다. 값에 콜론이 들어가면 항목이 세 개 이상이 되어 실패한다.
line = "Referer: https://market.example/items/42"
name, value = line.split(":")
첫 번째 콜론만 구분자로 쓰고 구분자 존재 여부를 확인한다. 실제 파서에서는 이름과 값 검사도 이어져야 한다.
line = "Referer: https://market.example/items/42"
name, separator, value = line.partition(":")
if not separator:
raise ValueError("헤더 구분자가 없다")
value = value.strip(" \t")
서버 오류면 메서드와 무관하게 반복하기
다음 판단은 POST가 이미 처리되었을 가능성과 재시도 한도를 무시한다.
method = "POST"
status = 503
should_retry = status >= 500
고친 예는 이 장에서 다루는 일시적 상태와 중복 안전 조건을 함께 검사한다. duplicate_safe는 클라이언트의 추측이 아니라 확인된 서버 계약이어야 한다. 참이 되어도 실제 반복에는 대기와 횟수 제한을 적용한다.
method = "POST"
status = 503
duplicate_safe = False
attempts_left = 2
repeat_safe = method in {"GET", "HEAD", "PUT", "DELETE"}
should_retry = (
status in {429, 503}
and (repeat_safe or duplicate_safe)
and attempts_left > 0
)
헤더 이름의 대소문자를 서로 다르게 취급하기
다음은 이름이 다르다고 판단해 동일 필드를 두 개 저장하는 예다. 이후 코드가 어떤 이름을 조회하느냐에 따라 결과가 달라진다.
headers = {}
for name, value in [("Host", "market.example"), ("host", "other.example")]:
headers[name] = value
고친 예는 이름을 정규화한 뒤 이 실험의 중복 거부 정책을 적용한다. 값은 원래 대소문자를 유지한다.
headers = {}
for name, value in [("Host", "market.example"), ("host", "other.example")]:
key = name.lower()
if key in headers:
raise ValueError(f"중복 헤더: {key}")
headers[key] = value
자격 증명 요청에 허용 출처 별표를 쓰기
다음 응답 헤더 조합은 자격 증명을 포함한 교차 출처 응답 공유 조건을 충족하지 못한다.
response_headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": "true",
}
고친 예는 서버 설정에서 관리하는 허용 목록과 비교한다. 응답을 출처에 따라 달리한다면 공유 캐시도 그 차이를 구분하도록 해야 한다. 아래 예는 기존 Vary 값이 없는 단순한 경우다. 실제 응답에 다른 값이 있다면 보존해 합친다.
request_origin = "https://web.market.example"
allowed_origins = {"https://web.market.example"}
response_headers = {}
if request_origin in allowed_origins:
response_headers["Access-Control-Allow-Origin"] = request_origin
response_headers["Access-Control-Allow-Credentials"] = "true"
response_headers["Vary"] = "Origin"
이 설정만으로 사전 요청의 메서드·헤더 허용, 쿠키 전송 조건, 인증과 요청 위조 방어가 모두 해결되지는 않는다. 어떤 동작을 허용하는 코드인지 범위를 좁혀 설명해야 한다.
한눈에 보기
| 주제 | 핵심 사실 | 함께 말할 경계 | 중고 거래 예시 |
|---|---|---|---|
| 주소 입력 흐름 | 이름 조회·연결·보안·요청·화면 구성 | 캐시와 연결 재사용, HTTP 버전 | 상품 이미지 요청이 뒤따른다. |
| TCP | 순서 있는 바이트 스트림 | 메시지 경계와 업무 완료는 별개다. | 응답 유실만으로 등록 실패를 판단하지 않는다. |
| UDP | 데이터그램 전송 | 자체적으로 도착·순서를 보장하지 않는다. | 실시간 통신의 지연 요구를 검토한다. |
| 멱등성 | 반복 후 의도한 서버 효과가 같다. | 응답 코드까지 같을 필요는 없다. | 같은 상품 삭제를 반복할 수 있다. |
| 재시도 | 원인과 반복 효과를 함께 확인한다. | 횟수·대기·기한을 제한한다. | 관심 등록의 중복 방지를 확인한다. |
| 쿠키·세션·토큰 | 전송 수단·상태 연결·증표의 역할 | 조합할 수 있으며 폐기 정책이 필요하다. | 세션 식별자를 쿠키로 전달한다. |
| HTTPS | TLS 구간의 기밀성·무결성·서버 인증 | 로그인과 저장 데이터 보호는 별도다. | 로그인 요청의 전송 내용을 보호한다. |
| CORS | 브라우저의 교차 출처 응답 공유 | 인증·인가를 대신하지 않는다. | 개발 화면의 API 응답 읽기를 허용한다. |
답변을 마칠 때는 “어떤 전제에서, 무엇을 보장하며, 무엇을 추가로 확인해야 하는가”를 한 문장씩 점검한다. 상태 코드 숫자나 속성 이름을 많이 나열하는 것보다 요청이 어느 단계까지 진행되었는지 설명하는 편이 문제를 좁히는 데 도움이 된다.
연습 문제
- 완성 코드의 요청 메서드를 DELETE로 바꾸고, 상태 표의 GET 열을 DELETE 열로 바꾸어 출력하라. 같은 삭제 요청의 첫 응답이 200이고 두 번째 응답이 404여도 멱등적일 수 있는 이유를 설명하라.
- 헤더에
X-Trace: part:a:b를 추가하라. 저장되는 값을 예상하고,split(":")으로 처리하면 무엇이 달라지는지 설명하라. 이어서host를 하나 더 추가했을 때 거부되는 이유를 설명하라. - POST 관심 상품 등록에서 응답이 유실되었다. 서버 처리 여부, 중복 방지 계약, 재시도 제한이라는 세 관점으로 답하라. 503을 받았다는 이유만으로 안전하다고 결론 내리지 말라.
- 개발 화면이 다른 출처의 API에 요청한 뒤 CORS 오류를 표시했다. 사전 요청이 실패한 경우와 실제 응답 읽기가 제한된 경우를 구분하고, 서버 처리 여부를 어떻게 확인할지 설명하라.
정답과 해설
- 요청 줄의 GET을 DELETE로 바꾸고 표 제목과
retry_advice호출의 메서드도 함께 바꾼다. DELETE는 멱등 메서드 집합에 있으므로 429와 503의 판단 문구는 GET과 같다. 첫 요청으로 대상이 제거되고 두 번째 요청에서 대상 부재를 알렸다면 반복 후 의도한 효과는 같다. 상태 코드가 다르다는 사실만으로 멱등성이 깨지지는 않는다. - 저장되는 값은
part:a:b다. 첫 콜론을 기준으로 나누므로 값 내부의 콜론은 보존된다. 제한 없이split(":")하면 네 조각으로 나뉘어 두 변수에 대입할 수 없다. 추가한host는 기존Host와 정규화 후 이름이 같으므로 중복으로 거부된다. 이는 이 학습용 파서가 선택한 명시적인 정책이다. - 응답 유실은 서버 미처리의 증거가 아니다. 동일 사용자·상품의 등록 효과를 하나로 유지하거나 작업 식별 키를 서버가 일관되게 관리하는 계약이 있는지 확인한다. 계약이 없다면 처리 결과 조회 등으로 먼저 상태를 확인하는 방법을 검토한다. 재시도가 가능해도 남은 기한과 시도 횟수를 제한하고 서버의 대기 안내를 반영한다. 503은 일시적 불가를 나타내지만 그것만으로 업무 중복 가능성을 제거하지 않는다.
- 사전 요청이 실패하면 브라우저는 해당 실제 요청을 보내지 않는다. 사전 요청 없이 전송되는 조건에서는 실제 요청이 처리된 뒤 응답 읽기만 제한될 수 있다. 브라우저의 요청 기록에서 OPTIONS와 실제 요청을 구분하고, 서버의 요청 식별자나 결과 조회로 처리 여부를 확인한다. 화면 오류를 근거로 등록을 반복하지 않으며, 허용 출처·메서드·헤더·자격 증명 조건을 각각 점검한다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.