화면이 잘못 나올 때 가장 먼저 열어 보아야 할 것은 코드가 아니라 실제로 오간 메시지다. 브라우저와 서버 사이를 지나간 요청과 응답 안에 원인의 대부분이 이미 적혀 있기 때문이다. 그 메시지를 읽으려면 먼저 요청과 응답이 어떤 부분들로 짜여 있는지 알아야 한다. 이번 편은 그 구조를 부분별로 해부한다.
HTTP 메시지는 겉보기에 복잡해 보이지만 뜯어 보면 매우 규칙적인 틀을 따른다. 요청이든 응답이든 시작 줄, 헤더, 본문이라는 세 부분으로 이루어지고, 각 부분의 위치와 경계가 명확히 정해져 있다. 이 틀을 한 번 익히면 어떤 요청과 응답을 마주쳐도 같은 방식으로 읽어 낼 수 있다. 도구가 내용을 예쁘게 정리해 보여 주더라도, 그 아래에는 언제나 이 원시 구조가 깔려 있다.
메시지의 세 부분
모든 HTTP 메시지는 세 부분으로 나뉜다. 첫 줄에 해당하는 시작 줄, 그 아래에 이어지는 헤더 묶음, 그리고 빈 줄 하나를 사이에 두고 오는 본문이다. 이 순서는 고정되어 있다. 시작 줄이 메시지의 의도나 결과를 밝히고, 헤더가 그 처리에 필요한 부가 정보를 담으며, 본문이 실제로 실어 나르는 데이터를 품는다.
요청과 응답은 이 세 부분의 틀을 공유하되 시작 줄의 성격만 다르다. 요청의 시작 줄은 무엇을 원하는지를 밝히는 요청 라인이고, 응답의 시작 줄은 처리 결과를 밝히는 상태 라인이다. 헤더와 본문의 역할은 양쪽에서 동일하다. 이 대칭 덕분에 요청을 이해하면 응답도 절반은 이해한 셈이 된다.
세 부분 중 본문은 없을 수도 있다. 조회를 요청할 때는 대개 보낼 데이터가 없으므로 본문이 비어 있고, 삭제 결과를 알리는 응답도 본문 없이 상태만 돌려줄 수 있다. 반면 시작 줄은 어떤 메시지에도 반드시 존재한다. 헤더는 거의 항상 붙지만, 그 개수와 종류는 메시지마다 다르다.
세 부분을 가르는 경계는 빈 줄 하나다. 헤더 묶음이 끝났음을 알리는 이 빈 줄이 헤더와 본문을 나눈다. 이 경계가 명확하기 때문에, 메시지를 받는 쪽은 어디까지가 부가 정보이고 어디부터가 실제 데이터인지 헷갈리지 않는다. 구조가 단순한 대신 규칙이 엄격하다.
이 세 부분의 모델은 버전이 올라가도 논리적으로 유지된다. 최신 버전이 메시지를 이진으로 압축해 보내더라도, 개발자 도구는 그것을 다시 시작 줄과 헤더와 본문으로 복원해 보여 준다. 그래서 구조를 익힐 때는 이 텍스트 모델을 기준으로 삼는 것이 가장 효율적이다.
세 부분으로 나뉜 구조는 각 부분의 책임이 겹치지 않도록 설계되어 있다. 의도와 결과는 시작 줄이 책임지고, 처리에 필요한 조건은 헤더가 책임지며, 실어 나를 데이터는 본문이 책임진다. 책임이 이렇게 갈려 있으므로, 어떤 정보가 어느 부분에 놓여야 하는지도 대체로 정해져 있다. 예컨대 인증 정보는 헤더의 몫이지 본문의 몫이 아니고, 실제로 저장할 데이터는 본문의 몫이지 헤더의 몫이 아니다. 이 구분을 흐리면 메시지가 규약의 관습을 벗어나 다루기 어려워진다.
요청 라인의 해부
요청의 첫 줄은 세 개의 토큰으로 이루어진다. 메서드, 대상 경로, 그리고 프로토콜 버전이다. 예를 들어 GET /articles/42 HTTP/1.1 이라는 요청 라인에서 GET 은 조회하겠다는 동작을 뜻하고, /articles/42 는 그 동작의 대상을, HTTP/1.1 은 사용하는 규약의 버전을 나타낸다.
메서드는 요청의 의도를 밝힌다. 자원을 가져오려는지, 새로 만들려는지, 바꾸려는지, 지우려는지가 이 한 단어에 담긴다. 같은 경로라도 메서드가 다르면 전혀 다른 동작이 된다. GET /articles/42 는 42번 글을 조회하지만 DELETE /articles/42 는 같은 글을 삭제한다. 메서드의 종류와 의미는 다음 편에서 따로 다룬다.
대상 경로는 동작이 향할 자원을 가리킨다. 이 경로는 서버 안에서 자원을 식별하는 주소이며, 뒤에 물음표를 붙여 /articles?page=2 처럼 추가 조건을 실을 수도 있다. 물음표 뒤에 오는 이 부분을 질의 문자열이라고 부르며, 정렬이나 검색어, 쪽 번호 같은 조회 조건을 전달하는 데 쓰인다.
세 번째 토큰인 버전은 이 요청이 어떤 규약을 따르는지 선언한다. 서버는 이 값을 보고 자신이 그 버전을 처리할 수 있는지 판단한다. 실무에서 이 값을 직접 신경 쓸 일은 드물지만, 서버와 클라이언트가 서로 다른 버전을 가정하면 통신이 어긋날 수 있으므로 규약상 빠질 수 없는 정보다.
요청 라인만 정확히 읽어도 그 요청이 무엇을 하려는지가 드러난다. 어떤 동작을 어떤 자원에 대해 요구하는지가 첫 줄에 압축되어 있기 때문이다. 문제를 진단할 때 이 한 줄을 먼저 확인하면, 요청 자체가 잘못 구성된 것인지 아니면 그 뒤의 처리가 잘못된 것인지를 빠르게 가를 수 있다.
응답의 시작 줄
응답의 첫 줄은 상태 라인이라고 부르며, 프로토콜 버전과 상태 코드, 그리고 짧은 설명 문구로 이루어진다. HTTP/1.1 200 OK 라는 상태 라인에서 200 이 처리 결과를 나타내는 숫자 코드이고, OK 는 그 코드를 사람이 읽기 쉽게 풀어 준 설명이다.
상태 코드는 응답의 성격을 세 자리 숫자로 요약한다. 성공했는지, 다른 곳으로 이동해야 하는지, 요청에 문제가 있었는지, 서버에 문제가 생겼는지가 이 숫자의 앞자리로 갈린다. 요청이 성공하면 200 계열이, 자원이 없으면 404 가, 서버 내부에서 문제가 생기면 500 이 돌아온다. 상태 코드의 체계는 뒤의 전용 편에서 자세히 다룬다.
상태 라인 뒤에 붙는 설명 문구는 어디까지나 사람을 위한 보조 정보다. 클라이언트가 판단의 근거로 삼는 것은 숫자 코드이지 설명 문구가 아니다. 그래서 서버가 설명 문구를 조금 다르게 적더라도 동작에는 영향이 없다. 이 점을 알아 두면 코드와 문구 중 무엇을 신뢰해야 하는지 헷갈리지 않는다.
상태 라인은 응답 전체의 요약이기도 하다. 본문을 열어 보기 전에 이 한 줄만 확인해도 요청이 어떻게 처리되었는지 대략 알 수 있다. 그래서 진단의 첫걸음은 대개 상태 코드를 읽는 것에서 시작한다. 코드가 성공을 가리키는데도 화면이 이상하다면 문제는 본문 처리에 있고, 코드 자체가 실패를 가리킨다면 요청이나 서버 쪽을 살펴야 한다.
주의할 점은 상태 코드가 항상 정직하지는 않다는 것이다. 잘못 설계된 서버는 실패했는데도 성공 코드를 돌려주고, 문제의 원인을 본문 안에만 적어 둘 때가 있다. 이런 서버를 상대할 때는 상태 코드만 믿지 말고 본문의 내용까지 함께 확인해야 한다. 규약을 지키지 않은 응답은 진단을 어렵게 만든다.
헤더가 놓이는 자리
시작 줄 아래에는 헤더가 줄줄이 놓인다. 헤더는 이름과 값의 짝으로 이루어지며, 콜론을 사이에 두고 Content-Type: application/json 처럼 적는다. 한 줄에 하나씩, 필요한 만큼 여러 개가 나열된다. 헤더는 메시지의 처리에 필요한 부가 정보를 담는 자리다.
요청 헤더는 서버에게 요청의 맥락을 알려 준다. 어떤 서버에 보내는지, 어떤 형식의 응답을 원하는지, 어떤 자격 증명을 지니고 있는지 같은 정보가 여기 실린다. Host 는 목적지 서버를, Accept 는 원하는 응답 형식을, Authorization 은 신분 증명을 담는 대표적인 요청 헤더다.
응답 헤더는 클라이언트에게 응답의 성격을 설명한다. 본문이 어떤 형식인지, 길이가 얼마인지, 얼마 동안 저장해 두어도 되는지 같은 정보가 여기 담긴다. Content-Type 은 본문의 형식을, Content-Length 는 본문의 길이를, Cache-Control 은 저장 규칙을 알린다. 헤더 각각의 의미는 뒤의 헤더 편에서 본격적으로 다룬다.
헤더의 이름은 대소문자를 구분하지 않는다. Content-Type 이든 content-type 이든 같은 헤더로 취급된다. 그래서 코드에서 헤더를 다룰 때는 이름의 대소문자에 의존하지 않도록 주의해야 한다. 값의 형식은 헤더마다 다르므로, 각 헤더가 어떤 형식을 요구하는지는 그 헤더의 정의를 따라야 한다.
헤더는 개수의 제한이 규약상 명시되어 있지 않지만, 서버는 대개 전체 헤더의 크기에 상한을 둔다. 헤더가 지나치게 커지면 서버가 요청을 거부하기도 한다. 쿠키가 계속 쌓여 요청 헤더가 비대해지면 이런 문제가 실제로 발생한다. 헤더는 가볍게 유지하는 편이 안전하다.
본문과 그 경계
헤더 묶음이 끝나고 빈 줄 하나가 지나면 본문이 시작된다. 본문은 실제로 실어 나르는 데이터가 담기는 자리다. 폼으로 제출한 값, 서버가 돌려준 문서, 주고받는 데이터 구조가 모두 이 본문에 실린다. 본문의 형식은 헤더의 Content-Type 이 무엇으로 선언되었는지에 따라 정해진다.
본문이 있는지 없는지는 메시지의 성격에 달렸다. 조회를 요청하는 GET 은 대개 본문이 없고, 데이터를 보내는 POST 나 PUT 은 본문에 그 데이터를 싣는다. 응답도 마찬가지여서, 삭제 성공을 알리는 응답은 본문 없이 상태만 돌려줄 수 있다. 본문의 유무를 잘못 가정하면 데이터가 비어 있는 이유를 엉뚱한 데서 찾게 된다.
본문의 경계를 정하는 방식은 두 가지다. 하나는 Content-Length 헤더로 본문의 길이를 미리 알려 주는 방식이고, 다른 하나는 길이를 미리 알 수 없을 때 조각으로 나누어 보내는 방식이다. 받는 쪽은 이 정보를 보고 본문이 어디서 끝나는지 판단한다. 이 경계 정보가 어긋나면 본문이 잘리거나 다음 메시지와 뒤섞인다.
본문의 형식이 헤더의 선언과 다르면 받는 쪽이 해석에 실패한다. 서버가 데이터 구조를 돌려주면서 형식을 문서로 잘못 선언하면, 클라이언트는 그것을 문서로 해석하려다 오류를 낸다. 그래서 본문을 다룰 때는 그 형식을 알리는 헤더가 정확한지 함께 확인해야 한다. 본문과 그 형식 선언은 언제나 짝으로 움직인다.
본문의 크기가 크면 처리 방식도 달라진다. 큰 파일을 한 번에 메모리에 올리는 대신, 조각으로 나누어 받아 처리하는 방식이 필요해진다. 본문이 언제나 작으리라 가정하고 설계하면 대용량 데이터에서 문제가 생긴다. 본문은 세 부분 중 크기가 가장 유동적인 부분이라는 점을 염두에 두어야 한다.
요청과 응답을 읽는 습관
구조를 알면 메시지를 읽는 순서가 잡힌다. 요청을 볼 때는 요청 라인으로 의도를 파악하고, 헤더로 맥락을 확인한 뒤, 본문으로 실제 데이터를 살핀다. 응답을 볼 때는 상태 라인으로 결과를 확인하고, 헤더로 응답의 성격을 파악한 다음, 본문으로 내용을 검토한다. 이 순서가 몸에 배면 진단 속도가 눈에 띄게 빨라진다.
문제의 위치를 좁히는 데도 이 구조가 쓰인다. 요청 라인과 요청 헤더가 예상대로인데 응답이 실패하면 문제는 서버 쪽에 있다. 반대로 요청 자체가 잘못 구성되어 있으면 서버가 아무리 정상이어도 원하는 응답이 오지 않는다. 요청과 응답을 나란히 놓고 비교하는 것만으로 책임 소재가 대개 드러난다.
개발자 도구의 네트워크 탭은 이 구조를 그대로 펼쳐 보여 준다. 각 요청을 선택하면 요청 라인과 헤더, 본문, 그리고 그에 대응하는 응답의 상태와 헤더, 본문을 한자리에서 확인할 수 있다. 추상적인 설명보다 실제 메시지 하나를 이 순서대로 짚어 보는 편이 구조를 이해하는 데 훨씬 빠르다.
구조를 안다는 것은 곧 어디를 손대야 하는지 안다는 것과 같다. 응답 형식을 바꾸고 싶으면 헤더의 형식 선언을 살펴야 하고, 보내는 데이터가 서버에 닿지 않으면 본문과 그 길이 정보를 확인해야 하며, 요청이 엉뚱한 자원으로 향하면 요청 라인의 경로를 다시 봐야 한다. 증상을 구조의 어느 부분과 연결 짓는 감각이 잡히면, 같은 문제를 두고 헤매는 시간이 크게 줄어든다. 구조에 대한 이해는 결국 문제를 다루는 지도가 된다.
이번 편은 요청과 응답이 시작 줄, 헤더, 본문이라는 세 부분으로 짜인다는 사실과 각 부분의 역할, 그리고 그것을 읽는 순서를 다루었다. 이 세 부분의 틀은 앞으로 이어질 모든 편의 바탕이 된다. 메서드는 시작 줄에서 다루고, 상태 코드도 시작 줄에서 읽으며, 쿠키와 캐시와 협상은 모두 헤더의 이야기이기 때문이다. 다음 편에서는 요청 라인의 첫 토큰인 메서드를 파고들어, 조회와 생성과 수정과 삭제가 각각 어떤 메서드로 표현되고 어떤 성질을 갖는지 살펴본다.