← Posts

HTTP · Web · Backend

요청과 응답 - 한 번의 왕복에 무엇이 오가나

주소창에 한 줄 쳤을 때 실제로 오가는 건 글자 몇 줄이다. 그 글자가 어떻게 생겼고 어디를 지나는지 본다.

목차
  1. 주소창에 한 줄 쳤을 뿐인데
  2. 요청 메시지는 네 덩어리다
  3. 응답도 같은 모양이다
  4. 메서드는 무엇을 하려는지다
  5. 상태 코드는 앞자리로 갈린다
  6. 상태 코드는 고르는 게 어렵다
  7. 헤더가 실제로 하는 일
  8. 요청은 서버 안에서 어디를 지나나
  9. 실무에서 이 왕복을 어디서 보나
  10. 정리

브라우저 개발자도구의 Network 탭을 처음 열면 줄이 수십 개 뜬다. 그 한 줄 한 줄이 요청 하나와 응답 하나의 짝이다.

주소창에 한 줄 쳤을 뿐인데

https://example.com/products/7 을 열면 화면에 상품 하나가 뜬다. 브라우저와 서버 사이에 무슨 일이 있었나.

diagramdiagram

핵심은 두 가지다. 하나, 언제나 클라이언트가 먼저 묻고 서버가 답한다. 서버가 먼저 말을 거는 일은 없다. 둘, 한 왕복에 하나씩만 오간다. 페이지 하나를 그리려고 왕복이 수십 번 일어난다.

그리고 그 왕복에서 실제로 선을 타고 흐르는 건 놀랍게도 그냥 글자다. HTTP 메시지는 사람이 읽을 수 있는 텍스트로 정의돼 있다.

참고

“텍스트다”는 HTTP/1.1까지의 이야기다. HTTP/2부터는 같은 내용을 사람이 못 읽는 이진 형식으로 나른다. 다만 담기는 내용(메서드·경로·헤더·바디)은 그대로라서, 개발자도구는 어느 버전이든 아래와 같은 모양으로 보여준다. 왜 형식이 바뀌었는지는 연결 글에서 다룬다.

요청 메시지는 네 덩어리다

브라우저가 보낸 요청을 글자 그대로 펼치면 이렇게 생겼다.

http
GET /products/7 HTTP/1.1
Host: example.com
Accept: text/html
User-Agent: Mozilla/5.0

네 덩어리로 나뉜다.

덩어리무슨 뜻인가
메서드GET무엇을 하려는지
경로/products/7어느 자원에
버전HTTP/1.1어떤 규칙으로 말할지
헤더Host: ...이 요청에 대한 부가 설명

그리고 헤더 뒤에 빈 줄이 하나 온다. 이게 “헤더 끝, 여기부터 바디”라는 신호다. 위 요청은 빈 줄 뒤에 아무것도 없다. GET은 보통 보낼 내용이 없기 때문이다.

바디가 있는 요청은 이렇게 생긴다.

http
POST /products HTTP/1.1
Host: example.com
Content-Type: application/json
Content-Length: 37

{"name": "키보드", "price": 79000}

Content-Type은 “바디가 무슨 형식인지”, Content-Length는 “몇 바이트인지”를 알려준다. 받는 쪽은 이 둘이 없으면 바디를 어디까지 읽어야 하는지도, 읽고 나서 뭘로 해석해야 하는지도 모른다.

응답도 같은 모양이다

응답은 요청과 다른 물건처럼 보이지만, 뜯어보면 구조가 거의 같다.

http
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 46

{"id": 7, "name": "키보드", "price": 79000}

다른 건 첫 줄뿐이다. 요청은 첫 줄에 “무엇을 하고 싶다”를 쓰고, 응답은 첫 줄에 **“그래서 어떻게 됐다”**를 쓴다.

diagramdiagram

이 대칭을 기억해두면 나중이 쉽다. 뒤에 나올 캐시도 압축도 인증도 전부 저 헤더 자리에 한 줄씩 붙는 것이지, 새로운 통신 방식이 아니다.

메서드는 무엇을 하려는지다

경로가 “어디”라면 메서드는 “무엇을”이다. 같은 /products/7이라도 GET이면 읽기고 DELETE면 삭제다.

메서드하려는 일
GET읽는다
POST새로 만든다 (또는 그 밖의 처리를 시킨다)
PUT통째로 바꾼다
PATCH일부만 바꾼다
DELETE지운다

여기서 초중급이 자주 놓치는 게 있다. 메서드는 약속이지 강제가 아니다. GET 핸들러 안에서 데이터를 지우는 코드를 짜도 HTTP는 막지 않는다. 다만 그렇게 하면 중간의 캐시나 프록시, 브라우저가 “GET은 읽기니까 마음대로 다시 보내도 되겠지”라고 판단해서 의도치 않게 삭제가 여러 번 일어날 수 있다. 약속을 어기는 쪽이 손해를 본다.

“여러 번 보내도 결과가 같은가”를 멱등성이라고 부른다. GET·PUT·DELETE는 멱등이고 POST는 아니다. 왜 그런지, 그리고 POST를 안전하게 재시도하려면 무엇이 필요한지는 멱등성 글이 따로 다룬다. 여기서는 “메서드마다 재시도해도 되는지가 다르다”까지만 알면 된다.

상태 코드는 앞자리로 갈린다

응답 첫 줄의 세 자리 숫자다. 외울 게 많아 보이지만 앞자리 하나만 보면 누구 잘못인지가 갈린다.

diagramdiagram

이 구분이 실제로 하는 일이 있다. 장애를 볼 때 4xx가 치솟으면 클라이언트나 API 계약을 의심하고, 5xx가 치솟으면 서버를 의심한다. 앞자리 하나로 수사 방향이 갈린다.

자주 보는 것만 추리면 이 정도다.

코드흔한 상황
200 OK됐다조회 성공
201 Created만들었다생성 성공
204 No Content됐는데 줄 게 없다삭제 성공
301 / 302주소가 바뀌었다리다이렉트
304 Not Modified안 바뀌었으니 갖고 있던 걸 써라캐시
400 Bad Request요청이 잘못됐다필수 값 누락
401 Unauthorized누군지 모르겠다로그인 안 함
403 Forbidden누군지는 아는데 권한이 없다남의 자원 접근
404 Not Found그런 건 없다잘못된 경로
500 Internal Server Error서버가 터졌다처리 중 예외

상태 코드는 고르는 게 어렵다

표를 외우는 건 쉽다. 어려운 건 내 상황이 어느 칸인지 정하는 일이다. 로그인을 안 한 것과 권한이 없는 것, 형식이 깨진 것과 값이 규칙에 안 맞는 것 사이에서 매번 갈린다.

이건 HTTP가 정해주는 게 아니라 API를 설계하면서 정하는 일이라, 상태 코드와 에러 응답이 갈림길별로 따로 다룬다.

주의

하나만 미리 못 박는다. 제일 흔한 사고는 에러를 200으로 주는 것이다. 바디에 {"success": false}를 담고 상태 코드는 200을 준다. 사람 눈에는 멀쩡해 보이지만, 상태 코드만 보는 것들이 전부 속는다. 모니터링은 에러율 0%를 그리고, 재시도 로직은 실패를 성공으로 알고 넘어가고, 캐시는 에러 응답을 정상 응답으로 저장한다.

헤더가 실제로 하는 일

헤더는 “부가 정보”라고 뭉뚱그리기 쉬운데, 실제로는 협상의 도구다. 클라이언트가 “나는 이런 걸 받을 수 있다”고 말하면 서버가 그중에서 고른다.

http
Accept: application/json
Accept-Language: ko-KR
Accept-Encoding: gzip

이렇게 보내면 서버는 JSON으로, 가능하면 한국어로, gzip으로 압축해서 답한다. 응답 헤더는 그 결과를 다시 알려준다.

http
Content-Type: application/json
Content-Language: ko-KR
Content-Encoding: gzip

Accept-*가 요청 쪽의 희망사항이고 Content-*가 응답 쪽의 실제다. 짝을 이룬다는 걸 알면 헤더 이름을 외울 일이 줄어든다.

그리고 하나 더. Host 헤더는 뺄 수 없다. 서버 컴퓨터 하나에 사이트가 여러 개 올라가 있으면, 요청이 도착했을 때 어느 사이트를 달라는 건지 알 방법이 이 헤더뿐이기 때문이다.

요청은 서버 안에서 어디를 지나나

지금까지는 선을 타고 오가는 글자만 봤다. 그 글자가 서버에 도착한 뒤에는 어떻게 되나.

diagramdiagram

여기서 눈여겨볼 건 가운데 칸이다. 로그를 남기고, 인증을 확인하고, 압축을 거는 일은 핸들러마다 할 일이 아니라 모든 요청이 똑같이 겪는 일이다. 그래서 핸들러 앞뒤로 공통 처리 구간을 따로 세운다. 그 구간을 다루는 게 미들웨어 글이다.

이 구조를 알아두면 뒤가 편하다. 캐시 헤더를 붙이는 일도, HTTPS를 벗기는 일도, 압축하는 일도 대체로 핸들러가 아니라 이 길목에서 일어난다.

실무에서 이 왕복을 어디서 보나

개발자도구 말고도 이 메시지를 마주칠 자리가 많다.

  • 서버 액세스 로그. 대부분 메서드 · 경로 · 상태코드 · 응답시간 순으로 남는다. 이 글에서 본 그대로다. 장애가 나면 여기서 상태 코드 분포부터 본다.
  • curl. curl -i https://example.com/api/products 를 치면 응답 헤더와 바디가 그대로 나온다. -v를 붙이면 보낸 요청까지 보인다. 브라우저를 거치지 않아 브라우저가 몰래 붙이는 것들을 걷어낸 순수한 왕복을 볼 수 있다.
  • API 게이트웨이·로드밸런서. 요청이 앱에 닿기 전에 이런 것들을 지나면서 헤더가 붙는다. 원래 클라이언트 IP를 알려주는 X-Forwarded-For가 대표적이다. 앱이 보는 요청은 클라이언트가 보낸 것과 이미 다를 수 있다.
  • 에러 응답 형식. 상태 코드만으로는 “왜”를 못 담아서, 바디에 에러 코드와 메시지를 함께 담는 게 보통이다. 이때도 상태 코드는 정직하게 주고, 상세는 바디에 담는다. 위에서 본 “에러를 200으로 주지 마라”가 여기서 지켜진다.

정리

한 왕복클라이언트가 묻고 서버가 답한다. 서버가 먼저 말 걸지 않는다
메시지 구조시작줄 · 헤더 · 빈 줄 · 바디. 요청과 응답이 같은 모양
메서드무엇을 하려는지. 약속이지 강제가 아니다
상태 코드앞자리로 갈린다. 4xx는 보낸 쪽, 5xx는 받은 쪽
헤더부가 정보가 아니라 협상. Accept-*Content-*가 짝
서버 안에서공통 처리 구간을 지나 핸들러로 간다

한 왕복의 구조를 봤다. 그런데 이 왕복에는 이상한 성질이 하나 있다. 방금 로그인한 사람이 다음 요청을 보내도, 서버는 그가 누구인지 모른다. 요청 하나하나가 서로를 모른 채 도착하기 때문이다. 왜 그렇게 만들었고 그래서 무엇이 필요해졌는지가 다음 글이다.

관련 글