L7 프로토콜 - HTTP

mydailylogs
|2023. 8. 8. 03:39

Application Layer, L7


먼저 Application Layer라고도 불리는 L7은 유저와 네트워크 간의 인터페이스 역할을 수행하는 계층이다. 해당 계층에서는 애플리케이션 및 사용자 간의 통신과 데이터 전송 방식을 다양한 방식으로 정의하게 되는데, 이를 프로토콜이라고 부르게 된다. L7 내에는 매우 다양한 프로토콜이 존재한다. 가장 대표적으로는 웹 애플리케이션의 통신을 가능하게 하는 HTTP와 HTTPS부터, 파일 전송에 활용되는 FTP, 이메일 메세지를 전송하는 SMTP, 이메일 수신에 사용되는 IMAP/POP3, google.com을 해석하게 해주는 DNS, 초기 주소 할당시에 사용되는 DHCP, 터미널 접속 시 사용되는 SSH 등 굵직한 프로토콜들이 L7 프로토콜로 분류된다.  


모든 프로토콜을 전부 다뤄볼 수는 없을 것 같고 오늘은 이 중 HTTP를 집중적으로 살펴보고자 한다.

 

HTTP (HyperText Transfer Protocol)


웹에서 데이터를 전송하는데 사용되는 프로토콜이다. 주로 HTML 문서나 이미지와 같은 웹 리소스의 전송에 사용되지만, 다양한 종류의 데이터를 전송하는데 활용될 수도 있다. 다음은 HTTP의 주요한 특징이다.


  • Stateless: HTTP는 별도의 세션 연결을 사용하지 않는다. 즉, 각 요청은 독립적이며 서버 측에서는 클라이언트와의 이전 정보를 기억하지 않는다. 때문에 서버에서는 사용자의 상태를 유지하기 위해 쿠키나 세션과 같은 테크닉을 사용하게 되었다.


  • Request/Response 모델: HTTP는 클라이언트와 서버 간의 요청과 응답을 기반으로 수행된다. 클라이언트(e.g. 웹 브라우저)는 서버에 특정 리소스를 요청하고 서버는 해당 리소스를 찾아 응답으로 반환한다.


  • Method: HTTP는 다양한 메서드(or "Verb")를 제공한다. 주요 메서드로는 GET, POST, PUT, DELETE가 있다.

  • Versioning: HTTP는 시간이 지남에 따라 여러 버전이 개발되었다. (e.g. HTTP/1.0, HTTP/1.1, HTTP/2, HTTP/3)


  • Header: HTTP 요청과 응답에는 '헤더'가 포함되어 있다. 헤더는 요청 및 응답에 대한 메타데이터를 포함하며, 캐싱, 인코딩, 인증 등의 다양한 목적으로 사용된다.


  • Non-securable: 기본 HTTP는 암호화되지 않은 텍스트로 데이터를 전송하기 때문에 중간에서 데이터를 가로채는 공격에 취약하다(Man in the Middle Attack). 이를 해결하기 위해 HTTPS(SSL/TLS를 사용하는 HTTP)가 도입되었다.


  • Port: 기본적으로 80 포트를 사용한다.

 

각각의 특성들을 조금 더 자세히 이해해보자.


Stateless



Stateless라는 단어의 의미는 다르게 말해서 각각의 요청이 서로 독립적이며, 서버가 클라이언트의 이전 요청에 대한 정보를 저장하지 않는다는 것을 의미한다. 즉, 한 번의 클라이언트 요청과 이에 대한 서버의 응답이 전송되면 서버는 그 연결에 대한 모든 정보를 잊어버린다. 이렇게 설계된 이유는 원래 웹이 정적인 문서를 공유하기 위해 만들어진 시스템이기 때문이다. 클라이언트가 서버에 문서를 요청하면, 서버는 해당 문서를 찾아 응답으로 보내고 그 연결을 종료한다. 이러한 시나리오에서는 굳이 클라이언트의 이전 상태를 기억할 필요가 없다.


그러나 웹이 발전하면서 사용자들은 로그인, 장바구니, 그 외의 다양한 개인화된 페이지 등 상태를 유지해야 하는 다양한 기능들을 웹 환경에서 사용할 수 있게 되었고, 이에 대한 기술적 요구사항을 만족시키기 위해서 쿠키와 세션이라는 기술들이 도입되었다.


쿠키(Cookies)는 클라이언트 측에서 정보를 저장하는 작은 텍스트 파일이다. 쿠키는 먼저 웹사이트에 의해서 클라이언트(웹 브라우저)에 전송된다. 그 후 웹 브라우저는 쿠키를 통해 자신의 정보들을 웹 사이트에 보내는 형태로 웹사이트에게 자신의 상태를 전달한다. 즉 쿠키를 통해 서버는 Stateless한 패킷을 처리하기에 부담이 경감되지만, 클라이언트 쪽의 자원을 소모하여 Stateful한 것처럼 클라이언트 정보를 관리할 수 있게 된다.


다만 쿠키의 경우에는 클라이언트 측에 저장되기 때문에 보안 상 문제가 발생할 수 밖에 없다. 당장 유저만 하더라도 웹 브라우저의 개발자 도구를 통해서도 쿠키의 내용을 살펴보거나 수정할 수 있다. 이와 관련한 주요한 보안 문제로는 XSS(크로스 사이트 스크립팅), CSRF(크로스 사이트 요청 위조)가 있다.


세션(Session)은 서버 측에서 사용자 정보를 저장하는데 사용한다. 앞서 설명한 쿠키에 세션 ID를 첨부하여 전달하는 형태로 특정 사용자의 세션을 추적할 수 있게 된다. 이후 클라이언트의 모든 요청에는 서버로부터 전달받은 세션 ID가 포함된다. 서버는 이 세션 ID를 통해 사용자를 인식하고 상태를 유지할 수 있게 된다.


HTTP는 분명 Stateless한 프로토콜이지만 앞서 설명한 쿠키을 통한 세션 관리를 통해 마치 Stateful한 시나리오도 처리할 수 있게 되었다.


참고. XSS & CSRF


먼저, XSS는 대략 Stored XSS, Reflected XSS, DOM-based XSS 3종류로 나뉜다.


Stored XSS는 공격자가 악의적인 스크립트를 웹 애플리케이션의 DB에 삽입하는 것으로 이뤄진다. 예를 들어 댓글 기능이 있는 웹 사이트에서 사용자가 악성 스크립트를 포함한 글을 작성하게 되는 경우 다른 사용자가 그 댓글을 확인할 때마다 악성 스크립트를 실행하는 형태로 전개될 수 있다.


Reflected XSS는 사용자에게 악의적인 URL을 클릭하도록 유도하는 것으로 이뤄진다. 이때 말하는 URL에는 악의적인 스크립트가 내장되어 있으며 해당 URL을 클릭하면 웹 애플리케이션은 해당 스크립트를 실행시키는 형태로 공격이 전개된다. 주로 피싱 공격에 사용되는 것으로 알려져있다.


DOM-based XSS는 웹 애플리케이션의 클라이언트 측 스크립트가 DOM(Document Object Mode)의 구조나 속성을 변경하는 경우 발생한다. 공격자는 악의적인 입력을 통해 DOM을 수정하여, 수정된 스크립트가 실행되게 하는 것으로 공격을 전개한다.


이에 대한 가장 기본적인 방어 방법은 결국, 서버 측에서 들어오는 사용자 입력에 대해 신뢰를 하지 않는 것이다. 모든 입력에 대해 의심하고 검증하며 출력 시에는 특수 문자를 적절하게 인코딩해야 한다. 또한 콘텐츠 보안 정책(CSP)를 적절하게 설정하는 것으로도 어느 정도의 방어가 가능하다. CSP를 통해 웹 브라우저에게 특정 소스에서만 콘텐츠를 로드하도록 지시한다면, 어느정도는 공격자가 악의적인 스크립트를 삽입하는 것을 막을 수 있다. 


마지막으로, XSS를 방어하는 대표적인 방법으로 알려진 HttpOnly 플래그 설정이 있다. 특히 쿠키를 사용하여 공격을 전개하는 경우 효과적인데, HttpOnly 플래그가 설정된 쿠키의 경우 JavaScipt에서 접근을 막아버린다. 때문에 해당 스크립트가 동적으로 실행될 여지를 막는다는 점에서 XSS 공격을 통한 쿠키 도용을 방지한다.


이어서 CSRF는 사용자가 로그인 상태에서 악의적인 웹 사이트를 방문하게 되면, 그 사이트가 사용자 몰래 다른 사이트에 요청을 보내도록 하는 공격이다. 예를 들어 사용자가 은행 웹 사이트에 로그인한 상태에서 CSRF 공격이 포함된 악의적인 웹 페이지를 방문하면 그 페이지는 사용자의 은행 계좌에서 돈을 전송하는 요청을 은행 웹 페이지에 보낼 수 있다. 이는 SameSite 플래그를 설정하는 것으로 어느정도 방지가 가능하다.


예를 들어 mybank.com이라는 정상적인 은행 사이트가 있고, 악의적인 malicious.com이라는 사이트가 있다고 하자.

  1. 사용자는 은행 웹 사이트에 로그인하여 사용자의 브라우저에 로그인 상태를 나타내는 쿠키를 전송한다.


  2. 이후 사용자가 악의적인 웹사이트 (malicious.com)에 방문한다.


  3. malicious.com에서는 사용자의 브라우저를 이용하여 mybank.com에 악의적인 요청을 보내려고 시도한다.


  4. 이때 사용자의 브라우저가 이전에 설정된 mybank.com의 로그린 쿠키를 함께 전송하려고 시도한다.


  5. 만약 은행 웹 사이트 쿠키에 SameSite 플래그가 설정되어 있지 않다면 쿠키는 요청과 함께 전송된다. 은행의 웹사이트는 마치 악의적인 사이트에게서 발생한 요청이 사용자에게서 온 것처럼 착각하게 되며, 이는 CSRF 공격으로 이어진다.

이때 SameSite 플래그가 쿠키에 설정되어 있다면 mybank.com에서 발생한 쿠키는 오로지 mybank.com에 대한 요청에만 사용될 수 있다. malicious.com에서는 사용자가 은행과의 연결에 사용된 쿠키를 확인하고 싶어하지만, SameSite 플래그가 설정되어 있다면 해당 쿠키는 오로지 mybank.com 도메인과의 연결에서만 첨부되기에 malicious.com과의 요청에서는 해당 쿠키가 활용되지 않는다.

 

 Request/Response 모델



간단하게 설명하면, 클라이언트가 원하는 정보나 서비스를 요청하면 서버는 그에 따른 적절한 응답을 반환하는 방식을 의미한다.

Request (요청)



클라이언트(웹 브라우저)가 웹 서버에 특정 정보나 서비스를 요청하는 것을 의미한다.


요청 메서드: HTTP에는 여러 요청 메서드가 정의되어 있으며, 대표적인 메서드로는 GET, POST, PUT, DELETE 등이 있다.


  • GET

    • 정보를 조회하기 위한 요청이다.

    • 웹 페이지의 내용이나 이미지 등의 리소스를 요청하는 경우 주로 사용된다.

    • 데이터를 변경하는데 사용되어서는 안된다.

    • e.g. 웹 브라우저 주소창에 URL을 입력하고 페이지를 로드하는 경우, 검색 엔진이 웹 페이지를 크롤링하는 경우


  • POST

    • 정보를 서버에 전송하기 위한 요청이다.

    • 새로운 리소스를 생성하거나 기존 리소스에 데이터를 추가할 때 사용된다.

    • 다만, 서버의 데이터나 상태가 변경되는 상황에 주의해야 한다.

    • e.g. 회원가입, 로그인, 댓글 작성, 파일 업로드 등의 동작


  • PUT

    • 특정 리소스를 수정하거나 대체할 때 사용된다.

    • PUT 요청을 사용하여 리소스를 업데이트할 경우, 해당 리소스는 기존 데이터에서 완전 새로운 데이터로 대체된다.

    • 때문에 부분적인 업데이트를 원하는 경우, PUT은 권장되지 않는다.
      (e.g. 사용자 정보 중 '주소'만 변경하고 싶은 경우, '이름'과 '전화번호' 등의 기존 정보도 사라질 수 있다.)

    • e.g. 사용자 프로필 수정, 게시물 내용 수정


  • PATCH

    • 특정 리소스의 일부만 수정하는 경우 사용된다.

    • PUT 요청과 달리 리소스의 전체 내용을 대체하는 것이 아니라, 특정 부분만 변경한다.

    • e.g. 사용자 정보 중 '주소'만 변경하고 싶은 경우


  • HEAD

    • 헤더만을 가져오고 싶은 경우 사용된다.

    • 리소스를 가져오는 GET 요청과 유사하지만, 본문(Body)를 제거한다는 특징이 있다.

    • e.g. 리소스의 변경 변경 여부 확인, 리소스의 크기 확인, 지원하는 HTTP 메서드 확인하고 싶은 경우


  • OPTIONS

    • 특정 URL에 대해 어떤 HTTP 메서드가 허용되는지를 알아보기 위해 사용된다.

    • e.g. CORS(Cross-Origin Resource Sharing) 설정 검사 등에서 사용된다.

 

Response(응답)

서버가 클라이언트의 요청에 대한 결과를 반환하는 것을 의미한다. 요청의 처리 결과에 대한 상태코드, 응답 헤더, 응답 본문으로 구성된다.


상태 코드: 서버가 클라이언트의 요청을 어떻게 처리했는지 나타내는 코드

  • e.g. 200 OK는 성공적인 응답, 404 Not Found는 요청한 리소스를 찾을 수 없는 경우를 나타낸다.


헤더: 응답에 대한 메타데이터를 담고 있다. 콘텐츠의 유형(MIME), Set 쿠키, 서버 정보등의 내용을 포함할 수 있다.


본문: 요청한 리소스의 내용. HTML 페이지, 이미지, 동영상 등의 데이터를 포함할 수 있다.


이때 헤더 정보는 요청 메세지와 응답 메세지 모두에 들어가는 내용으로  해당 메세지의 메타데이터를 담는다.



HTTP 헤더


HTTP 헤더를 5가지 종류로 분류할 수 있다. 각 종류와 해당 헤더에는 어떤 종류들이 들어갈 수 있는지 알아보자


  1. General Headers: 요청과 응답 양쪽에서 모두 사용될 수 있는 헤더이다.

    • Cache-Control: 캐싱 동작을 제어한다.

    • Date: 메시지가 생성된 날짜와 시간을 나타낸다.

    • Connection: 네트워크 연결에 대한 제어를 위한 지시를 의미한다.


  2. Request Headers: 클라이언트에서 서버로의 요청에 사용되는 헤더이다.

    • Accept: 클라이언트가 이해할 수 있는 컨텐츠의 유형을 명시한다.

    • Host: 요청 대상의 도메인 이름을 명시한다.

    • User-Agent: 요청을 생성하는 클라이언트의 정보를 나타낸다. (e.g. 브라우저, 버전 등)


  3. Response Headers: 서버에서 클라이언트로의 응답에서만 사용된다.

    • Server: 응답을 생성하는 웹 서버의 정보이다.

    • Location: 3xx 응답의 경우, 클라이언트가 이동해야할 URL이다.

    • WWW-authenticate: 401 Unauthorized 응답에 사용되며, 클라이언트에게 어떤 인증 방식을 사용해야 하는지 알린다.


  4. Entity Headers: 요청이나 응답의 본문에 대한 정보를 포함한다.

    • Content-Type: 본문의 미디어 유형 (e.g. 'text/html', 'application/json' 등)

    • Content-Length: 본문의 길이 (Byte 단위)

    • Content-Encoding:  본문이 어떻게 인코딩되는지 (e.g. gzip)


  5. Custom Headers: 표준화된 헤더 외에 개발자는 필요에 따라 사용자 정의 헤더를 추가할 수 있다.

    • 일반적으로 'X-' 접두사를 사용한다. (e.g. X-Requested-With)

    • 현대의 애플리케이션의 경우 굳이 접두사를 붙이지 않고 사용자 정의를 생성하지 않기도 한다.

       

HTTP 요청을 보내는 경우 필요에 따라 여러 헤더들을 모두 포함하여 보낼 수 있다.


예를 들어 POST 요청을 통해 JSON 데이터를 서버에 보낼 때 User-Agen와 Host와 같은 요청 헤더와 함께 'Content-Type: application/json', 'Content-Length'와 같은 엔티티 헤더를 동시에 포함하여 보낼 수 있다.

 

HTTP의 역사 (버전 별 변화 내용)

 

HTTP의 경우 1996년의 초기 버전 HTTP/1.0에서부터 2019년 이후 초안이 등장한 HTTP/3에 이르기까지 꾸준히 새로운 버전을 업그레이드하며 기존의 한계와 문제점을 개선하고 있다. 


해당 챕터에서는 각각의 HTTP 버전을 이해하고, 서로 비교하는 데에 초점을 둔다.


먼저 기본적으로 HTTP/1.0 방식의 커뮤니케이션에서는 각 요청에 대해 새로운 TCP 연결을 생성되며 요청이 완료되는 경우 생성된 TCP 연결이 바로 닫히는 형태로 구현되었다. 이는 Stateless라는 HTTP의 설계 원칙에는 부합하였으나, 만약 여러 요청이 있을 때 매번 TCP의 Handshake 과정을 수반하기에 연결을 설정하고 해제하는 과정에서 오버헤드를 야기했다. 뿐만 아니라 HTTP/1.0을 설계할 당시에는 효율적이였던 솔루션들보다 더욱 효율적으로 연결을 수행할 수 있는 방법들이 생성되었다. 1997년에 정의된 HTTP/1.1은 이러한 내용에 대한 개선을 담고 있다.


HTTP/1.1이 HTTP/1.0에 비해 어떤 장점을 갖는지, 하나하나 따져보자


HTTP/1.0 vs HTTP/1.1


지속적인 연결 (Persistent Connection)

  • 여러 요청을 하나의 TCP 연결에서 처리할 수 있게 되었다.

  • General Header에 'Connection: Keep-alive' 헤더를 사용하여 연결을 열린 상태로 유지할 수 있게 되었다.

 

파이프라이닝 (Pipelining)

  • 지속적인 연결을 통해 클라이언트가 이전 요청의 응답을 기다리지 않고도, 연속적으로 여러 요청을 보낼 수 있게 되었다.

  • 그러나 응답은 순서대로 받아야한다는 점에는 변화가 없다.

 

캐싱 메커니즘 개선


ETag, If-Modified-Since, If-Unmodified-Since, If-None-Match 등의 헤더를 도입하여 캐싱 기능이 크게 향상되었다. 이러한 헤더들은 웹 브라우저나 중간의 캐시 저장소(e.g. proxy caches)에 최신 버전의 웹 컨텐츠가 있는지, 굳이 새로운 버전을 가져올 필요가 있는지 등을 판단하는데 도움이 된다. 이를 통해 네트워크 대역폭을 절약하고, 불필요한 데이터 전송을 줄여 웹의 전체적인 퍼포먼스를 개선할 수 있었다.


아래는 앞서 소개한 HTTP/1.1의 추가된 헤더의 목록들이다.


  • Etag(Entitiy Tag): 리소스의 버전을 식별하는 고유한 문자열
    • 서버는 리소스가 변경된다면 해당 값을 업데이트해야 한다.
    • 클라이언트가 마지막으로 받은 버전과 현재 서버의 버전 간에 차이가 있는지 비교하는데 활용된다.
    • ETag를 활용하면 리소스의 내용이 실제로 변경이 이뤄졌는지 여부를 확인할 수 있으므로, 불필요한 데이터 전송을 피할 수  있다.


  • If-Modified-Since: 마지막으로 리소스를 받은 이후의 날짜/시간을 지정한다.
    • 서버는 이 날짜 이후에 리소스가 수정되었는지를 확인한다.
    • 만약 수정이 없었다면, 304 Not Modified 응답을 반환하여, Body는 보내지 않는다.


  • If-Unmodified-Since: 직전과 반대로 특정 날짜 이후 리소스가 수정되지 않은 경우만 요청을 수락하도록 서버에 지시한다.
    • 리소스가 해당 날짜 이후에 수정되었다면 서버는 412 Precondition Failed 응답을 반환한다.


  • If-None-Match: 요청 헤더에서 ETag 값과 함께 사용되며, 클라이언트가 가진 리소스의 ETag 값과 서버의 리소스 Etag 값이 일치하지 않을때만 리소스를 전송하도록 서버에 요청한다.
    • ETag 값 간에 불일치가 발생한다면, 서버는 304 Not Modified 응답을 반환하여 불필요한 데이터 전송을 피한다.

 

호스트 헤더(Host Header)의 등장


HTTP/1.1에는 호스트 헤더('Host')가 추가 되었다. 호스트 헤더는 각 HTTP 요청이 어떤 웹사이트를 타겟으로 하는지를 명시한다.


예를 들어 A라는 사람이 동시에 운영하는 example.com과 another-example.com이라는 사이트가 있다고 하자.  HTTP/1.1에는 호스트 헤더가 추가되어, 헤더에 'Host: example.com' 혹은 'Host: another-site.com'이라는 정보를 담는다. 이를 통해 해당 요청이 어떤 사이트를 대상으로 수행되는지를 알 수 있다.


그럼 왜 호스트 헤더라는 개념이 추가되어야 했을까? 바로 가상 호스팅이라는 개념을 도입할 수 있기 때문이다.


가상 호스팅은 하나의 IP 주소에 여러 도메인을 연결하는 기술이다. HTTP/1.0에서는 하나의 DNS 주소에는 하나의 IP 주소가 매핑되어야 했다. 때문에 A가 2개의 사이트를 운영하기 위해서는 2개의 IP가 필요했다. 


HTTP/1.1에서는 굳이 2개의 IP 주소 없이도 2개의 DNS 주소를 사용할 수 있다. 요청 헤더에 목표로 하는 DNS 호스트 이름을 명시하게 되면 굳이 IP 주소를 통해 목표를 식별할 필요없이 호스트 헤더를 통해 식별이 가능해진다.


앞의 예시에서 A는 123.123.123.13이라는 가상의 IP 주소를 사용한다고 가정하자. 해당 주소는 example.com과 another-site.com라는 두 사이트 모두를 호스팅한다. 대신 사용자가 요청을 보낼때, 123.123.123.13이라는 주소에 'host:example.com'이라는 호스트 헤더를 포함한 요청을 보낸다. 서버는 호스트 헤더를 활용하여 해당 요청이 어떤 사이트를 대상으로 하는지를 알 수 있다. 이를 가상 호스팅이라고 부른다.


가상 호스팅은 결국 IP 주소의 경감으로 이어진다. 또한 굳이 각 웹사이트마다 별도의 서버나 IP 주소를 사용할 필요가 없기에 호스팅 비용을 절감할 수 있다.


1997년 첫 HTTP/1.1 버전이 정의된 이래로 HTTP/1.1은 위의 개선 사항들을 포함하는 변화들을 이어왔다. 그러다 2015년 경 중요한 변경점들과 함께 HTTP/1.1 대신 HTTP/2.0이라는 새로운 이름을 갖게 되었다. 



HTTP/1.1 vs HTTP/2.0


바이너리 기반 데이터 교환


먼저, HTTP/1.1까지의 데이터 교환은 텍스트 기반이였다. 개발자는 굳이 별도의 도구 없이도 디버깅 도구를 통해 메세지를 읽을 수 있는 것이 당연하다고 여겨왔다. HTTP/2.0의 메세지는 바이너리 기반이다. 인코딩이라는 추가적인 작업을 요구하지만, 바이너리를 통한 데이터 교환은 더욱 빠르고, 굳이 텍스트 형식을 유지하기 위한 내용을이 필요없기에 효율적으로 동작했다.



한 번의 연결에서 동시에 여러 요청외에도 여러 응답까지 교환할 수 있게 되었다. HTTP/1.1의 파이프라이닝의 경우 클라이언트는 여러 요청을 연속적으로 보낼 수 있게 되었다. 그러나 파이프라이닝에서는 여러 요청을 연속적으로 보내도, 서버는 순차적인 응답을 보장해야 한다. 즉, 첫 번째 요청의 응답이 완료되기 전에는 두 번째 요청의 응답을 시작할 수 없다. 이런 구조는 첫 번째 요청이 특히 오래 걸릴 경우 나머지 요청도 블락되는 문제인 HOL(Head-of-Line) 블로킹 문제를 야기한다.


멀티플랙싱


HTTP/2.0의 멀티플랙싱(Multiplexing)은 HOL 문제를 해결한다. HTTP/2.0에서는 단일 연결 위에 여러 개의 동시 스트림을 구축하는데, 이 각각의 스트림이 독립적인 요청과 응답을 처리할 수 있다. 각 스트림은 독립적으로 요청과 응답을 처리하며, 이로 인해 여러 요청의 응답은 독립적으로 도착하게 된다. 그 결과 특정 요청에 대한 응답이 지연되더라도 다른 요청에 대한 응답은 계속해서 도착할 수 있게 되어, HOL 블로킹 문제가 해결되었다.


스트림에 대해서 궁금증이 생겨서 더 찾아보니, 스트림을 관리하는 주체는 클라이언트와 서버 양쪽이라는 정보를 얻을 수 있었다. 클라언트와 서버는 1) 스트림 생성, 2) 스트림의 우선 순위 조정, 3) 플로우 제어(데이터 전송 속도 제어), 4) 스트림 종료, 5) 에러 처리 의 순서대로 각 주체는 스트림을 관리한다. 개인적으로 플로우 제어가 상당히 중요한 개념이라고 느꼈는데, "특정 스트림이 너무 많은 데이터를 전송하여 다른 스트림의 전송을 방해하는 것을 막기 위해 클라이언트나 서버는 각 스트림에 대한 데이터 전송 속도를 제어하는 기술" 정도로 이해할 수 있었다.

헤더의 압축


HTTP/2.0은 헤더를 HPACK 알고리즘을 통해 압축하여 헤더의 크기를 줄인다. 줄여진 헤더의 크기는 형태로 네트워크 오버헤드를 감소를 의미한다. 내용을 담고 싶었으나 설명이 다소 복잡하여 추후에 다시 이해해보는 걸로 넘어가고자 한다. 일단은 HPACK이라는 존재를 아는 것만으로도 충분해보인다. 대신 RFC 공식 문서를 첨부한다.

https://httpwg.org/specs/rfc7541.html

 

RFC7541

HPACK: Header Compression for HTTP/2

httpwg.org

 

마지막으로 HTTP/2.0과 HTTP/3.0의 차이를 비교하며 글을 마무리하려 한다.

 

HTTP/2.0 vs HTTP/3.0



HTTP/3.0의 가장 주요한 특징은  QUIC(Quick UDP Internet Connections)라는 새로운 전송 프로토콜을 사용한다는 점이다. HTTP/2.0까지의 전송은 당연하게도 TCP를 기반으로 한다. 신뢰성 있는 데이터 전송이라는 목적을 달성하기 위해 TCP의 사용은 필수적이였고, TCP의 3-way 핸드쉐이크 과정에서 발생하는 오버헤드는 당연했다.


이름에서 알 수 있듯, QUIC은 UDP를 기반으로 한다. UDP의 주요한 특징은 연결 지향적이지 않다는 것이다. 이는 곧 TCP에 비해서 별도의 연결 과정이 없기 때문에 속도가 빠르다는 장점으로 이어진다. 다만, 단점은 확실하다. 중간에 패킷이 손실되는 경우 UDP는 이를 재전송하는 매캐니즘이 없다. 또한 네트워크 지연 등으로 특정 패킷 간에 순서가 뒤바뀐다고 해도, UDP 통신으로는 해당 패킷들의 순서를 알 수 없다. 이는 모두 TCP의 특징이자 장점으로 작용한다.


QUIC은 UDP를 기반으로 하지만 연결지향적인 특징을 갖는다. 그럼에도 속도 측면에서 TCP보다 훨씬 빠른 속도를 보장한다. 이는 최초 연결이 이뤄진 이후의 연결에서 QUIC이 연결 정보를 캐시하여, 한번 연결이 된 클라이언트와 서버에 대해서는 연결 과정이 생략되기에 가능하다. 다만, 최초 연결 과정에서는 QUIC과 TCP의 핸드 쉐이킹은 상당히 유사하다. QUIC은 UDP 위에서 구현된다는 특징을 가지지만, 사실 UDP 위에 교묘하게 TCP의 기능을 섞은 매커니즘에 가깝다. QUIC은 UDP를 활용한 전송 매커니즘이다. 때문에 빠르다는 특성을 갖는다. 여기서 그럼 어떻게 전송하는 데이터의 신뢰성을 보장할 수 있는지가 핵심이다. 


QUIC은 데이터 전송의 신뢰성과 순서를 유지하기 위해 TCP에서 활용되는 몇몇 개념을 차용하지만 그 구현 방식이 조금 다르다. 우선 TCP는 연속적인 바이트 스트림으로 데이터를 보낸다면, QUIC은 메세지를 기반으로 데이터를 전송한다. 이는 각각의 메세지가 독립적으로 전송되고 심지어 다른 순서로 도착해도 문제가 없다는 것을 의미한다. 즉, 경계가 모호한 바이트 기반의 데이터 대신, 명확한 경계의 메세지 기반 데이터를 통해 다른 메세지들과의 독립성이 보장된다. 이때, QUIC은 HTTP/2.0의 멀티플랙싱과 마찬가지로 여러 스트림을 활용하는데, 경계가 모호한 메세지이기에 다양한 메세지가 독립적인 구조로 동작하여 다른 스트림에서 처리한 메세지의 의도치 않은 손실이나 다른 스트림의 병목에 대해서 영향을 받지 않는다.


그러나 QUIC의 최초 연결과 TCP 핸드쉐이킹은 암호화 연결을 어떻게 수행하는지에서 큰 차이를 갖는다.. TCP의 핸드쉐이크는 두 시스템 간의 연결을 초기화하고 서로가 데이터 전송을 위해 준비하는 과정을 의미한다. TCP는 별도의 암호화 과정이 없기에 TLS 프로토콜을 추가적으로 활용하는데, TLS를 통한 암호화 연결을 위해서는 추가적인 TLS 핸드쉐이킹이 필요하다. QUIC 또한 TLS 1.3을 활용한다. 다만 QUIC에서는 별도의 핸드쉐이크 과정을 필요로 하는 대신 통합된 형태로 TLS 1.3을 사용한다. 클라이언트는 암호화 정보와 함께 초기 연결 요청을 보내고 서버는 해당 정보를 기반으로 응답한다. 즉, TLS를 사용하되 이를 위한 핸드쉐이킹 과정을 생략함으로서 연결을 효율적으로 처리한다.
 


QUICK의 최초 연결 과정에 대한 순서이다.

 

  1. Client Initial(0.5 RTT): 최초 연결 시 클라이언트는 서버에 연결 설정을 위한 초기 메세지를 보낸다. 이 메세지에는 연결 ID, 버전 정보, 암호화 정보 등이 담겨있다.

  2. Server Response(0.5 RTT): 클라이언트의 메세지에 대해 서버는 이를 받아들여 자신의 연결 ID와 함께 클라이언트에 응답한다.

  3. Client Response: 클라이언트는 서버의 키를 사용하여 암호화된 메시지를 전송하고, 이를 통해 양쪽 모두 상대방의 키를 올바르게 받았음을 확인한다.


그림에서 살펴보듯, TCP에 비해 그 절차가 훨씬 간소화되었으며, TCP + TLS가 붙는 경우 2-RTT까지 증가하는 초기 연결 비용을 1-RTT로 끌어내린다.

 

https://commons.wikimedia.org/wiki/File:Tcp-vs-quic-handshake.svg

 


또한 QUIC은 한번 연결이 설정되는 경우, 이를 캐싱하여 이후 두 호스트 간에 연결이 발생하는 경우 굳이 1-RTT의 연결을 사용하지 않고도 0-RTT를 통해서 연결이 이뤄진다. 0-RTT의 핵심 아이디어는 클라이언트가 서버와의 연결에서 얻은 정보(세션 티켓과 키)를 재사용하여 다음 연결 시에 왕복 없이 바로 데이터 전송을 시작한다는 것이다.


예를 들어 웹 브라우징 시에 사용자가 웹 페이지를 방문한 이후 다시 해당 웹페이지를 방문하려고 할때 0-RTT를 활용하면 연결 설정 시간이 크게 줄어든다.


그러나 0-RTT에는 2가지 문제가 존재한다.


먼저, 네트워크가 지연되거나 기타 이유로 0-RTT 요청 패킷이 소실되는 경우 해당 세션의 초기 설정 상태에 문제가 발생한다. 초기 0-RTT 요청 패킷이 도달하지 않으면 서버는 클라이언트가 캐싱한 세션 정보를 사용하여 연결을 시도하고 있음을 인지하지 못하며, 이러한 상황 속에서 클라이언트가 추가적인 데이터 패킷을 보내더라도 서버는 해당 패킷들을 적절한 연결 컨텍스트 내에서 해석할 수 없게 된다. 때문에 0-RTT의 요청 패킷이 문제가 된다면 이후의 데이터들도 정상적으로 전달되지 못한다. 물론 요청이 제대로 도달하지 않았을 경우를 추적하는 여러 매커니즘이 있다. 먼저 서버 측 관점에서는 Packet Numbering 방식을 통해 각 패킷에 고유한 번호를 할당하고 패킷의 순서를 보장할 수 있다. 만약 초기 연결 설정에 대한 패킷이 누락된다면, 서버는 해당 패킷이 손실되었다는 것을 인지하고 클라이언트에게 재전송을 요청할 것이다. 만약 이를 서버 측에서 처리해내지 못한다고 해도 클라이언트 측에서는 타임아웃을 활용하여 일정 시간이 지나도 돌아오지 않는 응답에 대해서, 다시 요청을 보낸다.


두 번째 잘 알려진 문제로는 Replay Attack의 위험성이 크다는 것이다. 만약 악의적인 공격자가 네트워크 상에서 0-RTT를 가로챈 후 이를 변조하여 다시 전송한다면 이는 크나큰 보안 위협으로 작용한다. 이러한 이유로 0-RTT 데이터는 일회성의 요청, 즉 "GET" 요청과 같은 것에만 권장된다.


이어서 QUIC은 연결을 마이그레이션하는 경우이라는 큰 장점을 갖는다. 사용자가 네트워크 환경을 전환할 때 (LTE에서 Wifi) QUIC 연결은 기존의 연결을 유지하면서 빠르게 새로운 IP 주소로 마이그레이션할 수 있다. 이로 인해 연결을 재설정한다거나 새로운 핸드쉐이크 없이도 네트워크를 전환할 수 있다.


 

여기까지 HTTP를 살펴보았다. 사실 추가적으로 더 넣고 싶었던 내용도 있었고 편집 과정에서 너무 과하다고 생각되는 내용들도 있었다. 특히 HTTPS를 설명하지 못한게 아쉽다. HTTPS의 경우 사실 TLS와 SSL에 대한 설명이 주가 되기 때문에 네트워크에 대한 글보다는 보안이론에 대한 설명이 주가 될 거 같다. 어쩌다보니 꼬박 하루 동안 글을 쓰고 있는데, 얻어가는게 많아 시간이 아깝지는 않았던 것 같다.

 


'CS' 카테고리의 다른 글

IPv4에서 IPv6로의 전환 매커니즘  (0) 2023.08.06
정적 라이브러리  (0) 2022.12.05
실행가능한 목적파일  (0) 2022.12.03
Linking(링킹) 소개  (0) 2022.12.02
시간 지역성을 위한 캐시 재배치  (0) 2022.12.01