메서드가 요청의 의도를 밝히고 상태 코드가 결과를 요약한다면, 그 사이의 세밀한 맥락을 실어 나르는 것은 헤더다. 어떤 형식의 응답을 원하는지, 본문이 무엇으로 이루어졌는지, 응답을 얼마나 저장해도 되는지 같은 정보가 전부 헤더에 담긴다. 헤더는 메시지의 부가 정보를 담는 자리이지만, 실제로는 웹의 많은 동작이 이 헤더의 값 하나로 결정된다.
헤더의 종류는 매우 많고 계속 늘어난다. 그러나 그것을 전부 외울 필요는 없다. 헤더가 어떤 역할별로 나뉘는지, 각 부류가 무엇을 담당하는지를 이해하면 처음 보는 헤더도 그 이름과 맥락으로 뜻을 짐작할 수 있다. 이번 편은 헤더의 형식과 역할별 분류, 그리고 헤더를 다룰 때 지켜야 할 원칙을 정리한다.
헤더의 역할과 형식
헤더는 이름과 값의 짝으로 이루어진다. 콜론을 사이에 두고 Content-Type: text/html 처럼 적으며, 시작 줄 아래에 한 줄에 하나씩 나열된다. 이름은 그 헤더가 무엇에 관한 것인지를 나타내고, 값은 구체적인 설정을 담는다. 이 단순한 형식이 수백 가지 헤더에 공통으로 적용된다.
헤더의 이름은 대소문자를 구분하지 않는다. Content-Type 이든 content-type 이든 같은 헤더로 취급되므로, 코드에서 헤더를 읽을 때 이름의 대소문자에 의존하면 안 된다. 반면 값은 헤더마다 형식이 다르다. 어떤 값은 단순한 문자열이고, 어떤 값은 쉼표로 여러 항목을 나열하며, 어떤 값은 세미콜론으로 부가 옵션을 덧붙인다.
헤더는 크게 요청에 붙는 것과 응답에 붙는 것으로 나뉘고, 그 안에서 다시 역할별로 세분된다. 요청의 맥락을 설명하는 헤더, 응답의 성격을 설명하는 헤더, 본문의 형식을 설명하는 헤더, 보안 정책을 지시하는 헤더가 각각의 부류를 이룬다. 이 역할별 분류를 기준으로 삼으면 방대한 헤더 목록이 몇 개의 갈래로 정리된다.
같은 이름의 헤더가 요청과 응답 양쪽에 쓰이기도 한다. 예를 들어 본문의 형식을 알리는 헤더는 요청에서는 보내는 데이터의 형식을, 응답에서는 돌려주는 데이터의 형식을 나타낸다. 방향에 따라 의미가 조금씩 달라지므로, 헤더를 해석할 때는 그것이 요청에 붙었는지 응답에 붙었는지도 함께 봐야 한다.
헤더의 값에는 부가 옵션이 붙기도 한다. 형식을 알리는 헤더에 세미콜론을 붙여 문자 인코딩을 함께 지정하는 것이 그런 예다. 이렇게 하나의 헤더가 주된 값과 부가 옵션을 동시에 담을 수 있으므로, 값을 해석할 때는 구분자를 기준으로 각 부분을 나누어 읽어야 한다. 값의 구조가 헤더마다 다르다는 점 때문에, 헤더를 다루는 코드는 그 헤더의 정의된 문법을 따라야 한다. 단순히 문자열로 비교하다가는 부가 옵션이 붙은 값을 놓치는 실수가 생긴다.
요청을 설명하는 헤더
요청 헤더는 서버에게 이 요청의 맥락을 알려 준다. 가장 기본적인 것은 목적지 서버를 지정하는 Host 다. 한 서버가 여러 도메인을 함께 호스팅할 때, 서버는 이 헤더를 보고 어느 사이트에 대한 요청인지 판단한다. 이 헤더가 없으면 같은 주소로 여러 사이트를 운영할 수 없다.
클라이언트가 원하는 응답의 성격을 밝히는 헤더들도 있다. Accept 는 어떤 형식의 응답을 원하는지, Accept-Language 는 어떤 언어를 선호하는지를 서버에 전한다. 서버는 이 값을 참고해 여러 형태 중 가장 적합한 것을 골라 돌려준다. 이 협상 과정은 뒤의 콘텐츠 협상 편에서 자세히 다룬다.
신분을 증명하는 헤더는 인증의 핵심이다. Authorization 헤더에 자격 증명을 실어 보내면, 서버는 그 값을 확인해 요청을 보낸 주체가 누구인지 식별한다. 무상태인 HTTP에서 로그인 상태를 이어 가려면 매 요청마다 이런 증명 정보를 함께 실어야 하는데, 그 자리가 바로 이 헤더다.
요청의 출처와 맥락을 담는 헤더도 있다. 어떤 페이지에서 이 요청이 시작되었는지를 알리는 헤더, 어떤 프로그램이 요청을 보냈는지를 알리는 헤더가 여기에 해당한다. 서버는 이런 정보를 참고해 통계를 내거나, 특정 출처의 요청을 다르게 처리하기도 한다. 다만 이런 값은 클라이언트가 얼마든지 바꿀 수 있으므로 보안 판단의 유일한 근거로 삼아서는 안 된다.
응답을 설명하는 헤더
응답 헤더는 클라이언트에게 이 응답을 어떻게 다루어야 하는지를 알려 준다. 본문의 형식을 알리는 헤더, 본문의 길이를 알리는 헤더, 응답을 얼마나 저장해도 되는지를 지시하는 헤더가 대표적이다. 클라이언트는 이 값들을 보고 본문을 어떻게 해석하고 언제까지 재사용할지 결정한다.
응답을 어떻게 저장할지 지시하는 헤더는 성능에 직결된다. Cache-Control 은 이 응답을 캐시해도 되는지, 얼마나 오래 유지해도 되는지를 지시한다. 이 값 하나가 같은 자원을 매번 서버에서 받을지 저장된 사본으로 대신할지를 가른다. 캐시 관련 헤더는 뒤의 전용 편에서 깊이 다룬다.
새로 만들어진 자원의 위치를 알리거나 다른 주소로 이동을 지시하는 헤더도 있다. 생성이 성공했을 때 그 자원이 어디에 있는지 알려 주는 헤더, 이동 응답에서 새 주소를 담는 헤더가 그것이다. 클라이언트는 이 헤더의 값을 읽어 다음에 어디로 요청을 보낼지 판단한다. 상태 코드와 이 헤더가 짝을 이루어 이동을 완성한다.
서버 자신에 관한 정보를 담는 헤더도 흔하다. 어떤 서버 소프트웨어가 응답했는지, 언제 응답이 만들어졌는지 같은 정보가 여기 실린다. 이런 정보는 진단에 도움이 되지만, 서버의 종류와 버전을 지나치게 자세히 노출하면 공격의 실마리를 줄 수 있으므로 보안상 감추기도 한다. 무엇을 드러내고 무엇을 감출지는 운영의 판단이다.
본문을 설명하는 표현 헤더
본문이 어떤 형태인지를 설명하는 헤더들을 따로 묶어 표현 헤더라고 부른다. 가장 중요한 것은 본문의 형식을 알리는 Content-Type 이다. 이 값이 text/html 이면 본문을 문서로, application/json 이면 데이터 구조로 해석하라는 뜻이다. 받는 쪽은 이 선언을 보고 본문을 어떻게 다룰지 결정한다.
본문의 크기를 알리는 헤더도 표현 헤더에 속한다. 본문의 길이를 미리 알려 주면 받는 쪽은 어디까지가 이 메시지의 본문인지 판단할 수 있다. 길이를 미리 알 수 없을 때는 조각으로 나누어 보내는 방식을 쓰는데, 이 경우에는 그 방식을 알리는 헤더가 대신 붙는다.
본문이 압축되어 있는지를 알리는 헤더도 중요하다. 서버가 본문을 압축해 보낼 때는 어떤 방식으로 압축했는지를 헤더로 알리고, 받는 쪽은 그 방식으로 압축을 풀어 원래 내용을 복원한다. 압축은 전송량을 크게 줄여 주므로, 이 헤더의 협상이 성능에 미치는 영향이 작지 않다.
표현 헤더가 실제 본문과 어긋나면 문제가 생긴다. 데이터 구조를 보내면서 형식을 문서로 잘못 선언하면, 받는 쪽이 그것을 문서로 해석하려다 실패한다. 압축했는데 압축 방식을 잘못 알려도 마찬가지다. 그래서 표현 헤더는 본문의 실제 상태를 정확히 반영해야 하며, 이 둘은 언제나 함께 맞추어야 한다.
보안에 관여하는 헤더
헤더 중에는 브라우저의 보안 동작을 지시하는 것들이 있다. 이 헤더들은 데이터를 나르는 것이 아니라 브라우저에게 특정 보호 조치를 켜라고 명령한다. 응답에 이런 헤더를 붙이면 브라우저가 그 지시에 따라 스스로를 보호하므로, 서버가 몇 줄의 헤더만으로 여러 공격을 막을 수 있다.
암호화된 연결만 쓰도록 강제하는 헤더가 대표적이다. 이 헤더를 받은 브라우저는 정해진 기간 동안 그 사이트에 대해 반드시 암호화된 연결로만 접속한다. 사용자가 실수로 암호화되지 않은 주소를 입력해도 브라우저가 자동으로 안전한 연결로 바꾼다. 중간에서 통신을 가로채려는 시도를 막는 효과가 있다.
어떤 출처의 자원을 불러올 수 있는지를 제한하는 헤더도 강력한 보호 수단이다. 이 헤더로 허용된 출처를 명시하면, 브라우저는 그 밖의 출처에서 스크립트나 자원을 불러오지 못하게 막는다. 악성 스크립트가 몰래 삽입되어 외부로 데이터를 빼돌리는 공격을 이 헤더가 상당 부분 차단한다.
다른 사이트가 우리 페이지를 자기 화면 안에 끼워 넣는 것을 막는 헤더, 브라우저가 본문의 형식을 임의로 추측하지 못하게 하는 헤더도 보안 헤더에 속한다. 이런 헤더들은 각각 특정 공격 유형을 겨냥한다. 여러 개를 함께 붙여 두면 방어가 겹겹이 쌓이므로, 서비스를 운영한다면 이 헤더들을 기본으로 갖추는 것이 좋다.
사용자 정의 헤더와 관례
규약이 정의한 헤더 외에, 서비스가 자체적으로 만든 헤더를 붙일 수도 있다. 요청을 추적하기 위한 고유 식별자나 내부 처리에 필요한 정보를 담는 헤더가 그런 예다. 이런 사용자 정의 헤더는 그 서비스 안에서만 통용되므로, 이름을 명확히 짓고 그 의미를 문서로 남겨 두어야 한다.
과거에는 사용자 정의 헤더의 이름 앞에 특정 접두사를 붙이는 관례가 있었다. 그러나 이 관례는 나중에 권장되지 않는 방향으로 정리되었다. 표준 헤더가 되었을 때 이름을 바꾸어야 하는 번거로움이 생겼기 때문이다. 지금은 접두사 없이 의미가 분명한 이름을 쓰되, 표준 헤더와 겹치지 않도록 주의하는 편이 낫다.
사용자 정의 헤더를 남발하면 관리가 어려워진다. 헤더가 늘어날수록 요청과 응답이 무거워지고, 그 의미를 아는 사람만 코드를 이해할 수 있게 된다. 그래서 표준 헤더로 표현할 수 있는 것은 표준을 쓰고, 정말로 필요한 경우에만 자체 헤더를 만드는 절제가 필요하다. 헤더는 늘리기는 쉽지만 줄이기는 어렵다.
중간의 프록시나 캐시 서버가 사용자 정의 헤더를 어떻게 다룰지도 고려해야 한다. 어떤 중간 장비는 알지 못하는 헤더를 그대로 전달하지만, 어떤 장비는 걸러 낼 수도 있다. 그래서 중요한 정보를 자체 헤더에만 의존해 전달하는 설계는 위험하다. 헤더가 목적지까지 온전히 도달하는지 확인하는 것이 안전하다.
헤더를 다루는 원칙
헤더를 다룰 때 첫 번째 원칙은 값을 신뢰하기 전에 출처를 확인하는 것이다. 요청 헤더의 상당수는 클라이언트가 임의로 조작할 수 있다. 출처를 알리는 헤더나 클라이언트를 식별하는 헤더는 위조가 가능하므로, 보안이 걸린 판단을 이런 값에만 의존해서는 안 된다. 헤더는 참고 정보이지 신뢰의 근거가 아닐 때가 많다.
두 번째 원칙은 헤더를 가볍게 유지하는 것이다. 요청 헤더가 커지면 매 요청마다 그만큼의 데이터가 실려 나가 성능을 갉아먹는다. 쿠키가 계속 쌓여 헤더가 비대해지는 것이 대표적인 사례다. 꼭 필요한 헤더만 남기고, 매 요청에 실릴 이유가 없는 정보는 다른 방식으로 옮기는 것이 좋다.
세 번째 원칙은 헤더의 의미를 정확히 지키는 것이다. 형식을 알리는 헤더에는 정확한 형식을, 길이를 알리는 헤더에는 정확한 길이를 담아야 한다. 헤더의 값이 실제와 어긋나면 받는 쪽이 잘못된 가정으로 동작하고, 그 오류는 원인을 찾기 어려운 형태로 나타난다. 헤더는 사소해 보여도 그 값 하나가 전체 처리의 방향을 바꾼다.
헤더를 이해한다는 것은 결국 메시지의 어느 부분이 어떤 정보를 책임지는지를 안다는 뜻이다. 형식은 표현 헤더가, 저장 규칙은 캐시 헤더가, 신분은 인증 헤더가, 보호는 보안 헤더가 각각 맡는다. 어떤 동작을 조정하고 싶을 때 어느 헤더를 손대야 하는지가 이 분류에서 곧바로 나온다. 응답이 캐시되지 않게 하려면 캐시 헤더를, 특정 형식을 강제하려면 표현 헤더를 살피면 된다. 헤더의 지도를 머릿속에 그려 두면 문제의 해결책이 어느 헤더에 있는지 빠르게 좁혀진다.
이번 편은 헤더의 형식과 역할별 분류, 그리고 헤더를 다루는 원칙을 정리했다. 헤더 중에서도 상태를 유지하는 데 쓰이는 쿠키는 그 쓰임이 특별하고 다룰 내용이 많다. 다음 편에서는 쿠키와 세션을 따로 떼어, 무상태인 HTTP에서 어떻게 로그인 상태가 이어지는지를 파고든다.