Backend Engineer - 통신 디자인 패턴
Backend Engineering
HTTP, TCP/UDP, IP와 운영체제, 스케쥴링에 대한 이해도가 있으면 매우 좋습니다.
빠르게 구현하기 위해 먼저 Node.js를 기반으로 프로그램을 개발하였습니다.
샘플 프로젝트 링크 Github
푸쉬 (Push)
일반적으로 서버가 클라이언트에게 데이터를 푸쉬(push)하는 방식으로, 주로 WebSocket이나 서버 센트 이벤트(Server-Sent Events, SSE)를 사용하여 구현할 수 있습니다.
여기서는 WebSocket을 사용하여 Node.js로 간단한 푸쉬 서버를 구현했습니다.
폴링 (Short polling)
클라이언트가 주기적으로 서버에 요청을 보내어 새로운 데이터를 확인하는 방식입니다. 이는 서버 푸쉬와는 반대로 클라이언트가 주도적으로 데이터를 요청하는 방식입니다.
메시지 독점 방지하는 방법
- 서버 메시지 큐에 메시지들이 쌓여 있다라고 가정하자.
- 만약 client가 1개가 아니고 2개일 경우, 특정 하나의 client가 메시지들을 매번 독점하는 경우가 생길 수 있다.
예를 들어, 매 5초마다 폴링 할때, A 클라이언트가 B보다 먼저 polling하게 되면, B는 매번 어떠한 데이터도 얻지 못한다.
(1) 라운드 로빈 방식
라운드 로빈 방식은 각 클라이언트가 순차적으로 메시지를 받을 수 있도록 하는 방법입니다. 서버는 각 클라이언트를 식별하고, 클라이언트 리스트를 유지합니다. 메시지가 도착할 때마다 다음 순번의 클라이언트에게 메시지를 전달합니다.
(2) 브로드캐스트 방식
브로드캐스트 방식은 모든 클라이언트에게 동일한 메시지를 전송하는 방법입니다. 모든 클라이언트가 동일한 메시지를 받을 수 있기 때문에 특정 클라이언트가 메시지를 독점하는 문제를 방지할 수 있습니다.
(3) 메시지 큐 분배
메시지 큐를 각 클라이언트별로 분리하는 방식입니다. 각 클라이언트에게 고유한 큐를 할당하여, 각 클라이언트가 자신의 큐에서만 메시지를 읽도록 합니다. 이를 통해 각 클라이언트가 공정하게 메시지를 받을 수 있습니다.
(4) 공정 큐잉 (Fair Queuing)
공정 큐잉은 메시지를 각 클라이언트에게 공정하게 분배하는 알고리즘을 사용합니다. 서버는 메시지를 수신한 클라이언트의 상태를 기록하고, 각 클라이언트가 일정한 비율로 메시지를 받을 수 있도록 조절합니다.
(5) 메시지 레디스 분산 (Redis Pub/Sub)
Redis의 Pub/Sub 기능을 사용하여 메시지를 분산하는 방법입니다. 각 클라이언트는 특정 채널을 구독하고, 서버는 메시지를 해당 채널에 게시합니다. 클라이언트는 자신의 구독 채널에서 메시지를 받아볼 수 있습니다.
(6) WebSocket 사용
WebSocket을 사용하여 서버와 클라이언트 간의 실시간 통신을 구현합니다. 서버는 모든 클라이언트와 지속적인 연결을 유지하고, 새로운 메시지가 도착할 때마다 모든 클라이언트에게 메시지를 전송할 수 있습니다.
(7) 세션 기반 큐
각 클라이언트가 서버에 접속할 때 세션을 생성하고, 세션별로 메시지 큐를 유지하는 방법입니다. 클라이언트가 메시지를 요청할 때 해당 세션의 메시지 큐에서 메시지를 제공합니다. 이를 통해 각 클라이언트가 고유한 메시지 큐를 가질 수 있습니다.
이러한 방법들은 각기 장단점이 있으며, 특정 상황에 맞게 선택할 수 있습니다. 예를 들어, 브로드캐스트 방식은 실시간 알림 시스템에 적합하지만, 개별 메시지 전달이 필요한 경우에는 라운드 로빈이나 공정 큐잉 방식이 더 적합할 수 있습니다. 각 방법의 구현은 시스템의 요구 사항과 설계에 따라 달라질 수 있습니다.
롱 폴링 (Long Polling)
클라이언트와 서버 간의 통신을 효율적으로 관리하기 위해 등장한 기술 중 하나입니다. 이는 기존의 짧은 폴링(short polling)의 단점을 보완하기 위해 개발되었습니다.
롱 폴링의 등장 배경
- 짧은 폴링의 문제점:
- 빈번한 요청: 클라이언트가 정해진 간격으로 주기적으로 서버에 요청을 보내는 방식입니다. 예를 들어, 클라이언트가 매 5초마다 서버에 새 데이터를 요청합니다.
- 비효율성: 데이터가 자주 업데이트되지 않는 경우에도 클라이언트는 계속해서 요청을 보내야 하므로, 네트워크 자원과 서버 리소스가 비효율적으로 사용됩니다.
- 지연 시간: 데이터가 즉시 업데이트되어야 하는 상황에서도 클라이언트는 다음 폴링 주기를 기다려야 하기 때문에 실시간성이 떨어질 수 있습니다.
- 롱 폴링의 해결 방법:
- 지속적인 연결 유지: 클라이언트가 서버에 요청을 보내면, 서버는 새로운 데이터가 생길 때까지 응답을 보류합니다. 데이터가 준비되면 서버가 응답을 보내고, 클라이언트는 즉시 다음 요청을 보냅니다.
- 효율성 향상: 서버에 새로운 데이터가 있을 때만 응답을 보내므로, 불필요한 요청이 줄어들어 네트워크와 서버 리소스를 효율적으로 사용할 수 있습니다.
- 지연 시간 최소화: 데이터가 생기는 즉시 클라이언트에 전달되므로, 짧은 폴링보다 더 실시간에 가깝게 동작합니다.
롱 폴링의 동작 방식
- 클라이언트 요청: 클라이언트는 서버에 데이터를 요청합니다.
- 서버 대기: 서버는 즉시 응답을 보내지 않고, 새로운 데이터가 준비될 때까지 요청을 대기 상태로 유지합니다.
- 데이터 준비: 서버에 새로운 데이터가 생기면 대기 중인 요청에 응답을 보냅니다.
- 클라이언트 처리: 클라이언트는 응답을 받아 데이터를 처리하고, 즉시 다음 요청을 보냅니다.
- 반복: 이 과정이 반복되면서 클라이언트는 실시간에 가깝게 데이터를 받을 수 있습니다.
SSE (Server-Sent Events)
클라이언트와 서버 간의 효율적인 데이터 전송을 위해 등장한 기술입니다. 이는 클라이언트가 서버로부터 지속적으로 데이터를 수신할 수 있도록 합니다.
SSE의 등장 배경
- 폴링의 비효율성:
- 폴링(polling)은 클라이언트가 주기적으로 서버에 요청을 보내 새로운 데이터를 확인하는 방식입니다.
- 이 방식은 빈번한 요청으로 인해 네트워크 트래픽이 증가하고 서버 리소스를 비효율적으로 사용하게 됩니다.
- 롱 폴링의 개선 필요성:
- 롱 폴링(long polling)은 클라이언트가 서버에 요청을 보내고, 서버는 새로운 데이터가 준비될 때까지 응답을 지연시키는 방식입니다.
- 이는 폴링보다 효율적이지만, 클라이언트가 새로운 요청을 보내는 과정을 반복해야 하는 단점이 있습니다.
- 웹소켓(WebSocket)의 복잡성:
- 웹소켓은 양방향 통신을 지원하며, 실시간 애플리케이션에서 많이 사용됩니다.
- 그러나 설정과 사용이 복잡할 수 있으며, 양방향 통신이 불필요한 경우에는 과도한 기술일 수 있습니다.
SSE는 이러한 문제점을 해결하기 위해 등장했습니다. 클라이언트가 서버와 단방향 연결을 맺고, 서버는 이벤트가 발생할 때마다 클라이언트에게 데이터를 푸쉬(push)하는 방식입니다.
SSE의 동작 방식
- 클라이언트 연결:
- 클라이언트는 서버에 SSE 연결을 설정하기 위해 HTTP 요청을 보냅니다.
- 서버는 text/event-stream MIME 타입을 사용하여 응답을 보냅니다.
- 서버 푸쉬:
- 서버는 연결된 클라이언트에게 이벤트가 발생할 때마다 데이터를 전송합니다.
- 전송되는 데이터는 일반적으로 이벤트의 이름과 데이터로 구성됩니다.
- 지속적인 연결:
- SSE 연결은 지속적이며, 서버는 새로운 데이터가 있을 때마다 클라이언트에게 데이터를 전송합니다.
- 연결이 끊어질 경우 클라이언트는 자동으로 재연결을 시도합니다.
SSE의 실제 사용 분야
- 실시간 피드: 뉴스, 소셜 미디어 피드, 주식 시세 등 실시간으로 업데이트되는 정보를 제공하는 데 사용됩니다.
- 실시간 알림: 이메일, 메시지, 시스템 알림 등 클라이언트에게 실시간으로 알림을 전송하는 데 사용됩니다.
- 실시간 데이터 스트리밍: 스포츠 경기 점수, 센서 데이터 등 실시간 데이터를 스트리밍하는 데 사용됩니다.
- 실시간 대시보드: 서버 상태 모니터링, 애플리케이션 성능 모니터링 등 실시간 데이터를 시각화하는 대시보드에 사용됩니다.
PUB/SUB (MQTT)
메시징 시스템에서 메시지를 게시(publish)하는 주체와 구독(subscribe)하는 주체를 분리하는 패턴입니다. 이 패턴은 메시지를 게시하는 발행자와 메시지를 수신하는 구독자 간의 의존성을 제거하여 높은 확장성을 제공합니다.
등장 배경
-
효율적인 데이터 전송의 필요성: 많은 디바이스가 인터넷에 연결되는 IoT(Internet of Things)의 발전과 함께, 수많은 디바이스 간 효율적이고 신뢰성 있는 데이터 전송 방법이 필요해졌습니다. 전통적인 클라이언트-서버 모델은 대규모 분산 시스템에서 확장성이 떨어지며, 효율성이 낮은 경우가 많습니다.
-
비동기 통신의 요구: 다양한 디바이스가 실시간으로 데이터를 송수신해야 하며, 이는 비동기 통신을 통해서만 효율적으로 처리될 수 있습니다. 특히, 센서 데이터와 같이 자주 업데이트되는 정보를 다루는 시스템에서는 비동기 통신이 필수적입니다.
-
네트워크 자원의 효율적 사용: IoT 디바이스는 제한된 네트워크 자원을 사용하는 경우가 많아, 최소한의 대역폭으로 데이터를 전송할 수 있는 프로토콜이 필요합니다.
발행자(Publisher)
특정 주제(topic)에 메시지를 게시합니다. 발행자는 자신이 게시한 메시지가 누가 구독하는지 알 필요가 없습니다.
구독자(Subscriber)
특정 주제를 구독하여, 해당 주제에 게시된 메시지를 수신합니다. 구독자는 자신이 어떤 발행자로부터 메시지를 받을지 알 필요가 없습니다.
브로커(Broker)
발행자와 구독자 간의 중개 역할을 합니다. 발행자로부터 메시지를 받아서 해당 주제를 구독하는 모든 구독자에게 메시지를 전달합니다.
유튜브를 예로 들어보자.
여러분이 유튜브에 비디오 업로드를 완료했다. 만약, 4k 영상을 업로드 했다고 하면, 유튜브는 이 비디오를 압축하고, 각 해상도에 맞춰 인코딩하려고 할 것이다. 만약 그 어느 하나의 서비스라도 오류가 발생한다면, 업로드(처음)부터 다시 시작해야 한다.
하지만, 이를 PUB/SUB(MQTT 또는 AMQP) 으로 변경한다면?
우리는 비디오 업로드만 완료시키면 된다. 이후의 모든 절차는 각각의 서비스들이 메시지 브로커를 구독해서, 어떠한 메시지가 있다면(이벤트로 봐도 무방), 본인의 역할에 맞는 일을 할 것이다.
- Compress Service는 Raw 비디오를 압축하고, 압축이 완료되면 압축된 사실을 알린다. 즉, Compress Service는 PUB이자 SUB인 셈.
- Format Service는 압축된 비디오를 해상도에 맞춰 인코딩 한다. 지원하는 최고 높은 해상도의 비디오가 완료되면, 이를 메시지 브로커에게 알린다.
- Notification Service는 구독하고 있다가, 완료된 사실을 아는 즉시 구독자에게 알림 서비스를 제공한다.
gRPC
gRPC는 Google에서 개발한 고성능, 오픈소스 원격 프로시저 호출(Remote Procedure Call) 프레임워크입니다. gRPC는 클라이언트와 서버 간의 효율적이고 신뢰성 있는 통신을 위해 설계되었습니다.
등장 배경
-
마이크로서비스 아키텍처의 확산: 마이크로서비스 아키텍처가 널리 사용되면서, 서로 독립적으로 배포되고 관리되는 많은 서비스들 간의 효율적이고 신뢰성 있는 통신이 필요해졌습니다. RESTful API는 널리 사용되고 있지만, 더 높은 성능과 효율성을 요구하는 애플리케이션에서는 한계를 보였습니다.
-
고성능, 저지연 통신의 필요성: 특히, 데이터 중심 애플리케이션에서는 저지연 통신이 필수적입니다. gRPC는 HTTP/2를 기반으로 하여, 다중화, 헤더 압축, 양방향 스트리밍 등을 지원하여 고성능 통신을 가능하게 합니다.
-
언어 중립성과 다중 플랫폼 지원: 다양한 언어와 플랫폼에서 작성된 서비스들이 통신할 수 있어야 했습니다. gRPC는 프로토콜 버퍼(Protocol Buffers)를 사용하여 언어 중립적인 인터페이스를 정의하고, 다양한 언어에 대한 클라이언트 및 서버 라이브러리를 제공합니다.
WebRTC
라우저 간에 실시간 통신을 가능하게 하는 오픈 소스 프로젝트입니다. 이를 통해 플러그인 없이 브라우저 간의 음성, 영상, 데이터 스트리밍이 가능합니다. WebRTC는 다음과 같은 주요 기능을 제공합니다:
-
음성 및 영상 스트리밍: WebRTC는 고품질의 실시간 음성 및 영상 스트리밍을 지원합니다. 이를 통해 화상 회의, 음성 통화 등의 애플리케이션을 구현할 수 있습니다.
-
데이터 채널: WebRTC는 브라우저 간에 임의의 데이터를 주고받을 수 있는 데이터 채널을 제공합니다. 이를 통해 파일 전송, 게임 데이터 교환 등 다양한 실시간 데이터 통신이 가능합니다.
-
P2P 연결: WebRTC는 NAT(네트워크 주소 변환) 및 방화벽을 통과하는 P2P(Peer-to-Peer) 연결을 지원합니다. 이를 통해 중간 서버 없이 직접 통신이 가능해 네트워크 지연을 최소화하고 대역폭을 효율적으로 사용할 수 있습니다.
-
보안: WebRTC는 모든 통신을 암호화하여 안전한 통신을 보장합니다.
등장 배경
-
실시간 통신의 필요성: 인터넷의 발전과 함께 실시간 통신에 대한 수요가 급증했습니다. 화상 회의, 음성 통화, 실시간 스트리밍 등 다양한 애플리케이션에서 고품질의 실시간 통신이 필요하게 되었습니다.
-
플러그인 없는 웹 통신: 기존의 실시간 통신 기술은 플래시(Flash) 또는 다양한 브라우저 플러그인을 필요로 했습니다. 이는 보안 문제와 호환성 문제를 일으켰습니다. HTML5와 함께 브라우저 내에서 플러그인 없이 실시간 통신을 가능하게 하는 기술이 필요하게 되었습니다.
-
표준화된 API의 필요성: 다양한 브라우저와 플랫폼 간의 호환성을 보장하기 위해 표준화된 API가 필요했습니다. WebRTC는 이러한 요구에 맞춰 W3C와 IETF의 협력을 통해 표준화되었습니다.
HTTP2
HTTP1.1
HTTP 1.1 HTTP 1.1은 1997년 1월에 표준 승인되었다.
- 데이터는 문자열로 전송한다.
- 연결당 하나의 요청과 응답을 처리한다. 그래서 동시전송 문제와 다수의 리소스를 처리하기에 속도와 성능의 문제를 가지고 있다.
- *HOL-Blocking 발생, *RTT(Round Trip Time)의 증가
- 매 요청시 마다 쿠키 정보를 헤더에 포함시키고, 중복된 헤더 값을 전송한다.
특정 응답 지연(Head Of Line-Blocking): 네트워크에서 같은 큐에 있는 패킷이 첫번째 패킷에 의해 지연될 때 발생하는 성능 저하 현상을 말한다. (패킷의 처리속도 지연, 최악의 경우 드랍까지 발생 가능)
양방향 지연(Round Trip Time) : 네트워크 시작 지점에서 대상 지점으로 이동하고 시작 지점으로 다시 이동하는데 걸리는 시간(ms)을 말한다.
해결 방법은?
- *이미지 스프라이트를 사용하여 리소스를 최대한 절약한다
이미지 스프라이트(Image Sprite): 여러개의 이미지를 하나의 이미지로 합쳐서 관리하는 기법을 말한다. 좌표와 범위로 구분하여 사용한다.(일일이 지정해주어야 해서, 손이 많이 가는 것이 단점)
- Domain Sharding : 리소스를 여러개의 도메인으로 나누어 저장하여 페이지 로드 시간을 빠르게하는 일종의 트릭 혹은 방법이다. 여러개의 도메인으로 나누어진 리소스를 다운받기 때문에 브라우저는 더 많은 리소스를 한번에 받을 수 있다.
- CSS 및 JavaScript를 압축한다.
- Data URI를 이용한다.
HTTP2의 특징
HTTP 2.0은 2015년 5월에 표준 승인되었다.
- 데이터는 바이너리로 인코딩하여 압축해서 전송한다.
- Multiplexed Streams 방식이 도입되어 한번의 연결으로 여러개의 메세지를 동시에 주고 받을 수 있다. 그러므로 HOL-Blocking이 발생하지 않는다.
- Stream Prioritization : 요청 리소스간 우선순위를 설정하여 응답을 빨리 받을 수 있다.
- Header Compression : 헤더 정보를 *HPACK 압축 방식을 이용하여 압축 전송한다.
- Server Push : 클라이언트 요청 없이 서버에서 리소스를 보내줄 수 있다.
HTTP2 한계
각 요청마다 Stream으로 구분해서 병렬적으로 처리하지만, 결국 이에는 TCP 고유의 HOL Blocking이 존재함. 서로 다른 Stream이 전송되고 있을 때, 하나의 Stream에서 유실이 발생되거나 문제가 생기면 결국 다른 Stream도 문제가 해결될 때 까지 지연되는 현상이 발생되기 때문이다.
즉, 이러한 TCP의 태생적인 HOL Blocking을 해결하기 위해 QUIC / HTTP/3이 등장
HTTP3
HTTP3는 2020년에 발표되었고, 2022년 6월에 표준화가 완성되었습니다. 위에서 언급했다시피 HTTP/3는 TCP를 선택하지 않았습니다. 아무리 발전을 꿰하려해도 TCP위에서는 한계가 있기 때문입니다. TCP는 신뢰성은 보장되지만 속도가 느리다는 단점이 있으며, HOLB의 문제가 있었고, 해당 문제를 TCP위에서 해결한다 할지라도 전세계적으로 사용되고 있는 네트워크 기기와의 호환성 문제도 있었습니다. 결국 HTTP/3는 UDP를 선택하게 되었습니다.
우선 UDP는 무엇일까요? 간략하게 설명드리자면, 데이터 전송의 안정성을 보장하지는 않지만 빠르게 전송할 수 있는 프로토콜입니다. 예를 들면, UDP는 ‘던진다~! 오케이 나는 던졌어. 받는건 니 책임~’와 같이 데이터를 전송하기 전과 후에 재차 확인하는 과정이 없습니다. 빠르게 데이터를 전송하지만 신뢰성을 보장하지 않는 프로토콜입니다. 반면, TCP는 ‘야! 받을 준비됐어? 된 거 맞지? 그럼 보낼께!’와 같이 재차 확인하는 과정을 통해 데이터의 안정성을 보장하면서도 속도가 상대적으로 느린 프로토콜입니다.
UDP는 빠르다고 할 수 있지만, 데이터의 안정성은 보장 못합니다. TCP에 비해 데이터가 손실될 확률이 높습니다. 하지만 이를 보완할 방법이 있기 때문에 HTTP/3에선 UDP를 선택했습니다. UDP의 단점은 QUIC이라는 프로토콜이 보완해줍니다. QUIC은 UDP와 TCP의 장점을 쏙 챙긴 그런 프로토콜이라고 할 수 있는데, 이에 대한 설명과 함께 UDP의 단점은 어떻게 보완되었는지도 설명해보겠습니다.
QUIC
- Google에서 개발한 UDP 기반의 전송 프로토콜 (Quick UDP Internet Connections)
- Google에서 TCP의 구조적 문제로 성능 향상이 어렵다고 판단하여 UDP 기반을 선택
- QUIC은 TCP의 3-way handshake과정을 최적화 하는 것에 초점을 두고 개발됨
- QUIC은 TCP의 Stream은 하나의 chain으로 연결되는 것과 다르게 각 Stream당 독립된 Stream chain을 구성하여 TCP HOL Blocking을 해결
빠른 연결 수립 (built-in security)
- HTTP2의 한계점. 결국 TCP 기반이기 대문에, 연결 수립 진행 후, 보안에 대한 연결을 진행함. 때문에 최소 2회 이상의 RTT가 발생하게 된다. QUIC은 1RTT에 해결한다. 만약 이전 정보를 캐싱해둘 수 있다면 0RTT까지도 가능하다.
왼쪽 그림을 보면 기존에는 TCP위에 TLS가 독립적으로 존재합니다. 그러나 오른쪽 그림의 QUIC에서는 TLS를 내장하고 있습니다. 이 말인즉, 기존에는 보안 설정을 하기 위해서는 TCP 연결을 수립하고 난 다음, 보안을 위한 추가적인 작업이 필요했지만, QUIC에서부터는 보안을 위해 추가적인 작업을 따로해 줄 필요 없이 기본적으로 내장하고 있게 되었다는 것입니다
HOLB가 해결된 멀티플렉싱
HOLB문제는 TCP를 기반으로한 프로토콜의 고질적인 문제점이었습니다. 패킷이 손실되면, 손실된 패킷이 재전송되기 전까지는 전체 데이터 전송의 흐름에 병목이 생길 수 밖에 없었습니다. 이런 문제를 해결하기 위해 각각의 패킷들도 stream별로 구분해주어야 합니다.
이런 일이 어떻게 가능할까요? HTTP2에서 멀티플렉싱이 이루어진 원리와 비슷합니다. HTTP2에서 어떻게 멀티플렉싱이 가능했나요? 바로 프레임에 stream id를 부여했기 때문에 가능했었습니다. 도착한 프레임의 순서가 막무가내여도, 해당 프레임에 부여된 stream id를 확인해서 각각의 stream에 맞게 프레임을 재조립 해주었기 때문에 요청을 보낸 순서대로, 응답이 도착할 필요가 없었던 것입니다. 그러니까 HTTP2 멀티플랙싱의 핵심은 stream id입니다.
먼저, QUIC으로 들어오면서 멀티플렉싱의 기능이 전송계층으로 내려갔습니다. 기존의 HTTP2에서는 멀티플렉싱의 기능이 어떤 계층에서 이루어졌었죠? Binary Framing Layer라는 계층이었습니다. 이 계층은 TCP라는 전송 계층위에, 그리고 application 계층 아래에 있었습니다. 전송계층에서 받아온 패킷 안의 프레임을 가지고 위의 계층으로 보내면, 그 위의 binary framing 계층에서 프레임 안의 stream id를 가지고 조립을 하는 방식이었습니다. 그러니까, Binary Framing Layer에서 멀티플렉싱이 이루어지고 있었습니다. 그런데, 그 멀티플렉싱이 QUIC에서는 전송계층으로 내려오면서, 각각의 패킷에 대하여, 아니 더 엄밀히 말하면, 패킷이 가지고 있는 byte stream에 대하여 stream id를 부여합니다. 그 stream id 덕분에 패킷들은 독립적인 패킷이 될 수 있습니다. 이 덕분에 멀티플렉싱이 가능해집니다.
무슨 말인지 모르겠죠? 그림과 함께 살펴보면 더욱 잘 이해될 것입니다.
TCP에서의 패킷 전송 흐름
위쪽의 주황색은 TCP에서의 패킷 전송흐름입니다. 위에서 설명드렸다시피, TCP는 신뢰성을 중요하게 생각하는 프로토콜이기 때문에 각각의 패킷들이 순서에 맞게 도착하도록 해야합니다. 그것을 위해서 byte range가 주어져있습니다. 주황색 그림을 예로 들어 설명해보겠습니다.
- packet1이 도착했습니다. byte range가 0-499이네요. 이 사실을 기억합니다.
- packet3이 도착했습니다. byte range가 750-1599이네요. 어, 중간에 450-749가 비었습니다. 문제가 있는 것 같아요. packet2를 재전송 요청합니다.
-
packet2가 도착할 때까지 이후의 다른 패킷들은 대시 상태에 들어가게 됩니다.
만약 위의 예시에서 packet1과 packet3 담고 있는 데이터의 종류가 stream1과 관련된 데이터였고, packet2가 담고 있는 데이터의 종류가 stream2와 관련된 데이터였다고 해보겠습니다. 그렇다면 사실 packet2가 손실된 것은 packet1과 packet3의 데이터와는 큰 상관이 없습니다. 애궂게 그냥 기다리는 겁니다.
QUIC에서의 패킷 전송 흐름
QUIC에서는 패킷이 도착하면 먼저 stream id를 확인합니다. 그런 다음, 이전 stream id에서 가지고 있던 byte range를 확인합니다. 아래의 내용을 차근차근 읽다보면 원리를 이해하게 될 것입니다.
- packet 1이 도착했다.
- stream id를 확인하니 1번이다. 해당 스트림에 대한 byte range를 기억한다.
- packet 3이 도착했다.
- stream id를 확인해보니 1번 stream이다. 그러면 이전에 stream id의 byte range를 확인한다. 확인해보니 0-449다. 지금 받아온 byte range는 450-999다. byte range 사이의 어떤 gap도 존재하지 않는다. 정상이라고 처리한다.
- packet 4를 받았다.stream id를 확인해보니 2번 stream이다. 그러면 이전에 stream id 2번의 byte range를 확인한다. 어, 그런데 해당 데이터가 존재하지 않는다. 지금 받아온 byte range는 300-599인데, 0-299라는 gap이 존재한다. stream id 2 번에 대한 이전 패킷을 다시 요청해야겠다.
-
stream 2번에 해당하는 이전 패킷을 요청한다. 다시 손실된 패킷을 받아오는 동안, packet 4번의 데이터는 보관된다. 그리고 나머지 상관없는 stream의 패킷에는 지연이 발생하지 않는다. 오로지 Stream 2번과 관련된 패킷에만 지연이 생긴다.
이런 원리를 통해서 http3에서는 TCP 차원에서 발생하던 HOLB의 문제를 해결했습니다. 결국 핵심은 stream id입니다. 이런 고유 번호를 통해서 독립적으로 전송될 수 있도록 하는 멀티플렉싱이라는 기술이 가능해진 것입니다.
Connection ID
HTTP3가 되면서 향상된 기능 중 또 주목할 만한 부분은 connection id라는 기능입니다. 해당 기능은 ip주소나 여타 다른 이유에 의해서 네트워크 인터페이스가 변경되더라도 생성했었던 연결을 유지할 수 있게 해주는 기능입니다. 사실 연결이 생성되었는데, ip 주소가 변경되는 일은 저희 일상 속에서 빈번하게 일어나는 일입니다.
집에서 wifi를 이용해 유튜브를 보면서 집을 나섭니다. 집과 멀어지는 순간부터는 wifi를 잡을 수 없기 때문에, 개인의 셀룰러 데이터를 이용하게 됩니다. 이 순간 ip주소는 변경됩니다. 그러는 동안 유튜브도 잠시 끊기죠. 만약 기존의 TCP기반의 네트워크를 이용하고 있었다면, 무조건 다시 새로운 연결을 맺어주어야 합니다. 하지만, HTTP3를 사용하면 ip주소가 변경된 이런 상황 속에서도 연결을 새로 맺어줄 필요가 없습니다. 바로 connection id 때문입니다.
connection id(CID)는 연결에 대한 랜덤하고 고유한 식별자입니다. 해당 정보는 QUIC의 패킷 헤더에 붙어있습니다.
CID의 주된 기능은 전송계층 이하의 단계(TCP, IP Etc.)에서 변경사항이 생긴다고 할지라도, 전송되는 패킷들이 잘못된 end point로 전달되지 않도록 보장합니다. 덕분에 네트워크 인터페이스가 변경되어도 항상 연결이 유지될 수 있으며, 심지어는 이전에 다운받았던 데이터를 새롭게 다운 받을 필요가 없어집니다. 이런 점은 용량이 큰 데이터나, 비디오 스트리밍 같은 경우에는 상당한 이점이 될 수 있습니다.
UDP의 단점을 보완한 QUIC
QUIC은 UDP의 2가지의 문제점을 해결하였습니다. 바로 신뢰성과 보안의 문제였죠. 패킷이 손실되어도 그것에 대해 책임을 지지 않는다는 점, 그리고 암호화 기능이 없어 중간에 데이터를 탈취하거나 변조하는 공격에 취약하다는 점.
신뢰성
- 기존의 UDP에서는 전송된 패킷이 손실되었을 때, 재전송을 요청하지 못했던 이유는 각 패킷에 대한 식별 정보가 없었기 때문입니다. UDP는 연결이 없는 프로토콜로, 각 패킷이 독립적으로 전송되며, 전송 순서나 패킷 손실에 대한 확인 및 복구 기능이 존재하지 않습니다.
- QUIC이 멀티플랙싱을 어떻게 가능하게 하는지에 대해서 설명하는 부분에서 해당 내용을 살펴보았기 때문입니다. QUIC에서는 각각의 패킷의 byte stream 대하여 stream id를 부여합니다. 그리고 그 stream id와 byte range가 일치하는지를 확인하고, 일치하지 않는다면 패킷을 재전송하기를 요청합니다.
보안
QUIC에서는 전송계층 자체에서 암호화기능을 통합함으로써 이 문제를 해결했고, 더 이상 보안이 옵션이 아닌 기본적으로 내장된 기능이 되었습니다.
Leave a comment