Netflix의 웹 성능 최적화를 위한 기법 소개
넷플릭스는 전 세계적으로 유명한 스트리밍 콘텐츠 서비스이다.
그만큼 서비스 내 사용되는 콘텐츠 데이터가 엄청나며 이를 이용하는 대용량 트래픽 또한 만만치 않을 것이다.
그럼에도 불구하고 신기한 점은, 수많은 트래픽 처리 속에서도 저렇게 많은 섬네일 이미지와, 이미지 위에 마우스를 올리면 보여지는 일부 영상이 매우 빠른 로딩 속도를 자랑한다는 것이다.
넷플릭스는 과연 어떻게 이러한 성능을 만들어 낼 수 있었을까?
지금부터 넷플릭스 페이지의 비밀에 대해 알 수 있는 만큼 최대한 파헤쳐 보도록 하자.
섬네일 이미지 사용
우선 개발자 도구-네트워크 탭을 열어, 페이지 로드 시 요청받는 리소스 목록들을 확인해 보았다.
페이지 렌더링이 완료된 후에도 쉬지 않고 계속해서 많은 리소스가 다운로드되며 리스트가 쭉쭉 늘어나길래 깜짝 놀랐다... 알고 보니 자동으로 재생되는 일부 영상 때문에 그랬던 것인데, 이 부분은 나중에 뒤에서 더 자세히 알아보도록 하자.
차세대 이미지 포맷 형식인 webP 사용
리소스 목록 중 섬네일 이미지를 클릭해 보면 해당 리소스의 콘텐츠 타입과 요청 URL, 리소스 요청 방식 등에 대해 알 수 있었다.
위에 표시한 것처럼 Content-Type이 image/webp로 되어 있다. webp는 JPG, PNG와 같이 이미지 포맷 형식 중 하나이며, 최고의 이미지 압축률을 자랑하는 차세대 포맷 형식이다. 따라서 웹 서비스 내에서 이미지 사용에 있어 성능 최적화 용도로 매우 좋다.
그러나 차세대 최신 포맷 형식인 만큼 webP를 지원하는 브라우저가 아직 많지 않다는 문제가 있다. 현재(2020.09.29일 기준) 애플의 사파리 브라우저도 14 버전이 되고서야 webP 포맷 형식을 지원하게 되었다.
HTTP 1.1의 Keep-Alive 커넥션 통신
Connections 헤더를 보면 keep-alive로 나와있는 것을 알 수 있다. 이것은 HTTP 1.1에서 네트워크 통신 성능 향상을 위해 추가된 기능이다.
이 부분에 대해 네트워크 개념을 살짝 설명을 하자면, 기존의 TCP 통신(기본적인 HTTP 통신으로 생각해도 무방)에서는 한 번의 데이터를 주고받기 위해 수신자와 송신자의 연결에 문제가 없는지 확인하기 위해 3way-handshaking, 4way-handshaking 방식을 이용하여 여러 번 신호를 주고받은 뒤 네트워크 커넥션이 이루어진다.
💡3way-handshaking이 무엇인가요?
수신자와 송신자가 네트워크 통신이 정확하게 이루어질 수 있도록 연결 전에 세 번의 신호를 주고받는다.
송신자가 수신자에게 '네트워크 신호를 보내려고 한다'라는 첫 신호를 보내면, 수신자가 응답으로 '확인했다. 통신을 시도해도 좋다.'라는 신호를 보낸다. 이를 확인한 송신자는 '그러면 이제 본래 목적인 네트워크 통신을 시도하겠다'라는 마지막 확인 신호를 보낸 뒤, 원래 목적의 통신 신호를 송신자에게 보내게 된다.
이처럼 한 번의 통신을 문제없이 정확하게 이루어지도록 하기 위해, 연결 전 3번의 신호를 주고받는다는 의미로 3way-handshaking이라고 한다.
💡4way-handshaking이 무엇인가요?
3way-handshaking이 통신 연결을 위해 주고받는 신호였다면, 4way-handshaking은 연결된 신호를 정상적으로 종료하기 위해 주고받는 신호라 할 수 있다. 송신자가 수신자에게 '이제 연결을 끊을 것이다'라는 첫 번째 신호를 보내면, 송신자는 '알겠다. 나도 종료 준비가 되는대로 신호를 보내겠다'라는 신호를 보낸다. 그리고 송신자는 네트워크 통신을 하던 작업을 마저 마무리한 뒤, 세 번째 신호로 '이제 나도 종료할 준비가 되었으니, 통신을 끊어도 좋다.'라는 신호를 보낸다. 송신자는 이 세 번째 신호를 받고 마지막으로 '알겠다. 나도 이제 신호를 끊겠다.'라는 신호를 보낸 뒤 통신을 끊게 된다.
이처럼 정상적인 통신 종료를 위해 4번의 신호를 주고받는다 하여 4way-handshaking이라고 부른다.
위와 같이 다 한 번의 신호를 주고받기 위해 부가적으로 주고받는 신호가 이렇게 많은 것을 알 수 있다...
위 설명과 같이 연속적으로 데이터를 주고받아야 할 때에는 부가적으로 주고받는 신호가 매우 급격히 늘어나 통신 성능 저하를 일으키게 된다.
이를 방지하기 위해 keep-alive timer를 두고 설정한 시간 동안 계속해서 네트워크가 연결 상태에 머무르도록 한다. 그러면 해당 타이머 동안에 불필요한 신호를 제외하고 연속적으로 데이터를 주고받으면서 빠르게 통신을 할 수 있게 된다.
gzip을 통한 콘텐츠 데이터 압축 전송
콘텐츠를 전송 시 빠른 전송을 위해 세션 계층(OSI 7 계층 중 5 계층)에서 해당 콘텐츠 데이터를 압축하게 되는데, Accept-Encoding은 클라이언트 단에서 받아들일 수 있는 압축 방식을 말한다. 일반적으로 웹 페이지에서 사용되는 리소스(HTML, CSS, JS, 이미지 파일 등)들에 대한 압축 알고리즘은 여러 가지가 있다.
위 사진에서는 Accept-Encoding에 gzip이 표기되어있는 것을 알 수 있으며, 요청 시 서버에게 '자신은 gzip, deflate, br 압축 방식을 받아들일 수 있다'라는 것을 알린다. 그러면 서버는 이러한 압축 알고리즘 중 하나를 택하여 압축을 하게 된다.
그중 gzip은 콘텐츠 압축에 있어 원본 데이터를 약 70%를 압축시킬 수 있는 매우 뛰어난 압축률을 자랑한다.
해당 섬네일 이미지를 gzip방식으로 압축하여 전송함으로써 더 빠르게 클라이언트에게 전달할 수 있는 것을 알 수 있었다.
프록시 서버 Nginx 서버 활용
Nginx는 Apach와 같이 웹 서버 환경을 구축해주는 웹 서버 프로그램이다.
나도 Nginx는 이름만 들어봤지 정확히 어떤 것인지 잘 몰랐는데, 이번 기회에 Nginx에 대해 자세히 알아볼 기회를 가지게 되었다. 그래서 아래에 살짝 알아본 내용을 정리해보려고 한다.
💡 Nginx는 무엇인가요?
Nginx는 웹 환경을 구축해주는 웹 서버 소프트웨어이다.
Node.js와 비교로 예시를 들면, Node.js Express로 서버를 구축하면 동적인 콘텐츠(DB 쿼리로 인한 정보 등)를 제공한다. 이러한 서버를 WAS(Web Application Server) 서버라고 한다.
Nginx는 정적인 콘텐츠(HTML, CSS, JS파일)를 내려주는 서버로 웹 서버(Web Server)라고 부르는 것이다.
Nginx에 대해 더 확인해보니, 주로 Nginx를 리버스 프록시 서버로 구축하여 서비스 성능을 향상시킬 수 있는 것을 알 수 있었다.
💡리버스 프록시란?
클라이언트가 서버에게 요청을 하면, 서버에게 바로 통신이 가는 것이 아니라, 중간의 프록시 서버가 뒷 단의 실제 웹 서버에게 요청을 하게 된다. 그러면 웹 서버의 응답을 프록시 서버가 받고 프록시 서버가 클라이언트에게 응답을 전달한다. 이러한 방식으로 클라이언트는 실제 서버의 위치는 모르기 때문에 보안성이 뛰어나며, 정적 파일 전송 또한 프록시 서버의 캐싱된 파일로 바로 전송 가능하다. 또한 로드밸런싱 기능까지 갖추어져 있으므로 트래픽 분산 효과까지 가지고 있다.
따라서 실제 웹 서버 앞 단에 프록시 서버로 Nginx를 세팅하여 사용하는 방식을 많이 이용하는 것 같다.
위 설명과 같이, 보안성, 정적파일 처리 성능 향상, 서버 요청 속도 향상 등으로 인해 Nginx를 이용하는 것을 알 수 있었다.
아마 위 이미지의 Server 헤더에 'nginx'라고 뜨는 것도 실제 웹 서버가 Nginx가 아니라, 중간다리 역할인 프록시 서버가 뜬 것으로 예측된다.
의외로 사용하지 않던 기능, CDN 서버 (넷플릭스에서 자체 CDN 서버를 적극 활용 중)
살짝 의외였던 점은, CDN 서버를 따로 사용하지 않는 점이었다. CDN(Contents Delivery Network) 서버를 이용하게 되면 넷플릭스에 접속하는 모든 유저들의 공용 캐시 서버로 더 빠른 콘텐츠 데이터를 다운로드하여 콘텐츠 다운로드 성능을 향상시킬 수 있다.
위 사진은 내가 트레바리에서 일할 때 AWS CloudFront라는 CDN 서버를 활용하여 공용 캐시 서버를 세팅한 모습인데, CDN 서버를 이용하면 위처럼 x-cache 헤더가 따로 보이게 된다.
(Hit from cloudfront라는 의미는 coudfront 서버에 캐싱된 이미지를 적중하여 S3까지 거치지 않고 바로 다운로드했다는 의미이다.)
만약 CDN 서버를 이용했다면 위 사진과 같이 x-cache의 헤더가 따로 존재했을 것이지만, 넷플릭스에서는 cache-control 헤더(개인 로컬 캐시)밖에 존재하지 않는 것을 보니, CDN 서버는 따로 이용하지 않는 것 같다.
위에서는 섬네일 이미지에 관해서 분석해 보았다면, 아래부터는 영상 처리에 대해서 어떤 방식을 적용해봤는지 알아보자.
작품의 일부 장면을 영상으로 실행
넷플릭스에서는 제일 상단의 메인 배너 혹은, 각 섬네일에 마우스를 갖다 대면 위처럼 해당 작품의 일부 장면이 영상으로 보이게 된다.
각 작품마다 이런 식으로 영상의 데이터를 가져오려면 엄청나게 많은 리소스가 필요할 것임에도 불구하고, 넷플릭스 웹 페이지에서는 엄청나게 빠른 속도로 영상을 재생시킬 수 있다.
콘텐츠 데이터 스트리밍 방식 다운로드
위 화면을 보면, 섬네일 이미지에 마우스를 갖다 대자마자 영상 재생을 위해 바로 필요한 데이터를 요청하며 엄청나게 많은 리소스를 계속해서 다운로드하는 것을 볼 수 있다.
해당 작품의 영상 프레임 전체를 한 번에 가져오게 되면 다운로드 속도가 엄청나게 느리기 때문에 작은 단위로 프레임을 분할 요청하여 다운로드한다. 다운로드된 영상 프레임을 실행하면서 다음에 필요로 한 프레임을 계속해서 요청하는데, 이러한 방식을 우리가 흔히 아는 '스트리밍(Streaming)'이라고 한다.
유튜브 영상이나 다른 실시간 라이브 영상에서도 이와 같이 스트리밍 방식을 사용하는 것을 알 수 있다.
(로그가 계속해서 쌓인다... 저 상태로 놔두면 언젠가는 브라우저가 폭발하지 않을까 생각해본다...)
이처럼 넷플릭스 내 모든 영상 실행은 스트리밍 방식으로 데이터를 가져와서 처리하는 방식인 것을 알 수 있었다.
특이한 점 발견, X-TCP-Info..?
영상 프레임 Response Headers 중, 클라이언트의 IP 주소가 들어가 있는 헤더를 발견했다. (위 IP는 우리 집 실제 IP 주소이다.)
TCP-Info 헤더가 무엇인지 확인해 봤는데 정보가 너무 없었는데, 구글링 5페이지로 넘어가던 중 이런 글귀를 발견할 수 있었다.
아하..! 넷플릭스는 CDN 서버를 사용하고 있었고, TCP-Info의 사용자 IP가 헤더에 추가되어 응답하는 것이 제대로 캐시가 적중되고 있다는 증거였군...(어쩐지.. CDN을 사용하지 않을 리가 없다고 생각했다..)
찾아보니 넷플릭스에서는 자체 CDN 서버를 이용하여 운영 중이었고, X-TCP-Info 헤더는, 넷플릭스 자체 CDN 서버를 이용함으로써 적용되는 헤더였기 때문에 구글링으로도 정보가 없을 수밖에 없던 것이었다.
우선 넷플릭스의 웹 성능 관련하여 직접 확인하며 알아볼 수 있었던 내용들은 아래와 같다.
- 이미지를 차세대 포맷 형식인 webp 이용
- keep-alive 커넥션 연결을 통해 연속적인 통신 리소스 절약
- 좋은 성능의 콘텐츠 압축률 방식인 gzip 인코딩을 통해 이미지 데이터를 전송
- 리버스 프록시 서버 용 Nginx 활용하여 정적 파일 처리 성능 향상 및 보안성 향상
- 자체 CDN 서버를 통해 유저 공용 캐시 서버 활용
- 각 영상 프레임 데이터를 스트리밍 방식으로 처리
위 6가지는 '개발자 도구의 네트워크 탭'만을 이용해서 알아볼 수 있는 내용들이었다.
이처럼 외부에서 확인해볼 수 있는 흔적들 외에도, 내부적으로 웹 성능 향상을 위해 작업 내용들이 있을 것이라는 생각이 들었다.
그래서 따로 '넷플릭스 웹 성능 향상'을 키워드로 구글링을 해보았고, 추가로 알게 된 내용들을 아래에 조금 더 정리해 보았다.
'넷플릭스 웹 성능' 관련 추가로 알아본 내용
역시나 구글링을 해보니, 넷플릭스에서 웹 퍼포먼스 향상을 위해 시도한 내용을 다룬 미디움 글을 확인할 수 있었다.
prefetch와 XHR을 이용한 미리 가져오기 기능 활용
prefetch는 운영체제 공부할 때 향후 필요할 것이라고 예상하는 데이터를 미리 메모리에 적재하는 방식이었던 것으로 기억하는데, 이러한 방법이 웹 성능 향상 기법으로도 쓸 수 있다는 것을 처음 알았다.
<!-- prefetch 사용 -->
<link rel="prefetch" href="page-2.html">
<!-- preload 사용 -->
<link rel="preload" as="script" href="super-important.js">
<link rel="preload" as="style" href="critical.css">
위처럼 <head> 태그 안에 브라우저 API 중 하나인 <link rel=prefetch>를 이용하여 사용할 수 있다.
이것은 브라우저에서 그 리소스를 미리 가져와 사용할 수도 있다는 것을 알리는 것이며, 여기서 리소스란 HTML, CSS, JS 파일 또는 이미지 등과 같은 파일이 될 수 있겠다.
(추가로 prefetch 뿐 아니라, preload, preconnection이라는 기능도 있는데, 이 부분은 향후 정확하게 알아봐야겠다.)
XHR(XMLHttpRequest)
이 뿐 아니라 XHR(XMLHttpRequest)를 이용해서도 미리 사용할 리소스를 선언할 수 있다.
// 새로운 XHR 요청 생성
const xhrRequest = new XMLHttpRequest();
// 리소스를 미리 가져오기 위해 요청 open
xhrRequest.open('GET', '../bundle.js', true);
// 요청 보내기
xhrRequest.send();
XHR로 리소스를 미리 가져오는 방법은 수년간 브라우저의 표준이었고, 브라우저가 리소스를 캐시 하도록 했을 때 95%의 성공 확률을 보였다고 한다. 그러나 이 방법은 prefetch 방식과 다르게 HTMl 문서에는 적용할 수 없어서 넷플릭스는 다음 이어지는 페이지의 자바스크립트와 CSS 파일을 미리 가져오도록 사용했다고 한다. 그리고 prefetch와 XHR을 이용한 리소스 미리 가져오는 방식을 통해 페이지 로드 속도를 30% 개선시킬 수 있었다고 한다.
(나중에 prefetch와 XHR을 이용해서 리소스 미리 가져오는 것에 대해서는 따로 공부해보면 엄청 큰 도움이 될 것 같다..!)
React와 클라이언트 측 라이브러리들을 바닐라(Vanilla) 자바스크립트로 리펙토링
위 미디움에 따르면, 컴포넌트가 재활용성이 필요한 페이지나 동적 처리가 많은 스크립트 코드를 제외하곤, 기본적인 랜딩 페이지나 기본 동작 스크립트, 라이브러리들은 모두 리액트를 걷어내고 바닐라(Vanilla) 자바스크립트로 변경하여 위와 같은 성과를 얻을 수 있었다고 한다.
리액트가 개발 생산성과 효율성을 높여줄 수는 있지만, 역시나 그만큼 부가적인 빌드 코드와 리액트 환경 관련된 코드들이 추가되기 때문에 성능 면으로는 바닐라 스크립트보다 엄청 뒤떨어질 수밖에 없나 보다.
마지막으로 아래는 위에서 설명한 넷플릭스에서 웹 성능 향상을 위해 작업했던 내용들을 발표하는 영상이다.
나도 언젠간 큰 성과를 통해 이런 발표를 꼭 하고 싶다...
이렇게 넷플릭스에서 페이지 웹 성능 향상을 위하여 시도한 방법에 대해 최대한 알아보았다.
기술적인 요소들을 이용한 방식도 있는 반면에 기존 코드들을 더욱 단순화시켜 성능을 향상시킴으로써, 유저들에게 더욱 빠른 서비스를 제공하기 위해 노력한 흔적들을 많이 찾아볼 수 있었다.
이번 글을 정리하면서 내가 몰랐던 웹 성능을 향상시킬 수 있는 방법에 대해 몇 가지 또 알 수 있게 되었고, 또 한수 배운 느낌이라 너무 기쁘다.
나중에 Nginx를 Node.js와 연동하여 사용하는 방식과 리소스를 미리 가져오는 방식에 대해서 따로 공부해보고 블로깅을 해보면 좋을 것 같다.