LINE Corporation이 2023년 10월 1일부로 LY Corporation이 되었습니다. LY Corporation의 새로운 기술 블로그를 소개합니다. LY Corporation Tech Blog

Blog


LINT: HTTP/2와 TLS를 통한 네트워크 현대화

LINE DEVELOPER DAY 2021에서 이벽산 님이 발표하신 LINT: HTTP/2와 TLS를 통한 네트워크 현대화 세션 내용을 옮긴 글입니다.

안녕하세요. LINE API 게이트웨이 팀에서 소프트웨어 엔지니어로 일하는 이벽산입니다. 이번 글에서는 HTTP/2와 TLS(Transport Layer Security)를 이용해 LINE 클라이언트와 서버 간의 연결성을 높였던 작업에 대해 공유하려고 합니다. 먼저 작업하게 된 배경을 설명하고 연결성을 향상한 세 가지 접근 방식을 설명하겠습니다.

 

작업 배경

우선 이번 글에서 다룰 범위를 말씀드리겠습니다. 아래 그림은 LINE 메시징 플랫폼의 전체 구조입니다. 

 

LINE 클라이언트는 LEGY(LINE Event delivery GatewaY)라는 API 게이트웨이를 사용해 톡 서버(Talk Server), 협업 서비스(Collaborative Services)와 직간접적으로 소통합니다. 톡 서버와 협업 서비스는 자체 비즈니스 로직을 실행해서 LINE 메시징과 미디어 전송 등의 LINE 기능을 제공합니다. LEGY의 주요 기능 중 하나는 클라이언트와의 연결을 관리하는 것인데요. 이번 글에서는 바로 이 LINE 클라이언트와 LEGY 간의 커뮤니케이션에 중점을 두겠습니다. 

 

LINE 서비스 트래픽의 대부분이 LEGY를 통해서 들어오기 때문에 LEGY는 효율적이고 안정적인 방식으로 작업을 처리할 수 있으면서 안전해야 하는데요. 이런 요구 사항을 만족하기 위해 사용하는 프로토콜을 교체하고 보완해 왔습니다.

 

HTTP/1.1

LINE이 2011년에 처음 서비스를 시작했을 때 HTTP/1.1을 사용했습니다. HTTP/1.1은 한 쌍의 요청과 응답에 대해 하나의 연결을 사용합니다. 

 

LINE의 주요 기능 중 하나는 클라이언트에 새로운 메시지가 도착했다는 것을 HTTP/1.1을 통해 알리는 것입니다. 이를 위해 롱 폴링(long polling) 테크닉을 사용했습니다. 아래는 롱 폴링의 작동 방식입니다. LINE의 모든 이벤트는 오퍼레이션(OP)으로 정의되며, 클라이언트는 오퍼레이션을 반환하는 API인 fetchOps를 사용해서 오퍼레이션을 동기화합니다. 

 

LEGY는 롱 폴링 방식으로 작동합니다. fetchOps 요청을 클라이언트에서 받은 직후 톡 서버로 전달하고, 만약 톡 서버가 아무 응답도 하지 않으면 톡 서버의 발행(publish)이 오기를 기다립니다. 새로운 오퍼레이션이 생성되면 톡 서버는 LEGY에 발행 요청을 보내고, LEGY는 톡 서버에서 발행을 가져온 후 다시 fetchOps의 요청을 톡 서버로 보내고 클라이언트로 응답을 보냅니다. 

 

HTTP/1.1에서 SPDY로

2012년 말부터 LEGY는 SPDY를 사용했으며, 이는 HTTP/2의 기반이 됐습니다. HTTP/1.1과는 다르게 SPDY는 하나의 TCP 연결을 설정하고 여러 요청과 응답을 그 연결에서 다중화할 수 있습니다. 

 

SPDY를 사용하기 시작하면서 여러 부분을 개선했습니다.

첫 번째는 헤더 압축입니다. SPDY는 요청에 큰 헤더를 포함시킬 수 있습니다. HTTP 헤더를 압축해 크기를 줄이기 때문인데요. 이를 통해 네트워크 대역폭을 줄일 수 있었지만, 압축 상태를 저장하기 위해 LEGY 메모리를 많이 사용하게 되면서 일부 연결에 대해서는 헤더 압축을 제대로 지원하지 못하는 상황이 발생했습니다. 

이에 헤더를 캐시해 문제를 개선했습니다. 클라이언트에서 많은 요청이 들어오면 동일한 HTTP 헤더가 있을 가능성이 있는데요. 이때 그 헤더가 무엇인지 알 수 있기 때문에 네트워크 대역폭과 메모리 사용량을 줄이기 위해 반복되는 헤더를 LEGY에 캐싱했습니다. 

 

LEGY로 요청이 들어오면 메모리에 캐시 가능한 헤더를 보관하고, 클라이언트에는 캐시 키를 반환합니다. 이후 요청에 대해서 클라이언트는 캐시 키를 HTTP 헤더로 첨부할 수 있으며, LEGY는 요청을 백엔드 서버로 보내기 전에 캐시한 헤더를 복원합니다. 

두 번째는 서버 푸시입니다. 서버 푸시를 이용하면 단일 요청에 여러 응답을 보내는 것이 가능합니다. 이를 통해 추가 리소스를 확보해서 더 빨리 웹 페이지를 렌더링할 수 있도록 만드는 것이 목표였습니다. 하지만 '서버 푸시'라고 부르기는 해도 서버 푸시 역시 최초에 요청이 하나 필요합니다. 또한 일부 LINE 서비스는 롱 폴링을 이용할 때 'No Content' 응답을 보내는 것을 피하기 위해 서비스 구독(subscribe)과 푸시 패턴이 필요합니다. 이를 위해서는 푸시 요청이 필요 없는 '요청하지 않은 푸시(unsolicited push)'가 필요했습니다. 이와 같은 푸시 방법을 활성화하기 위해서 SPDY 스펙을 따르지 않는 비표준 방법이긴 했지만 SPDY에서 사용하지 않는 스트림 0에 모든 푸시 스트림을 연결했습니다. 

 

클라이언트가 LEGY를 통해서 서비스를 구독하면, 푸시 상황이 발생할 때 서비스가 LEGY에 푸시 요청을 보내고 LEGY는 관련 클라이언트 연결을 찾습니다. 이후 데이터를 스트림 0으로 보냅니다. 이때 fetchOps API는 이 패턴을 따르지 않고 롱 폴링 기법으로 작동합니다. 

세 번째는 보안입니다. 통신에서 보안은 매우 중요합니다. 당시 더 강력한 보안을 제공하기 위해 TLS 사용을 장려했는데요. TLS는 속도가 느렸기 때문에 사용하는 데 부담이 컸습니다. 당시 안 그래도 느렸던 3G 모바일 네트워크의 속도를 더욱 늦춰 연결하는 데 오래 걸리게 만들었고, 그마저도 연결이 끊기고 다시 연결되는 현상이 자주 발생했습니다.

 

이에 LEGY 암호화라는 자체 암호화 방식을 만들었습니다. LEGY 암호화는 전체를 암호화하는 대신 메시지 본문과 민감한 헤더만 암호화합니다. 핸드셰이킹 절차는 TLS 버전 1.3의 0 RTT(Round Trip Time) 핸드셰이킹과 비슷하며, 암호화키 교환과 HTTP 요청 및 응답을 동시에 처리합니다. 또한 LEGY 암호화에 대한 일부 메타 데이터를 헤더에 추가하며, 요청과 응답에 있는 모든 민감한 내용의 헤더는 메시지 본문으로 옮겨 본문과 함께 암호화합니다. Wi-Fi 네트워크에서 연결된 클라이언트는 TLS를 사용하고, 모바일 네트워크에서 연결된 클라이언트는 LEGY 암호화를 사용합니다.

 

SPDY의 문제점

여기까지 LINE 클라이언트와 LEGY의 연결성을 높이기 위해서 사용한 기술들을 소개했는데요. 몇 가지 문제가 있었습니다. 일단 SPDY라는 프로토콜을 LINE 외 다른 곳에서는 더 이상 사용하지 않았습니다. 이에 따라 함께 작동하는 라이브러리가 없어서 자체적으로 SPDY 코드를 작성하고 관리해야 했습니다. 두 번째로 우리가 사용하는 프로토콜이 표준이 아니어서 네트워크를 디버깅하기 어려웠습니다. 마지막으로 자체 제작한 암호화 방식보다 TLS가 더 좋고 안전했지만, 앞서 말씀드린 문제 때문에 모든 연결에서 TLS를 사용할 수는 없었습니다.

 

LINT: LINE Improvement for Next Ten years

우리의 목표는 LINE을 더 신뢰할 수 있는 서비스로, 더욱 안전한 서비스로 만드는 것이었습니다. LINT는 LINE Improvement for Next Ten years의 약자로 LINE의 향후 10년을 위한 개선 프로젝트입니다. 지금까지 LINE이 급성장하면서 간과했던 사각지대를 돌아보고 개선하기 위한 프로젝트라고 할 수 있습니다. 이 LINT 프로젝트의 일부로 앞서 말씀드린 문제를 해결하고자 기존 SPDY 프로토콜을 HTTP/2로 교체하고, 표준을 준수하는 푸시 메커니즘을 도입하고, 네트워크 환경에 관계없이 모든 연결에 안전한 TLS를 사용할 수 있게 만드는 작업을 진행했습니다.

 

그럼 각 사항과 관련해 어떤 작업을 수행했는지 설명하겠습니다.  

 

HTTP/2로 마이그레이션

HTTP/2를 사용하는 것은 간단합니다. SPDY 프로토콜을 HTTP/2로 교체하면 됩니다. HTTP/2에는 효율적인 헤더 압축 방식인 HPACK이 있기 때문에 표준 HTTP/2를 사용하면 헤더 캐시는 더 이상 사용하지 않아도 됩니다. 그럼 어떻게 마이그레이션했는지 살펴볼 텐데요. 이번 글에서는 SPDY와 HTTP/2의 스펙이나 성능을 비교하기보다는 어떻게 안전하게 교체했는지에 집중하겠습니다.

마이그레이션이 실패할 수 있는 상황에는 두 가지가 있습니다. 첫 번째는 서버 구현에 버그가 있는 것입니다. 서버는 클라이언트와 다르게 언제든 고칠 수 있긴 하지만 고치는 데까지 어느 정도의 시간이 걸리느냐가 관건입니다. 만약 발생한 버그가 사소한 것이 아니라 수정하는 데 오래 걸리는 버그라면 수정을 완료할 때까지 HTTP/2를 완전히 비활성화하는 게 가장 좋은 방법일 것입니다. 이를 위해서는 클라이언트가 HTTP/2를 사용하지 않도록 제어할 수 있는 기능이 필요했습니다. 

두 번째는 클라이언트에 버그가 있는 경우입니다. 이런 경우에는 클라이언트가 요청을 보내는 데 실패하거나 응답을 받지 못할 수 있으며, 이와 같은 오류가 일시적으로 혹은 영구적으로 발생할 수 있습니다. 가장 최악의 경우 모든 LINE 서비스 요청이 실패할 수 있는데요. 이런 상황을 처리하기 위해서는 클라이언트가 HTTP/2가 아닌 SPDY도 사용할 수 있어야 했습니다.  

아래는 통신 작동 방식을 나타낸 그림입니다. 

 

네트워크 추상화 계층을 도입해서 프로토콜을 직접 고르는 게 아니라 어떤 프로토콜을 사용할지 어떤 프로토콜로 처리할지를 추상화 계층에서 자율적으로 설정합니다. 기본은 HTTP/2로 설정했습니다. 이 추상화 계층은 HTTP/2 마이그레이션뿐 아니라 추후 프로토콜을 업그레이드할 때도 유용하게 사용할 수 있습니다. 장애가 발생하면 추상화 계층에서 감지하고 아래와 같이 SPDY로 전환합니다. 이에 따라 클라이언트는 기존과 같이 아무 문제 없이 LINE 서비스를 사용할 수 있습니다. 

 

만약 HTTP/2를 비활성화해야 하는 경우 외부 설정을 업데이트합니다. 업데이트한 설정을 클라이언트 네트워크 추상화 계층으로 전파하면, 추상화 계층에서 사용 가능한 프로토콜 목록에서 HTTP/2를 제외합니다. 이를 통해 클라이언트는 SPDY만 사용하게 됩니다. 

 

이후 다시 HTTP/2를 설정하면 네트워크 추상화 계층에서 다시 HTTP/2를 사용하기 시작합니다.

클라이언트 설정을 위해서는 Connection Info(이하 Conn. Info)라는 것을 사용합니다. Conn. Info는 LEGY에 정의하는 네트워크 설정입니다. 클라이언트는 Conn. Info를 보고 연결할 LEGY와 연결 방법을 정하고 일부 기능 파라미터를 정의하는 등 네트워크 행동을 결정합니다. 그런데 사실 Conn. Info가 새로운 기능은 아닙니다. HTTP/2 사용을 고려하기 전부터 오랜 기간 Conn. Info를 정의하고 사용해 왔는데요. 무슨 이유로 Conn. Info를 사용하고 있었을까요? 이는 서로 다른 환경에서 서비스를 사용하는 사용자에게 동일하게 안정적이고 신뢰할 수 있는 서비스를 제공하기 위해서입니다. 

LINE은 글로벌 서비스입니다. 전 세계에 여러 개의 LEGY PoP(Point of Presence)을 운영하고 있으며, 클라이언트는 빠르고 안정적으로 통신하기 위해 적절한 위치에 있는 PoP에 연결해야 합니다. 또한 국가마다 네트워크의 특성이 다르기 때문에 네트워크 정책을 다르게 적용할 수도 있습니다. 클라이언트 유형도 여러 개가 있어서 서로 다른 유형의 클라이언트는 각자 자신이 실행되는 플랫폼에서의 성능을 높이기 위해 네트워크 설정을 서로 다르게 할 수 있으며, 사용자의 애플리케이션 버전에 따라 기능의 차이가 있을 수도 있습니다. 이에 따라 Conn. Info를 이용해 국가나 클라이언트 유형 혹은 버전에 따라서 설정을 다르게 제어하고 있는데요. 예를 들어 신년에 트래픽이 폭발할 때에는 Conn. Info를 통해 클라이언트에서 자동으로 재시도 요청을 보내는 부분을 비활성화하기도 합니다.

아래는 Conn. Info 사용 예시입니다. 

 

가장 위에는 설정 항목이 나옵니다. 이번 예시는 LEGY 서버 설정입니다. 그 아래는 국가로 전체(all)와 일본(JP), 두 가지가 있습니다. 예를 들어 일본 사용자를 위한 설정이 적합하지 않은 경우에는 all을 선택해 그 아래 나오는 설정을 사용합니다. 다음으로 클라이언트 유형과 버전을 지정합니다. 이번 예시는 클라이언트 버전에 따라 다르게 설정했습니다. Android 클라이언트 버전 a.b.c와 iPhone 클라이언트 버전 x.y.z를 사용하는 사용자들은 첫 번째 설정을 사용하고, 나머지는 두 번째를 사용합니다. 다른 클라이언트 유형은 이 구성을 사용할 수 없기 때문에 실제로는 모든 클라이언트 유형과 버전에 대해서 설정해 놓습니다. 모든 클라이언트가 전부 LEGY에 연결돼야 하기 때문입니다. 설정은 모바일과 Wi-Fi, 두 가지로 나뉘어 있습니다. 어떤 설정 항목을 사용할지는 클라이언트의 현재 네트워크 환경에 달려 있습니다. 각 설정에는 LEGY 서버 정보와 우선순위 목록이 있습니다. 클라이언트의 네트워크 추상화 계층은 이를 참조해서 어떤 프로토콜을 선택할지 결정합니다. 보안 필드는 LEGY 암호화를 사용할지 TLS 암호화를 사용할지 결정합니다. iPhone 클라이언트는 특정 버전 이후로는 HTTP/2를 기본 프로토콜로 사용하게 돼 있는데요. 이 설정을 통해 HTTP/2를 비활성화하고 SPDY를 사용할 수 있습니다. 

그런데 클라이언트가 LEGY의 Conn. Info를 사용할 수 없는 경우도 있습니다. 예를 들어 클라이언트 애플리케이션이 사용자 기기에 새로 설치된 경우인데요. 이런 경우를 위해 클라이언트에게 기본 Conn. Info를 제공해야 합니다. 그래야 클라이언트가 LEGY의 Conn. Info 데이터가 없는 경우에도 작동할 수 있습니다. 

 

기본 Conn. Info는 LEGY 저장소에서 추출해 별도의 저장소에 보관합니다. 클라이언트 소스 코드에 이 저장소에서 가져온 정보를 포함시켜서 애플리케이션 바이너리로 설정을 제공합니다. 클라이언트가 실행되면 먼저 로컬 캐시에 Conn. Info 관련 정보가 있는지 확인합니다. 만약 없으면 기본 Conn. Info를 참고해서 연결할 LEGY 서버를 결정한 뒤 그곳에서 최신 Conn. Info를 가져옵니다. 조회에 성공하면 조회한 Conn. Info 데이터를 로컬 저장소에 저장하고 이를 이용해 네트워크 동작을 결정합니다. 만약 실패하면 내장된 기본 Conn. Info를 사용합니다. 

Conn. Info는 안전하게 마이그레이션하기 위한 도구로도 사용했습니다. 아래는 단계별로 마이그레이션했던 과정입니다. 

 

위와 같이 모든 단계에서 HTTP/2를 가장 높은 우선순위로 설정해서 SPDY가 아니라 HTTP/2를 사용하게 하고 Beta/RC 환경에서 먼저 테스트한 후 릴리스했는데요. Beta/RC 단계 때와는 다르게 이후에는 클라이언트에서 HTTP/2를 사용할 수 있는 사용자를 단계별로 제한했습니다. 먼저 LINE 직원들에게 HTTP/2를 허용한 다음에 일본 LINE 사용자, 전 세계 LINE 사용자 순서로 오픈했습니다. 만약 사용자 쪽에서 HTTP/2를 사용하는 것이 금지돼 있는 경우에는 Conn. Info의 HTTP/2 항목이 클라이언트에서 무시됩니다. 

다행히 마이그레이션 과정에서 큰 문제가 없어서 Conn. Info에서 HTTP/2를 완전히 비활성화하도록 업데이트할 필요는 없었습니다. 현재 LEGY 커넥션의 85%가 HTTP/2를 사용하고 있으며, 조만간 클라이언트에 있는 SPDY 코드를 완전히 제거하려고 준비하고 있습니다.

 

스트리밍 푸시

다음으로 스트리밍 푸시입니다. 서버 푸시를 위한 요구 사항을 다시 확인해 보면 HTTP/2 표준을 따르면서 사용하기 쉬워야 합니다. 또한 요청하지 않은 푸시도 사용할 수 있어야 합니다. 

SPDY와 마찬가지로 HTTP/2도 서버 푸시 기능을 제공하지만 형태가 다릅니다. HTTP/2에서는 PUSH_PROMISE 프레임을 정의해서 푸시를 보내기 전에 서버에서 먼저 PUSH_PROMISE를 클라이언트로 전송해서 푸시할 것이 있다는 것을 알려줘야 합니다. 각 푸시는 PUSH PROMISE와는 다른 별도 스트림으로 전송합니다. 아래 이미지를 보시면 두 개의 푸시 데이터가 스트림 B와 C로 전송될 것이라고 알려주는 PUSH_PROMISE가 스트림 A로 전송됐습니다.

 

이 기능을 사용해서 요청하지 않은 푸시를 사용할 수 있는 시퀀스를 고안했습니다. SPDY 푸시와의 유일한 차이점은 LEGY에서 푸시와 관련된 모든 스트림에 PUSH_PROMISE를 전송한다는 것입니다. PUSH_PROMISE는 구독 응답과 함께 전송하며, 각 푸시는 다음 푸시를 위한 PUSH_PROMISE를 보냅니다.

 

그런데 HTTP/2 표준에 따르면 PUSH_PROMISE는 클라이언트 요청 스트림에서 사용해야 합니다. 이 모델이 브라우저나 일부 라이브러리에서 테스트했을 때 잘 작동하기는 했지만, 최초 요구 사항이었던 '표준을 따른다'는 준수하지 못한다는 문제가 있는 것입니다.

또한 고려해야 할 것이 하나 더 있었습니다. LINE 클라이언트가 이전 푸시 모델을 사용하고 있는 상황에서 라이브러리나 운영체제에서 연결을 관리한다고 가정해 봅시다.

 

푸시를 받기 위해서 LINE 클라이언트가 먼저 구독 요청을 보내고 성공합니다. 이제 클라이언트는 필요할 때마다 LEGY가 푸시 데이터를 보낼 것이라고 믿습니다.

 

그런데 클라이언트에서는 연결을 제어할 수 없기 때문에 라이브러리나 운영체제로 구독 요청을 전송한 후 연결이 닫혀버릴 수 있습니다. LEGY 쪽에서는 클라이언트와의 연결이 끊긴 후 구독이 무효화됐기 때문에 클라이언트에 푸시를 보내지 못합니다. 

 

하지만 클라이언트는 이 상황에 대해서 알지 못해 여전히 구독이 유효하다고 알고 있습니다.

 

이런 문제가 SPDY 프로토콜에서는 발생하지 않습니다. SPDY 프로토콜에서는 LINE 클라이언트에서 연결을 완전히 제어하기 때문입니다. 그런데 HTTP/2 프로토콜에서는 LINE 클라이언트에서 연결을 완전히 제어할 수 없는 경우에 문제가 발생합니다.

그런데 이때 연결이 아니라 HTTP/2 스트림을 사용한다면 클라이언트가 사용하는 라이브러리와 상관없이 클라이언트가 요청을 시작, 중지, 완료할 시기를 결정하며 완전히 제어할 수 있습니다. 따라서 푸시 세션은 연결이 아니라 스트림에서 생성해야 합니다. 이때 LEGY가 푸시하려면 세션 자체가 종료되지 않는 한 스트림은 종료되지 않아야 합니다. 

이는 HTTP/1.1의 청크(chunk) 전송과 완전히 같은 것입니다. 청크 전송을 이용하면 데이터 스트리밍이 가능합니다. LEGY는 구독 요청에 청크 전송으로 응답할 수 있고 준비가 끝나면 데이터를 전송할 수 있습니다. 

 

위와 같은 일을 HTTP/2에서는 큰 문제 없이 하나의 연결로 할 수 있는데요. HTTP/1.1과 다르게 HTTP/2에서는 연결을 여러 스트림으로 다중화하기 때문입니다.

 

HTTP/2에서는 메시지 본문이 여러 데이터 프레임으로 나뉩니다. HTTP/2의 데이터 프레임은 본질적으로 HTTP/1.1에서의 청크 전송처럼 작동합니다. 따라서 HTTP/2는 자연스럽게 데이터 스트리밍을 지원할 수 있습니다. HTTP/2에서는 이와 같은 스트리밍의 특성을 이용해 푸시를 전송하는데 이를 '스트리밍 푸시'라고 부릅니다.

자, 이제 앞서 말씀드린 요구 사항을 만족하게 됐을까요? 그렇습니다. 우리는 HTTP/2 표준을 따랐고 아무것도 변경하지 않았습니다. 이에 따라 스트리밍 푸시를 하기 위해서 어떤 라이브러리든 쉽게 사용할 수 있고, 요청하지 않은 푸시도 클라이언트로 보낼 수 있습니다. 

 

아래는 작동 방식을 나타낸 그림입니다. 

 

클라이언트에서 특정 경로를 향한 스트림을 열면 클라이언트와 서버 모두 이 스트림이 스트리밍 푸시에 사용된다는 것을 알게 됩니다. LEGY는 헤더를 수신하고 푸시 세션이 열린 직후 바로 상태 '200'으로 응답합니다. 실제 구독 요청 자체는 sign-on 데이터로 이 세션 안에서 전송됩니다. LEGY는 필요한 서비스를 구독하고 구독 결과를 클라이언트로 반환합니다. 이제 서비스가 LEGY에 푸시 데이터를 보낼 때마다 LEGY는 요청하지 않은 푸시를 클라이언트로 보냅니다. 푸시 유형에 따라 클라이언트는 푸시에 대한 ACK 응답을 LEGY로 보낼 수 있습니다.  

롱 폴링 요청도 스트리밍 푸시에서 처리할 수 있습니다. 

 

Sign-on 데이터를 세션 시작할 때 딱 한 번만 전송하는 구독과는 다르게 롱 폴링은 클라이언트가 sign-on 결과를 수신할 때마다 계속 보냅니다. 따라서 롱 폴링으로 작동하는 fetchOps는 스트리밍 푸시 세션 혹은 별도 스트림에서 작동할 수 있습니다. 

 

TLS와 세션 재개(session resumption)

마지막으로 TLS입니다. 먼저 TLS 핸드셰이킹이 왜 느린지 말씀드리겠습니다. 현재 상위 프로토콜에 상관없이 트랜스포트 프로토콜로 TCP(Transmission Control Protocol)를 사용하고 있습니다. 일반적으로 TCP를 사용할 때의 통신 순서는 아래 왼쪽과 같이 간단합니다. 클라이언트와 서버는 1 RTT인 TCP 3-way 핸드셰이킹을 수행한 뒤 즉시 애플리케이션 데이터 전송을 시작합니다. LEGY 암호화는 일반 TCP에서 실행됩니다. 

 

위 오른쪽 이미지는 TLS 1.2 버전이 어떻게 작동하는지 보여줍니다. TLS가 TCP에서 작동하기 때문에 먼저 3-way 핸드셰이킹을 수행합니다. 이어서 애플리케이션 데이터를 전송하기 전에 TLS 세션에서 TLS 핸드셰이킹을 수행해서 암호화 파라미터를 협상합니다. 이때 협상하기 위해 추가로 2 RTT를 더 소비하는데요. 이 때문에 네트워크의 속도가 더욱 느려집니다. 특히 모바일 환경에서는 연결이 끊기거나 다시 연결되는 경우가 매우 많기 때문에 상황이 악화됩니다.  

다행히 TLS 1.3 버전은 더 나은 핸드셰이킹 메커니즘을 지원합니다. 협상을 1 RTT로 줄였습니다. 일부 설정에서는 TLS 1.3 버전에서 0 RTT 핸드셰이킹을 제공해 협상 중에 애플리케이션 데이터를 보낼 수도 있습니다. 

 

TLS 1.3 버전을 도입하면 두 가지 방식을 중복으로 사용하고 있던 LEGY 암호화에 대한 걱정을 해결할 수 있을 것이라고 생각했습니다. 그런데 그렇지 않았습니다. 아직도 서비스의 20%는 TLS 1.2 버전을 사용하고 있었기 때문입니다.

그렇다면 혹시 TLS 1.2 버전에서 협상을 1 RTT로 줄일 수는 없을까요? 서버에 다시 연결할 때 첫 번째 연결에서 사용한 암호화 파라미터를 재사용한다면, 클라이언트와 서버가 암호화 파라미터와 같은 세션 정보를 기억하고 재사용하기로 동의한다면, 협상 RTT를 줄일 수 있지 않을까요?

 

이를 세션 재개(session resumption)라고 합니다. 세션 재개를 구현하는 기술로는 두 가지가 있습니다. 첫 번째는 TLS 1.2 버전에서 지원하는 세션 ID이고, 다른 하나는 TLS 익스텐션으로 작동하는 세션 티켓입니다.

먼저 세션 ID는 각 세션에 ID를 부여해 서버에 암호화 파라미터를 저장하기 위한 것입니다. 아래 예시에서는 세션이 새로 열리면서 LEGY 서버에 ID: 1로 저장됐습니다. 

 

그런데 이후 클라이언트가 TLS 세션 ID: 1을 이용해 다시 LEGY에 연결할 때, 다른 LEGY 서버 인스턴스로 연결될 가능성이 있습니다. 그렇게 되면 이 서버는 세션 ID: 1을 알지 못하기 때문에 다시 완전한 TLS 핸드셰이킹을 수행하면서 두 개의 RTT를 소비해 버립니다. 

 

따라서 세션 ID가 작동하기 위해서는 모든 LEGY 서버 인스턴스가 세션 스토리지를 공유해야 합니다. 이를 위해 현재 세션 스토리지로 랑데부 해싱(Rendezvous Hashing) 방식을 적용한 Redis 클러스터를 사용하고 있습니다.

 

LEGY는 이제 세션 ID를 이용해 어떤 Redis 인스턴스를 요청할지 알 수 있습니다. 위 예시의 두 번째 연결은 LEGY 서버에서 세션 정보를 다시 복원해 핸드셰이킹을 1 RTT로 완료할 수 있었습니다.

그런데 세션 ID는 스테이트풀(stateful) 기술이기 때문에 모든 세션을 저장해야 합니다. 이에 자원 사용량을 줄이기 위해 세션 티켓(Sessoin Ticket) 기술을 사용했습니다.

세션 ID와는 다르게 세션 티켓은 스테이트리스(stateless) 세션 재개입니다. 세션을 서버에 저장하지 않고 티켓이라고 부르는 세션 데이터를 생성해서 이 티켓을 클라이언트로 리턴합니다. 세션에는 세션 정보가 있는데요. LEGY에서 그 정보를 추출해서 세션 정보를 얻어 티켓을 만듭니다. 클라이언트는 리턴된 티켓과 세션 정보를 연결합니다. 티켓은 LEGY의 비밀 키를 사용해서 암호화하기 때문에 클라이언트에서 조작할 수 없습니다. 보안을 위해 LEGY는 암호화 키를 로테이팅하며 생성한 키는 일정 시간 동안 계속 사용할 수 있습니다. 또한 키 부족을 방지하기 위해 추후 필요한 키를 미리 프로비저닝해 둡니다. 이를 통해 LEGY 인스턴트 간의 키 동기화를 더 쉽게 할 수 있습니다.

 

클라이언트는 두 번째로 LEGY에 접속할 때 LEGY로 티켓을 발송하며, LEGY는 이를 복호화해서 확인합니다. 이 과정을 성공적으로 완료하면 LEGY는 티켓에서 추출한 세션 정보를 사용하고, 클라이언트는 연결된 세션 정보를 사용합니다. 만약 티켓이 만료되면 전체 TLS 핸드셰이킹을 다시 수행합니다. 

 

서버 측에서 고려해야 할 것은 키 관리뿐입니다. 현재 키 관리 서비스를 이용해서 키를 배포하고 있으며, 키는 LEGY에서 생성하고 있습니다. 모든 LEGY 인스턴스가 같은 생성 로직을 사용하기 때문에 CronJob은 키 생성 요청을 LEGY 인스턴스 중 아무 데나 하나로 보내면 됩니다. 생성된 키는 키 관리 서비스에 업로드합니다. LEGY 인스턴스는 주기적으로 오래된 키를 버리고 키 관리 서비스에서 새로운 키를 가져와 채워 넣습니다. 이렇게 하면 모든 LEGY 인스턴스가 동일한 키를 사용해서 세션 재개를 할 수 있습니다. 

 

아래는 TLS 개선 결과의 예시로, 한국에 있는 클라이언트에서 미국에 있는 LEGY로 TLS 연결을 설정하는 중에 패킷을 캡처한 것입니다. 

 

맨 위 TLS 1.2 버전에서의 연결은 핸드셰이킹을 위해 두 개의 RTT를 사용했고 600ms가 걸렸습니다. 그 아래 두 이미지는 세션 ID와 세션 티켓을 사용해 핸드셰이킹을 1 RTT 줄인 것인데요. TLS 1.2 버전에서 소요된 시간의 절반인 300ms가 소요됐습니다.  

아래는 TLS 1.3 버전 패킷 캡처입니다. TLS 1.3 버전은 늘 1 RTT로 핸드셰이킹을 수행하기 때문에 세션 재개를 사용하든 사용하지 않든 1 RTT로 수행되는 것을 확인할 수 있습니다.

 

현재 LEGY에 연결된 서비스의 93%가 TLS를 사용하고 있고, 그중 80%는 TLS 1.3 버전을, 20%는 TLS 1.2 버전의 세션 재개를 사용하고 있습니다. 향후 오래된 클라이언트가 업그레이드되면 TLS 사용량과 TLS 1.3 버전 사용량이 더욱 증가할 것이라고 생각합니다.  

 

마치며

마지막으로 정리해 보겠습니다. LINE이 탄생해서 글로벌로 서비스한 지 10년이 지났습니다. LINE은 앞으로 10년 동안 더욱 성장할 텐데요. 우리의 성장은 안정성과 확장성, 보안성 등 다양한 측면을 개선하면서 한편으로 사용자 경험을 해치지 않아야 합니다.

이와 같은 목표의 첫 번째 단계로 네트워크 스택을 개선했고, 보안을 강화하기 위해 적극적으로 TLS를 도입했습니다. 개선 과정에서 사용자 경험에 나쁜 영향을 끼치지 않도록 TLS 1.3 버전을 도입하고 TLS 1.2 버전의 세션 재개와 세션 ID, 세션 티켓 기술을 구현했습니다. 그 결과 현재 연결의 93%가 TLS를 사용하고 있습니다. 

 

또한 안정성을 높이기 위해 SPDY 프로토콜을 단계적으로 배제하며 HTTP/2를 채택해 표준을 준수하고 있습니다. 현재 연결의 85%가 HTTP/2를 사용하고 있고 향후 모든 클라이언트가 HTTP/2를 사용하게 될 것입니다. 

 

확장성을 높이기 위해서는 스트리밍 푸시라는 푸시 메커니즘을 구현했습니다. 이를 통해 롱 폴링과 구독 요청을 원하는 대로 처리할 수 있으며, 롱 폴링 요청을 실제 푸시로 매끄럽게 마이그레이션할 수 있었습니다. 

 

글로벌 네트워크 환경은 끊임없이 변하고 있으며 새로운 프로토콜이 지속해서 나오고 있습니다. IPv6가 널리 사용되기 시작했고, HTTP/3와 QUIC과 같은 새로운 프로토콜이 나오고 있습니다. 새로운 버전의 TLS가 나올 수도 있고, 컴퓨팅 성능이 더욱 강력해지면서 일부 암호화 방법에서 취약점이 발견될 수도 있습니다. 이런 상황에서 우리는 안정성과 보안, 신뢰성이 서비스의 품질을 높이는 핵심이라는 것을 알고 있으며, 끊임없이 변하는 네트워크 환경 속에서 연결성을 지속적으로 향상시켜 나갈 것입니다. 긴 글 읽어주셔서 감사합니다.