웹에서 벌어지는 거의 모든 동작은 한 문장으로 요약된다. 클라이언트가 요청을 보내고 서버가 응답을 돌려준다. 화면에 나타나는 목록, 버튼을 눌렀을 때 바뀌는 데이터, 이미지가 뜨는 과정까지 전부 이 단순한 왕복의 반복이다. 그 왕복의 규칙을 정해 둔 약속이 HTTP다. 이 시리즈는 그 약속의 내부를 한 겹씩 열어 구조와 원리를 설명하는 데 목적을 둔다.


HTTP를 정확히 이해하지 못한 상태로 프레임워크만 익히면, 문제 상황에서 판단의 근거가 사라진다. 응답이 왜 느린지, 왜 로그인이 풀리는지, 왜 데이터가 갱신되지 않는지 같은 질문은 결국 이 규약의 성질로 환원되기 때문이다. 첫 편에서는 HTTP가 무엇을 정의하는 규약인지, 무상태라는 성질이 어떤 결과를 낳는지, 그리고 이 규약이 어떤 계층 위에 놓여 있는지를 다룬다.

규약으로서의 HTTP

HTTP는 하이퍼텍스트 전송 규약의 약자다. 핵심은 전송이 아니라 규약이라는 단어에 있다. 규약은 메시지의 형식과 순서, 각 부분의 의미를 미리 합의해 둔 규칙의 집합이다. 클라이언트와 서버가 이 규칙을 공유하기 때문에, 서로 다른 회사가 만든 소프트웨어끼리도 대화가 성립한다.


이 지점이 HTTP의 본질적 가치다. 브라우저를 만든 조직과 서버를 만든 조직은 서로를 모른다. 그런데도 약속된 형식으로 요청이 오면 서버는 그 뜻을 해석하고 정해진 형식으로 응답한다. 표준이 곧 상호 운용성을 보장한다. 웹이 특정 회사의 제품이 아니라 개방된 생태계로 성장한 이유가 여기에 있다.


규약은 두 역할을 명확히 분리한다. 요청을 시작하는 쪽이 클라이언트이고 응답하는 쪽이 서버다. 브라우저만 클라이언트인 것은 아니다. 모바일 앱, 다른 서버, 명령줄 도구, 검색 엔진의 수집기 모두 요청을 보내는 순간 클라이언트가 된다. 대화는 언제나 클라이언트가 먼저 걸고 서버가 답하는 단방향 개시 구조를 따른다.


초기의 HTTP는 문서 전송을 위해 설계되었다. 그러나 규약 자체는 전송하는 내용의 종류를 가리지 않는다. 문서, 이미지, 영상, 데이터, 압축 파일 무엇이든 같은 요청과 응답의 틀에 담겨 오간다. 오늘날 애플리케이션이 주고받는 데이터 교환도 결국 이 틀 안에서 이루어진다. 규약이 내용에 무관하게 설계되었기 때문에 확장이 가능했다.


규약을 다룬다는 것은 곧 형식을 다룬다는 뜻이다. 어떤 자원을 어떤 주소로 지정하고, 어떤 동작을 어떤 메서드로 표현하며, 결과를 어떤 상태 코드로 알릴지가 전부 이 규약 안에 정의되어 있다. 이 정의를 정확히 따르는 것만으로 시스템의 예측 가능성이 크게 올라간다.


규약이 개방되어 있다는 점도 짚어 둘 만하다. HTTP의 명세는 특정 기업의 소유가 아니라 공개된 표준 문서로 관리된다. 누구든 그 문서를 읽고 규약을 준수하는 클라이언트나 서버를 만들 수 있다. 이 개방성 때문에 브라우저, 서버, 프록시, 캐시 서버, 배포망 같은 다양한 구성 요소가 같은 규약을 공유하며 하나의 흐름 안에서 협력한다. 규약이 사유물이었다면 이런 생태계는 형성되지 못했다.

무상태라는 설계 결정

HTTP의 가장 중요한 성질은 무상태다. 서버는 이전 요청을 기본적으로 기억하지 않는다. 각 요청은 그 자체로 완결되며, 서버가 요청을 처리하는 데 필요한 정보는 그 요청 안에 모두 담겨 있어야 한다. 앞선 요청과의 연결은 규약 차원에서 존재하지 않는다.


이 성질은 언뜻 불편해 보인다. 로그인 이후의 페이지가 사용자를 알아보는 것은 서버가 기억하기 때문이 아니라, 매 요청마다 신분을 증명하는 정보가 함께 실려 오기 때문이다. 서버는 그 정보를 보고 그때그때 사용자를 식별한다. 상태를 유지하는 책임은 규약이 아니라 그 위에 올린 별도의 장치가 진다.


무상태가 주는 이점은 확장성이다. 서버가 아무것도 기억하지 않으므로 요청은 어느 서버로 흘러가도 동일하게 처리된다. 덕분에 같은 역할의 서버를 여러 대로 늘려 부하를 분산할 수 있고, 앞단에서 요청을 고르게 나누기만 하면 된다. 만약 각 서버가 서로 다른 상태를 품고 있었다면 이런 수평 확장은 성립하지 못한다.


대신 상태를 이어 가려면 보조 수단이 필요하다. 로그인 상태를 유지하려면 쿠키나 토큰으로 신분 정보를 매 요청에 실어 보내야 한다. 서버가 비워 둔 기억의 자리를 이런 수단이 대신 채운다. 이 부분은 뒤의 쿠키와 세션 편에서 자세히 다룬다.


설계에서 무상태를 어기면 확장성이 무너진다. 특정 서버의 메모리에 사용자 정보를 저장하고 다음 요청도 같은 서버로 오리라 가정하는 구조가 전형적인 실수다. 서버가 한 대일 때는 문제가 드러나지 않다가, 여러 대로 늘리는 순간 요청이 다른 서버로 흩어지면서 상태가 사라진다. 이 요청 하나만으로 처리가 완결되는가를 설계 초기에 확인하는 습관이 그래서 중요하다.


무상태는 장애 대응에서도 이점을 준다. 서버 한 대가 죽어도 그 서버가 품고 있던 상태가 따로 없으므로, 남은 서버들이 요청을 이어받는 데 아무 지장이 없다. 상태를 서버 바깥의 공유 저장소나 요청 자체에 두면, 개별 서버는 언제든 교체 가능한 부품이 된다. 무상태를 제약으로만 보면 불편하지만, 확장과 복구를 함께 얻는 설계 원칙으로 보면 그 가치가 분명해진다.

텍스트 기반 메시지 모델

HTTP 메시지는 본래 사람이 읽을 수 있는 텍스트 구조를 따른다. 첫 줄에 무엇을 원하는지 또는 결과가 무엇인지를 적고, 그 아래에 부가 정보인 헤더를 나열하며, 빈 줄 하나를 둔 뒤 실제 내용인 본문을 담는다. 이 세 부분의 구획이 요청과 응답 모두에 공통으로 적용된다.


예를 들어 특정 글을 요청하는 메시지는 다음과 같은 첫 줄로 시작한다. GET /articles/42 HTTP/1.1 은 42번 글을 조회하겠다는 의도를 담은 요청 라인이다. 그 아래에는 Host: example.com 처럼 어느 서버에 보내는지, 어떤 형식의 응답을 원하는지 같은 헤더가 이어진다. 응답은 HTTP/1.1 200 OK 처럼 처리 결과를 담은 상태 라인으로 시작한다.


메시지가 텍스트라는 사실은 학습과 진단 모두에 유리하다. 내부가 감춰진 채 동작하는 것이 아니라 오가는 내용을 그대로 열어볼 수 있으므로, 요청이 잘못 나갔는지 응답이 이상한지를 눈으로 확인할 수 있다. 브라우저 개발자 도구의 네트워크 탭은 이 원시 메시지를 사람이 읽는 형태로 펼쳐 보여 준다.


최신 버전에서는 성능을 위해 이 메시지를 압축하고 이진 형태로 인코딩해 전송한다. 그러나 그것은 전송 계층의 최적화일 뿐, 요청 라인과 헤더와 본문이라는 논리적 모델은 그대로 유지된다. 개발자 도구는 이진으로 오간 내용도 텍스트 모델로 복원해 보여 준다. 개념을 익힐 때는 이 텍스트 모델을 기준으로 삼으면 버전과 무관하게 통한다.


이 구조를 알면 문제 해결의 출발점이 분명해진다. 화면이 잘못 나올 때 추측을 먼저 하지 않고, 그 화면을 채운 요청과 응답을 열어 본다. 원인은 대개 세 곳 중 하나에 있다. 요청이 잘못 구성되었거나, 응답이 예상과 다르거나, 응답을 받은 뒤 화면에서 처리가 틀린 경우다. 원시 메시지를 확인하는 것만으로 원인의 상당 부분이 좁혀진다.


텍스트 모델의 또 다른 미덕은 배우는 사람이 규약의 내부를 직접 관찰할 수 있다는 데 있다. 감춰진 채 도는 이진 흐름은 도구 없이 들여다보기 어렵지만, 텍스트로 표현된 요청과 응답은 그 자체가 설명이 된다. 첫 줄만 읽어도 무엇을 원한 요청이고 어떤 결과의 응답인지가 드러나고, 헤더의 이름만 훑어도 어떤 정보가 오가는지 짐작이 선다. 규약을 명세 문서로 배우기 전에 실제로 오간 메시지를 눈으로 먼저 익히는 편이 빠른 이유가 여기에 있다. 추상적인 규칙보다 구체적인 사례가 먼저 남는다.

버전의 진화와 불변의 구조

HTTP는 한 번 정해지고 멈춘 규약이 아니라 수십 년에 걸쳐 개정되어 왔다. 오랫동안 웹을 떠받친 것은 1997년에 자리 잡은 HTTP 1.1이다. 이 버전은 하나의 연결로 여러 요청을 잇달아 보내는 방식을 도입해 초기 웹의 성능을 끌어올렸다. 지금도 많은 통신이 이 위에서 이루어진다.


2015년에 표준이 된 HTTP 2는 메시지를 이진 형태로 바꾸고, 한 연결 안에서 여러 요청을 동시에 실어 나르는 다중화를 도입했다. 이전 버전은 앞선 요청이 끝나야 다음 요청이 진행되는 병목을 안고 있었는데, 다중화가 그 제약을 상당 부분 걷어냈다. 헤더를 압축해 반복되는 정보의 낭비를 줄인 것도 이 버전의 성과다.


2022년에 표준이 된 HTTP 3은 연결의 밑바탕을 교체했다. 신뢰성 있는 연결을 제공하던 기존 전송 방식 대신, 더 가볍고 유연한 새 전송 방식 위에서 동작한다. 이 변화로 연결을 맺는 시간이 짧아지고, 한 요청의 지연이 다른 요청을 가로막던 문제도 완화되었다. 전송 계층의 세부는 별도의 편에서 다룬다.


이 진화 속에서도 개발자가 다루는 개념은 거의 바뀌지 않았다는 점이 중요하다. 요청과 응답, 메서드와 상태 코드, 헤더와 본문이라는 논리적 구조는 버전이 올라가도 유지된다. 바뀌는 것은 그 아래에서 데이터를 얼마나 빠르고 안전하게 나르느냐다. 개념을 한 번 익혀 두면 새 버전이 나와도 지식이 낡지 않는 이유가 이것이다.


실무에서 버전을 직접 지정할 일은 많지 않다. 앞단의 배포망이나 서버가 최신 버전을 알아서 협상해 주기 때문이다. 다만 어떤 버전으로 연결되어 있는지 확인하는 습관은 필요하다. 성능 문제를 파고들다 보면 예상보다 낮은 버전으로 붙어 있는 경우가 종종 발견되고, 그것이 지연의 원인일 때가 있다.

계층 위의 위치

HTTP는 홀로 동작하지 않는다. 그 아래에는 연결을 맺고 데이터를 신뢰성 있게 나르는 전송 계층이 있고, 다시 그 아래에는 주소를 찾아 경로를 정하는 계층이 있다. HTTP는 이 계층들이 제공하는 통로 위에서 메시지를 주고받는 응용 계층의 규약이다. 각 계층은 자기 책임만 지고 아래 계층의 세부는 감춘다.


이 계층 분리 덕분에 HTTP는 아래에서 무슨 일이 벌어지는지 몰라도 된다. 데이터가 어떤 경로로 전달되고 어떻게 오류를 복구하는지는 하위 계층의 몫이다. HTTP 3가 전송 방식을 통째로 바꿀 수 있었던 것도, 응용 계층과 전송 계층의 책임이 분리되어 있었기 때문이다. 한 계층의 교체가 위 계층의 개념을 흔들지 않는다.


보안 역시 이 계층 구조 위에서 얹힌다. HTTP 메시지를 암호화된 통로 위로 실어 보내면 그것이 우리가 흔히 말하는 보안 연결이다. 메시지의 형식 자체는 그대로이고, 다만 그 메시지가 지나가는 통로가 암호화될 뿐이다. 규약과 보안이 서로 다른 층에서 각자의 역할을 한다는 점을 구분해 두면 혼동이 줄어든다.


이렇게 HTTP를 계층 안에서 파악하면, 각 문제를 어느 층에서 다루어야 하는지가 분명해진다. 연결이 아예 맺어지지 않는 문제는 하위 계층의 영역이고, 요청은 갔는데 응답이 이상한 문제는 응용 계층의 영역이다. 문제를 올바른 층에 배치하는 것만으로 진단의 방향이 잡힌다.


계층 위에 놓였다는 사실은 HTTP가 아래 계층에 의존한다는 뜻이기도 하다. 이름을 숫자 주소로 바꾸는 조회가 실패하면 요청은 애초에 목적지를 찾지 못하고, 전송 계층의 연결이 불안정하면 응답이 중간에 끊긴다. 그래서 HTTP 문제로 보이는 현상이 실제로는 그 아래 계층에서 비롯된 경우가 적지 않다. 규약의 범위를 정확히 알아야 원인이 규약 안에 있는지 밖에 있는지를 구분할 수 있다.


HTTP를 익히는 일이 오래 남는 투자인 이유도 여기서 나온다. 언어와 프레임워크는 몇 년 주기로 바뀌지만, 그 아래에서 오가는 요청과 응답의 규약은 좀처럼 흔들리지 않는다. 유행하는 도구의 사용법은 시간이 지나면 낡지만, 규약의 원리는 낡지 않는다. 밑바탕이 단단할수록 그 위에 어떤 도구를 올리든 흔들림이 적다.


이번 편은 HTTP가 무엇을 정의하는 규약인지, 무상태가 어떤 결과를 낳는지, 텍스트 모델이 왜 유용한지, 버전이 어떻게 진화했는지, 그리고 이 규약이 어느 계층에 놓이는지를 훑었다. 각 주제는 뒤의 편들에서 다시 깊이 파고든다. 다음 편에서는 요청과 응답이 각각 어떤 부분들로 구성되는지, 그 해부도를 한 부분씩 뜯어본다.