인프런 - 모든 개발자를 위한 HTTP 웹 기본 지식에서 김영한 님이 강의해주는 내용을 바탕으로 정리를 진행했습니다. 강의를 직접 듣고 싶으신 분들은 하단 북마크를 눌러주세요.
*제 포스팅에 나오는 모든 이미지는 김영한 님께서 만든 강의 자료에서 가져왔습니다.
목차 보기
들어가며
이번 챕터에서는 헤더에 대해서 알아보도록 하겠습니다. 헤더가 너무 많기 때문에 이번 챕터와 다음 챕터로 나누어서 진행을 하려고 합니다. 이번 챕터에서는 일반적으로 자주 사용하는 헤더에 대해서 알아볼겁니다.
HTTP 헤더
이전 챕터에서 HTTP 헤더가 HTTP 메시지에서 어떤 위치에 있는지, 어떤 모양을 가졌는지 배운 적이 있습니다. [Network] HTTP 기본에서 배웠던 거 같은데요. 한 번 더 복습하고 넘어가봅시다.
전에 HTTP 헤더가 이러한 문법을 따른다고 배웠습니다. 그리고 field-name은 대소문자를 구분하지 않기 때문에 Host,host 모두 같은 뜻을 지닙니다. field-value는 대소문자 구분을 하기 때문에 이 부분을 주의해서 사용해야 합니다.
HTTP 메시지에서 헤더는 시작 라인 다음에 나타납니다. 헤더를 쭉 작성하고 한 칸 띄고 메시지 바디 부분을 작성하게 됩니다.
헤더는 HTTP 전송에 필요한 모든 부가정보를 담고 있는 부분입니다. 시작 라인을 제외한 모든 부가정보를 헤더가 가지고 있습니다. 예를 들어서, 메시지 바디의 내용, 메시지 바디의 크기, 압축 정보, 인증 정보, 요청 클라이언트 정보, 서버 정보, 캐시 관리 정보.. 등등 표준 헤더 필드가 너무 많이 존재합니다. 필요 시에 임의의 헤더 필드를 추가할 수 있기 때문에 표준 헤더 필드를 제외한 추가적인 헤더 필드가 생성될 수도 있습니다.
이러한 HTTP 헤더는 과거(RFC2616)에 크게 4가지로 분류되었습니다.
https://developer.mozilla.org/docs/Web/HTTP/Messages
•
General 헤더 : 메시지 전체에 적용되는 정보(요청, 응답 메시지에 구분없이 적용)
•
Request 헤더 : 요청을 보낼 때 들어가는 헤더
예) User-Agent 정보, 웹 브라우저 정보 …
•
Response 헤더 : 응답을 보낼 때 들어가는 헤더
예) 현재 요청을 받아서 처리하는 서버 정보 …
•
Entity 헤더 : 엔티티 바디 정보
예) Content-Type, Content-Length …
HTTP 메시지 본문도 과거에는 엔티티 본문(entity body)을 전달하는데 사용되었습니다.
엔티티 본문이란 요청이나 응답에서 전달할 실제 데이터입니다. HTTP 메시지 바디에 메시지 본문이 들어있는데, 그 안에 엔티티 본문을 담아서 전송하는겁니다.
엔티티(Entity) 헤더는 엔티티와 관련된 헤더라는 뜻으로 지어졌습니다. 엔티티 헤더 내부에는 엔티티 본문이 Text인지, HTML인지 알 수 있는 데이터 유형에 관한 정보가 들어있고, 엔티티 본문의 길이가 어느정도 되는지에 대한 정보도 들어있습니다. 즉, 엔티티 헤더는 엔티티 본문의 데이터를 해석하기 위한 정보들을 가지고 있습니다.
그런데, 2014년에 RFC2616이 폐기되면서 RFC7230~7235가 등장했습니다. 스펙이 많이 개정되면서 엔티티 바디라는 용어도 사라졌습니다. 그렇다면 이젠 엔티티를 어떻게 부를까요?
표현(Representation)
RFC723x 부터는 엔티티를 표현(Representation)이라고 말합니다.
표현은 표현 메타데이터(Representation Metadata) + 표현 데이터(Representation Data)를 뜻합니다.
그렇다면, HTTP 헤더와 바디의 내용도 어떻게 바뀌었을까요?
최신 버전에서는 메시지 본문을 통해서 표현 데이터를 전달한다고 합니다. 표현은 요청이나 응답에서 전달할 실제 데이터로, 메시지 본문 안에 포함되어 있습니다.
표현 헤더에는 표현 데이터를 해석할 수 있는 정보가 들어있습니다. 예를 들어서 데이터 유형, 데이터 길이, 압축 정보 등등 들어있습니다. 표현 헤더는 표현 메타데이터와 페이로드 메시지를 구분해야 하지만, 일단 여기서는 생략하도록 하겠습니다.
근데 왜 표현(Representation)이라고 했을까요?
예를 들어서, 메시지 본문 데이터에 회원과 관련된 회원 내역을 HTML로 제공해준다고 합시다. 회원이라는 리소스를 HTML로 표현한겁니다. 이번엔 회원 내역을 JSON 타입으로 조회한다고 해봅시다. 그러면 회원이라는 리소스가 JSON으로 표현될겁니다.
즉, 회원이라는 리소스를 HTML이라는 표현으로 전달하던지, JSON이라는 표현으로 전달할거라는 겁니다.
실제 리소스라는 개념은 추상적입니다. 리소스는 DB에 있을 수도 있고, byte 코드로 어딘가에 저장이 되어있을 수도 있고, file 형태로 저장되어 있을 수도 있습니다. 클라이언트와 서버 간에 메시지를 주고 받을 때는 이해할 수 있는 무언가로 데이터를 변환해서 전달해주어야 합니다. 서버가 DB에 있는 바이너리 데이터를 그대로 전달해줄 순 없으니깐요.
그래서 HTML이나 JSON 형태로 서버가 전송을 해주는건데, 이걸 HTML, JSON으로 표현한다라고 하는겁니다.
표현이라는 걸 하려면, 먼저 HTML로 가는지, JSON으로 가는지 알아야겠네요.
이런 표현 데이터를 해석할 수 있는 정보가 있는 곳이 바로 표현 헤더 부분이었습니다. 그렇다면 표현 헤더에는 어떤 헤더 필드가 있는지 알아볼까요?
표현 헤더는 전송, 응답 메시지 모두에서 사용 가능합니다.
•
Content-Type : 표현 데이터 형식
콘텐트 바디에 들어가는 내용이 어떤건지에 대한 정보가 들어있는 부분입니다.
예를 들어서, HTML이 들어간다고 하면 text/html 과 함께 문자 인코딩 관련 정보(charset=UTF-8)가 들어가게 됩니다. JSON 데이터가 들어간다고 하면 application/json이 들어갑니다. JSON 타입은 기본이 UTF-8이기 때문에 따로 인코딩 정보를 적어주지 않아도 됩니다.
•
Content-Encoding : 표현 데이터 압축 방식
표현 데이터를 압축하기 위해 사용하는 헤더 필드입니다.
데이터를 전달하는 곳에서 실제 메시지 바디에 있는 데이터를 gzip 같은 걸로 압축을 하고서 압축을 했다는 정보를 헤더 필드를 통해서 보내줍니다. 데이터를 읽는 쪽에서는 헤더 정보를 읽고서 어떤 걸로 압축했는지 확인하고 압축을 해제합니다. Content-Encoding 필드가 없으면 압축을 했는지, 어떤 형식으로 압축을 해제해야하는지 알 수 없습니다.
Content-Encoding 에 들어가는 필드 벨류 중에 gzip, zip, deflate 등이 있고, identity는 압축을 안했음을 뜻합니다.
•
Content-Language : 표현 데이터의 자연 언어
표현 데이터의 자연 언어, 즉 한국어인지, 영어인지를 표현하는 헤더 필드입니다.
Content-Language 의 필드 벨류로 ko 가 들어온다면 본문에 한국어가 들어있다고 생각하면 되고, en 이 들어온다면 본문에 영어가 적혀있다고 생각하면 됩니다.
우리가 공식 사이트같은 곳을 들어가면 언어를 설정할 수 있는 부분들이 존재합니다. 이렇게 언어를 다르게 설정할 수 있게 부가 작업을 할 수 있도록 해당 헤더 필드가 도와줍니다.
•
Content-Length : 표현 데이터의 길이
데이터의 길이 자체는 표현과 무관하지만 payload 메시지로 구분하면 너무 복잡해져서 그냥 표현 헤더에 담아서 설명하겠습니다.
표현 데이터의 길이를 나타내는 헤더 필드입니다. 길이는 바이트 단위로 나타냅니다.
해당 필드는 Transfer-Encoding 을 사용할 때 같이 사용하면 안됩니다. 왜냐하면 Transfer-Encoding 안에 해당 정보들이 다 들어가 있기 때문에 Content-Length 를 중복으로 사용할 수 없습니다.
콘텐츠 협상(네고시에이션)
콘텐츠 협상은 클라이언트가 선호하는 표현을 서버에게 요청할 때 사용합니다.
클라이언트와 서버가 있다고 합시다. 클라이언트는 원하는 표현으로 달라고 서버에게 요청합니다. 그럼 서버가 “클라이언트가 원하는 우선 순위가 이거니깐. 일단 내가 할 수 있는 한으로 최대한 그 정보를 내려줄게.” 라고 합니다. 그리고 클라이언트가 원하는 걸로 표현 데이터를 만들어서 보내줍니다. 하지만 서버가 클라이언트가 원하는 대로 못 만들어줘도 괜찮습니다.
•
Accept : 클라이언트가 선호하는 미디어 타입을 서버가 전달해주세요!
•
Accept-Charset : 클라이언트가 선호하는 문자 인코딩을 서버가 전송해주세요!
•
Accept-Encoding : 클라이언트가 선호하는 압축 인코딩을 서버가 전송해주세요!
•
Accept-Language : 클라이언트가 선호하는 자연 언어를 서버가 전송해주세요!
요청 시에만 사용하며 최대한 서버에게 해당 데이터를 내려줄 수 있도록 노력해달라고 클라이언트가 요청하는 겁니다.
Accept-Language 필드를 사용해서 협상을 진행하는 예시를 보여드리겠습니다.
상황) 한국어 브라우저를 사용해서 외국 사이트 접근
우리는 한국어 브라우저를 사용해서 /event 사이트를 방문했습니다. 해당 사이트는 다중 언어를 지원하는 서버를 가지고 있고, 영어로 된 데이터를 기본으로 보내주긴 하지만 한국어도 지원합니다.
1.
웹 브라우저는 /event 사이트에 들어갑니다.
2.
서버는 요청을 받았지만 클라이언트가 한국어 브라우저인지, 아닌지에 대한 정보가 없기 때문에 기본값인 영어로 응답 메시지를 내려줍니다.
3.
클라이언트는 한국어가 익숙하기 때문에 응답 메시지에 있는 영어를 보고 당황스러워합니다.
상황) 한국어 브라우저를 사용해서 외국 사이트 접근(협상 적용)
이번에는 한국어 브라우저가 요청 시에 한국어로 보내달라고 서버에 요청할겁니다.
1.
웹 브라우저는 /event 사이트에 들어갑니다. 이번엔 한국어가 있다면 한국어로 데이터를 내려달라고 서버에게 요청값을 보냅니다.
2.
서버는 요청 메시지를 확인하고 Accept-Language 부분에 ko 가 들어있는걸 확인합니다. 서버는 영어말고도 한국어를 지원하기 때문에 한국어 데이터를 클라이언트에게 보내줍니다.
3.
클라이언트는 한국어가 익숙하기 때문에 응답 메시지를 보고 행복해합니다.
협상 헤더를 통해서 클라이언트가 원하는 데이터를 받아올 수 있었네요. 하지만 항상 이렇게 쉽게 협상이 될까요?
상황) 한국어 브라우저를 사용해서 외국 사이트 접근(협상 적용했지만 복잡함)
이번에도 이전 상황과 동일하게 한국어 브라우저가 한국어로 보내달라고 서버에 요청할겁니다. 이번 서버는 다중 언어를 지원하지만 한국어를 포함하지 않습니다. 클라이언트는 한국어를 받을 수 없다면 영어로 받고 싶은 상황입니다.
1.
웹 브라우저는 /event 사이트에 들어갑니다. 한국어가 있다면 한국어로 데이터를 내려달라고 서버에게 요청값을 보냅니다.
2.
서버는 요청 메시지를 확인하고 Accept-Language 부분에 ko 가 들어있는걸 확인합니다. 서버는 한국어를 지원하지 않기 때문에 기본값인 독일어로 데이터를 전송합니다.
3.
클라이언트는 응답 메시지를 보고 몹시 당황합니다.
서버는 클라이언트가 한국어가 없다면 영어라도 받고 싶어한다는 사실을 모릅니다. 이런 사실을 알리기 위해서는 각 언어에 우선 순위를 붙여서 서버에 보내주어야 합니다.
우선순위 ⓵
아까 사용하던 Accept-Language 헤더 필드 벨류 값에 우선 순위를 적용했습니다. 0~1 사이의 값을 가지며, 클수록 높은 우선 순위를 가집니다. 우선 순위 값이 생략되어 있다면 1을 가집니다.
위의 그림에 있는 헤더에 있는 값에 대해서 순위를 매겨볼까요?
1.
ko-KR;q=1
2.
ko;q=0.9
3.
en-US;q=0.8
4.
en;q=0.7
이렇게 나타나는군요. 그럼 우선 순위를 매긴 값을 가지고 복잡했던 저번 상황에 적용시켜봅시다.
상황) 한국어 브라우저를 사용해서 외국 사이트 접근(우선순위 적용)
동일하게 한국어 브라우저가 한국어로 보내달라고 서버에 요청할겁니다. 서버는 다중 언어를 지원하지만 한국어를 포함하지 않습니다. 클라이언트는 한국어를 받을 수 없다면 영어로 받고 싶은 상황입니다.
1.
웹 브라우저는 /event 사이트에 들어갑니다. 우선 순위를 매긴 벨류를 넣어서 서버로 보내줍니다. 클라이언트는 한국어로 데이터를 받고 싶지만 안된다면 영어로 데이터를 받고 싶습니다.
2.
서버는 요청 메시지를 확인하고 Accept-Language 부분에 우선 순위가 매겨져 있는 필드 벨류를 확인합니다. 1, 2, 3순위에 속하는 ko-KR, ko, en-US는 지원하지 않습니다. 마지막 벨류인 en 이 있는걸 확인하고 영어로 데이터를 내려줍니다.
3.
클라이언트는 그나마 영어라서 다행이라고 생각합니다.
우리가 구글에다가 검색을 했을 때에도 Accept-Language에 우선 순위가 포함되어서 요청이 보내지는 걸 확인할 수 있습니다. 저는 한국인인데, 왜 en-US 가 높은거죠? Hi, there!
우선 순위를 정하는 방법은 2가지 더 있습니다.
우선 순위 ⓶
구체적인 것을 우선으로 하는 겁니다. 예를 들어서, 아래 내용처럼 헤더가 들어온다고 해볼게요.
위의 그림에서는 우선 순위가 적혀있진 않지만 어떤 걸 구체적으로 적었나에 따라서 우선 순위가 매겨집니다.
1.
text/plain;format=flowed
2.
text/plain
3.
text/*
4.
*/*
1번이 가장 구체적이기 때문에 우선 순위가 가장 높을 것이고, 4번이 가장 추상적이기 때문에 우선 순위가 가장 낮을겁니다.
우선 순위 ⓷
마지막으로 구체적인 것을 기준으로 미디어 타입을 맞추는 겁니다. 물론 이렇게까지 구체적으로 협상 헤더가 보내질 일은 크게 없습니다.
Accept: text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5
위에 보낸 협상 헤더로 서버에 요청이 들어간다고 할 때, 각 Media Type이 가지게 되는 Quality를 적어보겠습니다.
Media Type | Quality |
text/html;level=1 | 1 |
text/html | 0.7 |
text/plain | 0.3 |
image/jpeg | 0.5 |
text/html;level=2 | 0.4 |
text/html;level=3 | 0.7 |
•
text/html;level=1 은 요청 헤더에서 우선 순위가 생략되어 있기 때문에 q=1 이라는 걸 알 수 있습니다.
•
text/html 은 요청 헤더에서 q=0.7 이라는 걸 알 수 있습니다.
•
text/plain 은 요청에서 똑같은 타입을 찾을 수는 없지만 text/* 가 text/plain 를 포함하고 있기 때문에 q=0.3 이라는 걸 알 수 있습니다.
•
image/jpeg 는 동일한 요청 타입도 없고 대부분의 요청 벨류가 text/* 형식이기 때문에 맞는 값이 없습니다. 마지막에 들어온 */* 가 image/jpeg 를 포함하고 있으므로 q=0.5 가 됩니다.
•
text/html;level=2 은 요청 헤더에 해당 값이 q=0.4 의 값을 가진다고 되어 있기 때문에 그 값을 우선 순위로 가집니다.
•
text/html;level=3 은 동일한 값은 없지만 text/html 에 포함되기 때문에 q=0.7 을 가집니다.
물론 클라이언트가 협상하는대로 다 되는 건 아닙니다. 서버가 데이터를 그런 식으로 제공을 해주지 못한다면 어쩔 수 없는것이죠. 협상 헤더를 사용해서 클라이언트는 최대한 자신이 원하는 우선순위를 나열해서 보내고 서버에서는 협상 헤더를 확인하고 Quality value를 매칭해본 다음에 데이터를 보내주게 됩니다.
어떤 클라이언트 A는 XML 형식으로 데이터를 보내주길 원하고, 어떤 클라이언트 B는 JSON 형식으로 데이터를 보내주길 원한다고 합시다. 이때 Accept 헤더 필드에 A는 XML를 보내고, B는 JSON를 보내게 됩니다. 그러면 서버는 각 클라이언트가 원하는대로 값을 내려주게 됩니다. 물론, 클라이언트가 서버에게 해당 형식을 지원한다는걸 미리 알고 있어야 겠죠.
전송 방식
전송 방식은 단순하게 총 4가지로 분리됩니다. ⓵ 단순 전송, ⓶ 압축 전송, 쪼개서 전송하는 ⓷ 분할 전송, 범위를 지정해서 전송하는 ⓸ 범위 전송 입니다.
단순 전송
단순 전송은 요청하면 응답을 주는겁니다. 단순하게 한 번에 요청하고 한 번에 쭉 받는 방식입니다.
메시지 바디에 대한 Content-Length 를 지정해서 보냅니다. 즉, Content에 대한 길이를 알 수 있을 때에만 사용할 수 있습니다. 전송할 메시지 바디의 길이가 3423줄 이라면 그 값을 Content-Length 에 담아서 보내게 됩니다.
압축 전송
압축 전송은 서버에서 메시지 바디를 gzip 같은 걸로 압축을 한 다음, Content-Encoding에 어떤 형식으로 압축을 했는지 추가로 넣어서 보내주는 방식입니다.
메시지 바디를 압축했기 때문에 실제로 데이터 용량이 굉장히 많이 줄어든다고 합니다.
어떤 형식으로 압축되었는지 보내주는 이유는 어떤 방식으로 압축이 되었는지 알아야지만 받는 쪽에서 압축 형식을 알고 압축 해제를 할 수 있기 때문입니다.
분할 전송
분할 전송은 쪼개서 전송을 하는 방식입니다. Transfer-Encoding 헤더 필드를 사용해서 분할 전송 형식을 보내줍니다.
하단 그림에서는 Transfer-Endcoding 안에 chunked 값을 넣어주었습니다. chunked 는 덩어리 형태로 쪼개서 보낸다는 뜻을 가지고 있습니다.
덩어리 형태로 쪼개서 보낸다는 게 무슨 뜻인가요?
위에서 메시지 본문에 있는 데이터를 노란 박스 형태로 쪼개는 겁니다. 단어 위에 쓰여있는 5는 하단 데이터의 길이를 나타냅니다. 5바이트짜리 Hello, 5바이트짜리 World 를 뜻합니다. 마지막에 나오는 0과 \r\n은 데이터 끝단임을 나타냅니다.
그러면 쪼갠 데이터를 어떻게 전송할까요? 네트워크가 5바이트짜리 Hello를 서버에서 클라이언트로 먼저 보냅니다. 클라이언트는 5바이트짜리 Hello를 다른 데이터들보다 먼저 받아볼 수 있어요.
용량이 되게 큰 데이터를 한 번에 보내면 클라이언트는 큰 데이터가 올 때까지 기다려야 합니다. 하지만 데이터를 분할해서 전송하게 되면 오는대로 바로바로 데이터를 화면에 표시할 수 있기 때문에 좋습니다.
참고로 분할 전송 시에는 Transfer-Encoding 헤더 필드를 사용하기 때문에 Content-Length 헤더 필드를 넣으면 안됩니다. 왜냐하면, Content-Length 를 처음부터 예상할 수 없습니다. 또한 분할해서 보내면서 각 바이트 정보를 각 덩어리들이 가지고 있습니다. 덩어리들마다 길이를 알아서 가지기 때문에 Content-Length가 따로 필요없는 겁니다.
범위 전송
예를 들어서 이미지를 받는 상황을 가정해봅시다. 우리가 이미지를 절반정도 받았는데, 중간에 다운로드가 끊겨서 다시 이미지를 요청해야 하는 상황입니다. 처음부터 이미지를 다시 요청하면 처음부터 다시 받아야 하기 때문에 용챵이 아깝습니다.
따라서, 범위를 지정해서 여기부터 여기까지 달라고 요청을 하는 겁니다. 이게 바로 범위 전송입니다.
클라이언트가 Range 헤더 필드에 범위를 정해서 보내면 서버에서 Content-Range 헤더 필드에 클라이언트가 요청한 범위 / 범위 끝 을 보내줍니다.
일반 정보
일반 정보는 쉽고 단순한 HTTP 헤더로, 정보성 헤더입니다.
From
From은 유저 에이전트의 이메일 정보입니다. 요청에서 사용하는 헤더이며 일반적으로 잘 사용하진 않습니다.
검색 엔진 같은 곳에서 내 사이트를 크롤링 했을 경우에 검색 엔진 담당자에게 내 사이트에 오지 말라는 연락을 할 수 있습니다. 이때 담당자에게 연락을 할 수 있는 방법이 필요한데, 그게 바로 From에 있는 유저 에이전트 이메일 정보입니다.
Referer
Referer은 현재 요청된 페이지의 이전 웹 페이지 주소를 가지고 있습니다. 요청에서 사용하는 헤더이며 정말 많이 사용됩니다.
A 페이지에서 B 페이지로 이동하는 경우 B를 요청할 때 Referer: A 를 포함해서 요청하게 됩니다. Referer 헤더를 사용해서 현재 사이트를 어느 사이트를 통해서 들어오게 되었는지, 유입 경로 분석이 가능해지게 됩니다. 따라서, 데이터 분석 시에 많이 사용됩니다. 여기에 들어오게 된 이전의 계기를 확인할 수 있으니깐요.
크롬 브라우저를 열어서 구글 검색창에 hello를 검색해보겠습니다.
그리고 검색 결과에 있는 Hello 위키 백과 페이지에 들어갑니다.
아마 위키백과 페이지에 들어가는 요청을 하면서 referer 헤더에 이전 사이트인 google.com 를 보내주었을 겁니다. 한 번 확인해볼까요?
referer 헤더에 https://www.google.com/ 이 적힌 걸 볼 수 있습니다.
User-Agent
User-Agent는 유저 에이전트 애플리케이션 정보를 가지고 있습니다. 내 웹 브라우저 정보 또는 클라이언트 애플리케이션 정보를 가지고 있으며 요청시에 사용합니다.
유저 에이전트 정보는 서버 입장에서는 도움이 되는 정보입니다.
만약, 특정 브라우저에서 버그가 막 생긴다고 할 때에 로그 파싱을 통해서 알 수가 있습니다. 로그를 비교해봄으로써 특정 브라우저에서 문제가 생긴다는걸 파악할 수 있는겁니다.
또한, 통계 정보를 뽑아내기 좋기 때문에 사용자들이 어떤 브라우저에서 많이 들어오는지 확인하기 좋습니다.
Server
Server는 요청을 처리하는 ORIGIN 서버의 소프트웨어 정보를 응답시에 보내줍니다.
ORIGIN 서버는 HTTP 요청 메시지를 보낼 때 중간 여러 프록시 서버를 거치는데 그런 서버들말고 요청 메시지를 받아서 응답 메시지를 보내주는 끝단 서버를 말합니다. 실제 애플리케이션에서 표현 데이터를 만들어주는 바로 그 서버를 ORIGIN 서버라고 합니다.
Date
Date는 메시지가 발생한 날짜와 시간입니다.
과거 스펙에서는 요청에서도 Date 헤더를 사용했지만 최신 스펙에서는 응답에서만 사용하도록 바뀌었습니다.
특별한 정보
실제 애플리케이션에 영향을 주는 특별한 정보들에 대한 헤더입니다.
Host
Host는 요청한 호스트 정보를 가지고 있습니다. 요청 시에 사용되며 필수로 사용해야 합니다.
Host 헤더는 하나의 서버가 여러 도메인을 처리해야 할 때, 하나의 IP 주소에 여러 도메인이 적용되어 있을 때 각 도메인을 구분하기 위해서 사용합니다.
예를 들어서, 가상 호스트를 통해서 여러 도메인을 한 번에 처리할 수 있는 서버가 있다고 합시다. 해당 서버는 200.200.200.2 를 IP 주소로 가지고 있습니다. 해당 서버 안에서는 여러 개의 애플리케이션이 다른 도메인으로 구동 될 수 있다고 합니다.
그런데 이런 상황에서 Host 헤더 없이 /hello 사이트를 요청하는 메시지를 서버로 보냅니다. 서버 입장에서는 /hello 가 aaa.com, bbb.com, ccc.com 어느 도메인으로 보낸 요청인지 알 수 없습니다. 도메인을 구분할 방법이 없으니깐요.
왜 Host 헤더가 없으면 구분을 할 수 없는 걸까요?
IP로만 통신하기에 aaa.com, bbb.com, ccc.com 도메인을 구분할 방법이 아예 없습니다. 초창기엔 이걸로 인해서 문제가 다수 나타나기도 했습니다. 후엔 Host 헤더를 무조건 넣어야 하는걸로 스펙이 개정되었습니다.
그렇다면 Host 헤더를 넣어주면 어떤 일이 발생할까요?
Host 헤더에 도메인을 입력하면 서버는 도착한 요청 메시지에서 host 헤더 필드를 보고 aaa.com 도메인으로 들어간다는 걸 확인합니다. 그리곤 aaa.com 으로 가상 호스팅합니다.
따라서, 호스트 정보를 필수로 보내주어야 합니다.
Host와 Port
Location
Location은 페이지 리다이렉션시 사용합니다. 웹 브라우저는 3xx 응답의 결과에 Location 헤더가 있으면, Location 위치로 자동 이동합니다.
3xx 응답 코드 이외에도 201 상태 코드에서도 Location를 사용할 수 있습니다. 물론, 각 응답 코드마다 Location이 가지는 의미는 다릅니다.
•
201 : Location 값은 요청에 의해 생성된 리소스 URI
•
3xx : Location 값은 요청을 자동으로 리디렉션하기 위한 대상 리소스
Allow
Allow는 허용 가능한 HTTP 메서드입니다.
만약, URL 경로가 있는데 GET, HEAD, PUT 메서드만 제공하고 POST 메서드는 제공하지 않는다고 합시다. 그러면 405 오류를 내리면서 응답에 Allow 헤더를 넣고 “GET, HEAD, PUT 메서드만 제공합니다” 라는 정보를 보내주어야 합니다.
클라이언트는 서버가 보낸 Allow: GET, HEAD, PUT 헤더를 보고서 POST 는 지원을 하지 않는다는걸 알 수 있습니다.
Retry-After
Retry-After는 유저 에이전트가 다음 요청을 하기까기 기다려야 하는 시간입니다.
503 상태 코드일 때, 서비스가 언제까지 불능인지 알려줄 때 사용됩니다. 날짜를 표기할 수도 있고, 초 단위를 표기할 수도 있습니다. 7시부터 서비스가 이용 가능하다면 날짜로 표기를 해줄 것이고, 몇 분 뒤부터 서비스 이용이 가능하다면 초 단위로 계산해서 보여줄 겁니다.
하지만 Retry-After 헤더는 사용하기 쉽지 않은 헤더 입니다.
인증
인증 헤더는 인증과 관련된 헤더입니다.
Authorization
Authorization은 클라이언트 인증 정보를 서버에 전달하는 헤더입니다.
참고로 인증과 관련해서는 인증하는 여러 매커니즘이 존재합니다. 그것마다 Authorization 헤더에 들어가는 value 값이 완전히 달라집니다. Authorization 헤더는 어떤 인증 매커니즘을 사용하는가에 상관 없이 헤더를 제공하기 때문에 사용하는 매커니즘에 맞게 관련 값을 넘겨주면 됩니다.
WWW-Authenticate
WWW-Authenticate는 리소스 접근시 필요한 인증 방법을 정의합니다.
만약 클라이언트가 서버에 요청을 했는데, 인증이 잘 안되거나 인증에 문제가 있다면 401 오류 응답과 함께 WWW-Authenticate 헤더를 내려줍니다.
클라이언트가 인증을 하기 위해서는 이런 정보를 참고해서 제대로 된 인증을 만들어서 서버로 넘겨주면 된다는 정보가 적혀있습니다.
쿠키
쿠키는 상당히 많이 사용하고 중요한 부분입니다. 쿠키 사용시에 2개의 헤더를 사용하는데 ⓵ Set-Cookie 와 ⓶ Cookie 입니다.
Set-Cookie는 서버에서 클라이언트로 쿠키를 전달하는 헤더고, Cookie는 클라이언트가 서버에서 받은 쿠키를 저장하고 HTTP 요청 시 서버로 전달하는 헤더입니다.
그렇다면 쿠키가 왜 필요한지부터 알아봅시다.
상황) welcome 페이지 접근(쿠키 미사용)
1.
웹 브라우저는 /welcome 페이지에 접근합니다.
2.
서버에 /welcome 요청이 들어옵니다. 서버는 응답 메시지를 내려줍니다.
상황) 로그인 진행(쿠키 미사용)
1.
웹 브라우저에서 user 정보를 넣어서 로그인을 요청합니다.
2.
서버에서는 새로운 리소스를 생성하고 로그인 성공했다는 응답을 줍니다. 로그인을 했기 때문에 사용자 정보를 넣어서 응답 메시지를 내려줍니다.
우리는 상태로 welcome 페이지에 접근하는 상황으로 돌아가려고 합니다. 우리는 서버에서 응답 메시지로 “안녕하세요. 손님” 대신에 “안녕하세요. 홍길동님” 이라고 내려주길 원합니다.
상황) 로그인 후 welcome 페이지 접근(쿠키 미사용)
하지만, 같은 상황에서 서버는 “안녕하세요. 손님”으로 응답 데이터를 내려줬습니다.
왜 로그인을 했는데도 “안녕하세요. 손님” 이라고 응답 데이터를 내려준걸까요?
서버는 웹 브라우저가 보낸 요청 메시지만을 보고서 로그인한 사용자라는걸 알 수 있는 방법이 없습니다. 아무리 봐도 저 요청 메시지가 홍길동 유저가 보낸 요청이라는 걸 구분할 수 있는 방법이 없어요.
HTTP는 연결이 다 끝나고나면 연결을 끊는 특징이 존재합니다. 기본적으로 Stateless한 프로토콜이기 때문이에요. 클라이언트와 서버가 요청과 응답을 주고 받고나면 연결을 끊습니다. 다시 요청을 하면 서버는 이전 요청을 기억하지 않기 때문에 상태가 유지될 수 없어요. 위의 문제는 HTTP가 Stateless한 특징을 가지기 때문에 발생합니다.
나는 분명 로그인을 했다고 생각하고서 /welcome 페이지에 접근한건데, 서버 입장에서는 누군지 알 수 없으니 “안녕하세요. 손님” 이라고 응답 데이터를 내린겁니다.
이 문제를 해결하기 위해서는 대안이 필요합니다.
모든 요청에 사용자 정보를 포함해서 보낸다.
모든 요청에 유저 정보를 계속 보내는 겁니다. 쿼리 파라미터 형식으로 유저 정보를 계속 보내주면 서버는 어떤 유저가 보낸 요청인지 구분할 수 있게 됩니다.
하지만, 이 대안은 심각한 문제가 존재합니다. 모든 요청과 링크에 사용자 정보를 다 포함해야 해요.
그렇게 되면 보안 문제도 발생하고 개발자가 모든 요청에 사용자 정보를 포함해서 개발해야하기 때문에 개발이 힘들어집니다. 또한, 브라우저를 완전히 종료하고 다시 열었을 때 다시 로그인을 해서 사용자 정보를 다시 받아와야 한다는 문제가 존재합니다.
이런 문제가 있어서 예전부터 문제를 해결하기 위해 쿠키라는 개념이 도입되었습니다.
상황) 로그인 진행(쿠키 사용)
1.
웹 브라우저에서 user 정보를 넣어서 로그인을 요청합니다.
2.
서버에서는 새로운 리소스를 생성하고 로그인 성공했다는 응답을 줍니다. 응답 시에 Set-Cookie 헤더에 사용자 정보를 집어넣어서 응답해줍니다.
3.
응답 메시지를 받은 클라이언트는 웹 브라우저 내부 쿠키 저장소에다가 유저 정보를 저장해둡니다.
상황) 로그인 후 welcome 페이지 접근(쿠키 사용)
1.
웹 브라우저에서 /welcome 페이지에 접근합니다. 그때 자동으로 Cookie 헤더에 쿠키 저장소에 있는 유저 정보가 추가되어서 서버로 보내집니다.
2.
서버는 Cookie 헤더를 열어서 유저를 알아내고 유저에 맞는 응답 메시지를 전송합니다.
웹 브라우저에서 서버로 요청을 보낼 때마다 자동으로 쿠키 저장소를 뒤져서 쿠키를 찾아냅니다. 꺼낸 쿠키는 Cookie 헤더에 넣어서 서버로 전송됩니다. 서버는 헤더에서 쿠키를 꺼내서 유저를 알아낼 수 있습니다. 쿠키를 사용하게 되면 지저분하게 URL에 유저 정보를 넣거나 할 필요가 없기 때문에 깔끔하게 유저 정보를 서버로 보낼 수 있습니다.
쿠키 매커니즘을 통해서 Stateless로 인해서 유저 정보를 유지하지 못하는 문제를 해결했습니다.
그런데, 모든 곳에 쿠키 정보를 보내면 아무래도 보안 및 여러 방면으로 문제가 발생하게 됩니다. 물론 이런 문제를 해결하는 방법이 있습니다.
그 전에 쿠키에 대한 디테일한 설명을 더 하고 가겠습니다.
쿠키를 서버에서 세팅할 때, 이렇게 세팅됩니다.
set-cookie: sessionId=abcde1234; expires=Sat, 26-Dec-2020 00:00:00 GMT; path=/; domain=.google.com; Secure
대충 쿠키 헤더에 들어가는 값들을 보자면, expires 는 만료되는 시간으로 이해하면 될 거 같고, path 는 이런 경로들에 대해서 쿠키를 허용해주겠다고 이해를 해보면 될 거 같네요. Secure 는 쿠키의 보안 정보에 관련된 부분 같네요. 각 영역에 대한 디테일한 설명은 추후에 더 하겠습니다.
이런 쿠키를 주로 사용하는 곳은:
1.
사용자 로그인 세션 관리
예시에는 사용자가 로그인을 했을 때 Set-Cookie 로 사용자 정보를 그대로 보내주었지만 그렇게 보내는 것은 위험한 방식입니다. 그렇게 보내는 것보다는 로그인에 성공했을 때 세션 키 라는 걸 서버에서 생성해서 서버 DB에 저장해놓고 session 값을 클라이언트에게 반환해주는 것이 좋습니다.
클라이언트는 서버에 요청을 할 때마다 Session Id를 보내게 됩니다. 서버는 Session Id를 통해서 유저 정보를 파악할 수 있습니다.
2.
광고 정보 트래킹
쿠키는 광고 정보 트래킹에도 사용됩니다. “해당 웹 브라우저를 사용하는 사람이 주로 이런 광고를 보는 구나!” 라고 트래킹이 가능합니다.
쿠키 정보는 위의 예시에서도 봤지만 쿠키가 있으면 자동으로 서버에 전송이 됩니다.
그렇기 때문에 네트워크 트래픽을 추가로 유발할 수 있습니다. 우리가 보낸 데이터말고도 쿠키에 있는 데이터가 추가로 넘어가기 때문입니다. 따라서, 문제가 발생하지 않도록 최소한의 정보만을 사용해야 합니다. 방금 말한 세션 id 값, 인증 토큰 같은 값만을 들고 있어야 합니다.
만약, 내가 쿠키를 사용하고 싶긴한데, 클라이언트에서만 들고 있었으면 좋겠다라고 생각한다면 web storage(localStorage, sessionStorage)를 참고하시면 됩니다.
보안한 민감한 데이터(주민번호, 신용카드 번호 등등)는 절대 저장해서는 안되며 이 부분을 주의해서 사용해야 합니다.
쿠키의 생명주기
우리는 쿠키를 무제한으로 보관할 수는 없습니다. 따라서 만료일(expires)을 넣어서 만료일을 넘으면 쿠키가 자동으로 삭제되도록 합니다.
만료일말고도 max-age 값을 넣어서 만료 시점을 세팅할 수도 있습니다. max-age 는 0, 음수로 값을 지정하면 쿠키가 삭제되고 max-age 넣은 기간이 지나도 삭제됩니다.
생명 주기가 있는 쿠키는 2가지로 분류됩니다.
1.
세션 쿠키 : 만료 날짜를 생략하면 브라우저 종료시까지만 유지
예) 맥 브라우저를 완전히 껐다가 키면 다시 처음부터 로그인을 해야 하는 경우를 생각해주시면 됩니다.
2.
영속 쿠키 : 만료 날짜를 입력하면 해당 날짜까지 유지
쿠키의 도메인
쿠키에는 도메인을 적을 수 있습니다. 쿠키에 도메인이 왜 필요한 걸까요?
만약, 내가 지정한 쿠키를 아무 사이트에서나 접근이 가능하다고 합시다. 큰일나겠죠? 따라서 도메인을 지정해서 쿠키가 아무 사이트에서나 접근이 가능하지 않도록 하는겁니다.
도메인을 지정하는 방법은 2가지가 있습니다.
1.
명시
명시는 명시한 문서 기준 도메인 + 서브 도메인을 포함해서 전송하는 겁니다.
예를 들어서, example.org 로 지정해서 쿠키를 생성했습니다. 그러면 example.org 는 물론이고, dev.example.org 도 쿠키 접근이 가능합니다.
2.
생략
생략은 현재 문서 기준 도메인만 적용됩니다.
예를 들어서, example.org 에서 쿠키를 생성하고 domain 지정을 생략했습니다. 그러면 example.org 에서만 쿠키 접근이 가능하고, dev.example.org 는 하위 도메인이기 때문에 쿠키 접근이 불가능합니다.
쿠키의 경로
쿠키는 도메인을 사용해서 쿠키를 사용하는 도메인을 필터하고, 경로를 사용해서 추가로 필터링을 합니다.
경로를 넣어주면 해당 경로를 포함한 하위 경로 페이지만 쿠키 접근이 가능합니다. 일반적으로 경로는 path=/ 로 지정합니다. 왜냐하면, 한 도메인 안에서 보통 쿠키를 다 전송하길 원하기 때문에 루트로 경로를 지정하는 겁니다.
예를 들어서, path=/home 으로 지정했다고 합시다.
•
/home : 쿠키 접근 가능
•
/home/level1 : 쿠키 접근 가능
•
/home/level1/level2 : 쿠키 접근 가능
•
/hello : 쿠키 접근 불가능 → 레벨이 안맞기 때문에 쿠키에 접근할 수 없습니다.
쿠키의 보안
쿠키 보안 방법은 총 3가지가 있습니다.
•
Secure
쿠키는 http, https를 구분하지 않고 전송합니다. 하지만 Secure 로 설정하게 되면 https인 경우에만 전송하게 됩니다.
•
HttpOnly
XSS 공격을 방지합니다. 원래 Javascript에서 쿠키 접근이 가능한데 HttpOnly 를 사용하면 Javascript에서 쿠키 접근이 불가능하게 됩니다.
HttpOnly 를 사용할 경우에는 HTTP 전송에만 사용 가능합니다.
•
SameSite
XSRF 공격을 방지합니다. 원래 요청 도메인과 쿠키에 설정된 도메인이 다른 경우에도 쿠키 전송이 가능했지만 SameSite 를 사용하면 요청 도메인과 쿠키에 설정된 도메인이 같은 경우에만 쿠키 전송이 가능합니다.
SameSite는 기능이 적용된지 몇 년 안되었기 때문에 브라우저에서 지원하는지 확인 후 사용하는 것이 좋습니다.