Netflix의 Java 사용 변천사 (번역)

Netflix는 Java, RxJava 및 Spring Boot를 활용해 마이크로서비스 아키텍처를 발전시켰으며, GraphQL Federation으로의 전환을 통해 더욱 발전했다. 이 글에서는 Netflix의 아키텍처 변화와 Java 사용 변천사를 설명한다.

Netflix의 Java 사용 변천사 (번역)
원글: Evolution of Java Usage at Netflix by ByteByteGo
Evolution of Java Usage at Netflix
Stop releasing bugs with fully automated end-to-end test coverage (Sponsored) Bugs sneak out when less than 80% of user flows are tested before shipping. But how do you get that kind of coverage? You either spend years scaling in-house QA — or you get there in just 4 months with QA Wolf

Intro

최근에 다시 Java와 Spring Boot를 사용하는 회사로 이직하면서, 지난 2년간 Relate에서 Ruby on Rails를 사용하며 잊고 지내던 Java와 Spring 생태계에 다시 발을 내딛었다.

현재 회사에서는 수많은 모놀리틱 서비스와 마이크로서비스가 함께 사용되는 상황이라 꽤 복잡한 아키텍처로 구성되어 있다. 그리고 새로운 Java 기반 서비스들이 빠르게 추가되고 있어서 각 서비스들 간의 통신이나 아키텍처 구성을 어떻게 개선해가면 좋을지 고민되는 상황이었다.

팀에 빅테크(FAANG) 출신 시니어 개발자들도 있어서 내가 이 개선을 주도하지는 않겠지만, 적어도 개선 방향에 대한 내 의견을 가지고 있으면 좋겠다는 생각으로 힌트를 얻고자 Java 기반 서비스들의 큰 형님격인 Netflix의 문을 두드렸다.

Netflix는 Java 기반 서비스들의 아키텍처를 어떻게 발전시켜나갔는지 궁금해서 찾아보다가 이 글을 찾게 되었고, 오랜만에 Java 생태계로 돌아온 나에게 너무나 유익했던 글이라 DeepL의 도움을 받아 번역해두게 되었다.


📌 목차

  1. Groovy 시대 with BFF(Backend For Frontend)
  2. RxJava와 리액티브 프로그래밍 사용
  3. GraphQL Federation으로 전환
  4. Netflix가 사용하는 Java 버전
  5. Netflix가 사용하는 Spring Boot

Netflix는 주로 Java를 사용하고, 모든 백엔드 서비스는 Java 서비스입니다. 여기에는 다음이 포함됩니다:

  • 내부 applications
  • 세계에서 가장 큰 영화 스튜디오 중 하나를 구동하고, 영화 제작에 사용되는 소프트웨어
  • Netflix 스트리밍 앱

하지만 Netflix의 Java 스택이 고정되어 있는 것은 아닙니다. 수년에 걸쳐 크게 발전해 왔습니다.

이 글에서는 변화하는 요구 사항을 지원하기 위해 이루어진 전반적인 아키텍처 변화에 비추어 Neflix에서 Java 사용의 진화를 살펴보겠습니다.

Groovy 시대 with BFF(Backend For Frontend)


Netflix가 마이크로서비스 아키텍처를 가지고 있다는 것은 이미 잘 알려진 사실입니다.

모든 기능과 데이터는 마이크로서비스가 소유하고 있으며, 수천 개의 마이크로서비스가 존재합니다. 또한 여러 마이크로서비스가 서로 통신하여 보다 복잡한 기능을 구현합니다.

예를 들어 Netflix에 접속하면 LOLOMO 화면이 표시됩니다. 여기서 LOLOMO는 list-of-list-of-movies의 약자로, 기본적으로 다음과 같은 여러 마이크로 서비스에서 데이터를 가져와서 구축됩니다:

  • 상위 10개 영화 목록을 반환하는 서비스
  • 영화별로 개인화된 이미지를 제공하는 Artwork 서비스
  • 영화 제목, 배우 정보 및 설명을 반환하는 영화 메타데이터 서비스
  • 사용자 홈 페이지에 실제로 렌더링할 목록을 제공하는 LOLOMO 서비스.

아래 다이어그램은 이러한 상황을 보여줍니다.

The Service Landscape at Netfilx
출처: https://blog.bytebytego.com/p/evolution-of-java-usage-at-netflix

Netflix 앱에서 하나의 화면을 렌더링하는 데 10개의 서비스를 호출해야 할 수도 있습니다.

하지만 디바이스(예: TV)나 모바일 앱에서 이렇게 많은 서비스를 호출하는 것은 일반적으로 비효율적입니다. 10개의 네트워크 호출은 확장성이 떨어지고 고객 경험에 좋지 않은 결과를 초래합니다. 많은 스트리밍 앱이 이러한 성능 문제를 겪고 있습니다.

이러한 문제를 방지하기 위해 Netflix는 다양한 API에 대해 단일 프런트 도어를 사용했습니다. 디바이스는 이 프런트 도어를 호출하여 모든 다른 마이크로서비스로의 fanout(전파)을 수행합니다. 프런트 도어는 gateway 역할을 하며 Netflix는 이를 위해 Zuul을 사용했습니다.

이 접근 방식은 여러 마이크로서비스에 대한 호출이 매우 빠른 내부 네트워크에서 이루어지기 때문에 성능에 영향을 미치지 않습니다.

하지만 해결해야 할 또 다른 문제가 있었습니다.

사용자가 Netfilx에 접속하는 데 사용할 수 있는 모든 디바이스는 미묘한 방식으로 서로 다른 요구 사항을 가지고 있습니다. Netfilx는 모든 디바이스에서 UI와 동작의 일관된 모양과 느낌을 유지하려고 노력했지만, 각 디바이스마다 메모리나 네트워크 대역폭에 대한 제한이 다르기 때문에 데이터를 로드하는 방식이 조금씩 다릅니다.

이렇게 다양한 기기에서 모두 작동할 수 있는 단일 REST API를 만드는 것은 어렵습니다. 다음과 같은 몇 가지 문제점이 있습니다.:

  • 너무 많은 데이터를 가져오거나 너무 적은 데이터를 가져오는 REST API
  • 모든 데이터 요구 사항을 처리하기 위해 하나의 REST API를 만들더라도 많은 데이터가 낭비되기 때문에 좋지 않은 경험이 될 수 있습니다.
  • API가 여러 개일 경우, 네트워크 호출이 여러 번 발생하게 됩니다.

이를 처리하기 위해 넷플릭스는 Backend For Frontend (BFF) 패턴을 사용했습니다.

이 패턴에서는 모든 프론트엔드 또는 UI에 자체 미니 백엔드가 있습니다. 미니 백엔드는 fanout을 수행하고 특정 지점에서 UI에 필요한 데이터를 가져오는 역할을 담당합니다.

아래 다이어그램은 BFF 패턴의 개념을 보여줍니다.

The Backend for Frontend Pattern at Netflix
출처: https://blog.bytebytego.com/p/evolution-of-java-usage-at-netflix

Netflix의 경우, BFF는 기본적으로 특정 디바이스의 특정 화면을 위한 Groovy 스크립트였습니다.

특정 화면을 렌더링하는 데 필요한 정확한 데이터를 알고 있었기 때문에 UI 개발자가 스크립트를 작성했습니다. 일단 작성된 스크립트는 API 서버에 배포되고, 적절한 Java 클라이언트 라이브러리를 호출하여 모든 마이크로서비스에 대한 fanout을 수행했습니다. 이러한 클라이언트 라이브러리는 gRPC 서비스 또는 REST 클라이언트를 위한 wrapper였습니다.

아래 다이어그램은 이 설정을 보여줍니다.

Netflix Groovy Era Java Services
출처: https://blog.bytebytego.com/p/evolution-of-java-usage-at-netflix

RxJava와 리액티브 프로그래밍 사용

Groovy 스크립트는 fanout을 수행하는 데 도움이 되었습니다.

하지만 Java에서 이러한 fanout을 수행하는 것은 간단하지 않습니다. 기존의 접근 방식은 많은 스레드를 생성하고 최소한의 스레드 관리를 사용하여 fanout을 관리하는 것이었습니다.

그러나 내결함성(fault tolerance) 때문에 상황이 빠르게 복잡해졌습니다. 여러 서비스를 처리할 때 그중 하나가 충분히 빠르게 응답하지 않거나 장애가 발생하면 스레드를 정리하고 제대로 작동하는지 확인해야 하는 상황이 발생하게 되었습니다.

Netfilx는 RxJava와 리액티브 프로그래밍을 통해 모든 스레드 관리의 복잡성을 처리함으로써 fanout을 더 나은 방식으로 처리할 수 있었습니다.

Netflix는 RxJava를 기반으로 failover와 bulkheading을 처리하는 Hystrix라는 내결함성(falut tolerance) 라이브러리를 만들었습니다. 리액티브 프로그래밍이 복잡하긴 했지만 당시로서는 매우 합리적이며 이 아키텍처를 통해 Netflix의 트래픽 요구 사항을 대부분 처리할 수 있었습니다.

하지만 이 접근 방식에는 몇 가지 중요한 한계가 있었습니다:

  • 각각의 endpoint를 위한 스크립트가 있었기 때문에 유지/관리해야 할 스크립트가 많았습니다.
  • UI 개발자는 모든 미니 백엔드를 만들어야 했고, Groovy Java 세계에서 RxJava로 작업하는 것을 좋아하지 않았습니다. RxJava는 UI 개발자들의 주로 사용하는 기본 언어가 아니기 때문입니다.
  • 리액티브 프로그래밍은 일반적으로 어렵고 학습 곡선이 가파릅니다.

GraphQL Federation으로 전환

지난 몇 년 동안 Netflix는 Java 서비스와 관련하여 완전히 새로운 아키텍처로 마이그레이션해 왔습니다. 이 새로운 아키텍처의 핵심은 GraphQL Federation입니다.

GraphQL과 REST를 비교할 때 가장 큰 차이점은 GraphQL에는 항상 스키마가 있다는 것입니다. 이 스키마는 다음과 같은 몇 가지 주요 측면을 정의하는 데 도움이 됩니다:

  • 다양한 쿼리 및 변형을 포함한 모든 작업
  • 쿼리에서 반환되는 유형에서 사용할 수 있는 필드

GraphQL을 사용하면 클라이언트는 필드 선택에 대해 명시적이어야 합니다. 그냥 영화를 요청하고 영화에서 모든 데이터를 가져올 수는 없습니다. 대신 영화의 제목과 다양한 리뷰의 점수를 얻고 싶다고 구체적으로 언급해야 합니다. 필드를 요청하지 않으면 해당 필드를 가져올 수 없습니다.

반대로 REST는 REST 서비스가 보내기로 결정한 모든 것을 받습니다.

클라이언트가 GraphQL에서 쿼리를 지정하는 작업은 더 많지만, 실제로 필요한 것보다 훨씬 더 많은 데이터를 가져오는 over fetching과 관련된 모든 문제를 해결합니다. 이를 통해 모든 다양한 UI에 서비스를 제공할 수 있는 하나의 API를 만들 수 있는 길이 열립니다.

GraphQL을 보강하기 위해 Netflix는 한 걸음 더 나아가 GraphQL Federation을 사용하여 이를 마이크로서비스 아키텍처에 다시 적용했습니다.

아래 다이어그램은 GraphQL Federation을 사용한 설정을 보여줍니다.

GraphQL Federation at Netflix
출처: https://blog.bytebytego.com/p/evolution-of-java-usage-at-netflix

보시다시피, 마이크로서비스는 이제 DGS 또는 도메인 그래프 서비스라고 불립니다.

DGS는 GraphQL 서비스를 구축하기 위해 Netflix에서 개발한 사내 프레임워크입니다. GraphQL과 GraphQL Federation으로 전환하기 시작했을 때, Netflix 규모에서 사용할 수 있을 만큼 충분히 성숙한 Java 프레임워크가 없었습니다. 따라서 저수준 GraphQL Java 프레임워크 위에 구축한 후 스키마 유형에 대한 코드 생성 및 Federation 지원과 같은 기능으로 보강했습니다.

핵심적으로 DGS는 GraphQL endpoint와 스키마가 있는 Java 마이크로서비스일 뿐입니다.

여러 개의 DGS가 있지만, TV와 같은 디바이스의 관점에서 보면 하나의 큰 GraphQL 스키마만 존재합니다. 이 스키마에는 렌더링할 수 있는 모든 가능한 데이터가 포함되어 있습니다. 디바이스는 백엔드에서 스키마의 일부인 다양한 마이크로서비스에 대해 걱정할 필요가 없습니다.

예를 들어, LOLOMO DGS는 제목만으로 영화 유형을 정의할 수 있습니다. 그런 다음 이미지 DGS는 해당 타입 영화를 확장하고 여기에 Artwork URL을 추가할 수 있습니다. 서로 다른 두 DGS는 서로에 대해 아무것도 알지 못합니다. Federation gateway에 스키마를 게시하기만 하면 됩니다. Federation gateway는 모두 GraphQL endpoint를 가지고 있기 때문에 DGS와 대화하는 방법을 알고 있습니다.

이 설정에는 몇 가지 장점이 있습니다:

  • 더 이상 API 중복이 없습니다.
  • 필드 선택 기능으로 인해 다양한 기기를 지원할 수 있을 만큼 유연한 API인 GraphQL을 사용하기 때문에 Backend For Frontend(BFF)가 필요하지 않습니다.
  • UI 개발자의 서버 측 개발이 필요 없습니다. 백엔드 개발자와 UI 개발자는 스키마에 대한 협업만 하면 됩니다.
  • 더 이상 Java로 된 클라이언트 라이브러리가 필요하지 않습니다. Federation gateway는 특정 코드를 작성할 필요 없이 일반 GraphQL 서비스와 대화하는 방법을 알고 있기 때문입니다.

Netflix가 사용하는 Java 버전

최근 Netflix는 Java 8에서 Java 17로 마이그레이션했습니다. 마이그레이션 후 코드 변경 없이 Java 8에 비해 Java 17에서 약 20% 더 나은 CPU 사용량을 확인했습니다. 이는 G1 가비지 컬렉터의 개선 덕분이었습니다. Netflix의 규모를 고려할 때 CPU 사용률이 20% 향상되었다는 것은 비용 측면에서 큰 의미가 있습니다.

일반적인 생각과는 달리 Netflix에는 자체 JVM이 없습니다. OpenJDK 빌드인 Azul Zulu JVM을 사용하고 있을 뿐입니다.

전체적으로 Netflix에는 약 2800개의 Java 애플리케이션이 있으며, 대부분 다양한 크기의 마이크로서비스입니다. 또한 약 1500개의 내부 라이브러리를 보유하고 있습니다. 이 중 일부는 실제 라이브러리이지만 대부분은 gRPC 또는 REST 서비스 앞에 있는 클라이언트 라이브러리일 뿐입니다.

빌드 시스템의 경우 Netflix는 Gradle을 사용합니다. Gradle과 함께 오픈 소스 Gradle 플러그인 세트인 Nebula를 사용합니다. Nebula는 재현 가능한 빌드에 도움이 되는 버전 잠금을 지원합니다.

최근 넷플릭스는 Java 21의 변경 사항을 적극적으로 테스트하고 배포하고 있습니다. Java 8에서 Java 17로 전환할 때와 비교하면 Java 17에서 21로 전환하는 것이 훨씬 쉬워졌습니다. Java 21은 다음과 같은 몇 가지 중요한 기능도 제공합니다:

  • 가상 스레드를 사용하면 thread-per-request 스타일로 작성된 서버 측 애플리케이션을 최적의 하드웨어 사용률로 확장할 수 있습니다. thread-per-request 스타일에서는 요청이 들어오면 서버가 이에 대한 스레드를 제공합니다. 요청에 대한 모든 작업은 이 스레드에서 이루어집니다.
  • 짧은 일시 중지 시간에 초점을 맞추고 더욱 다양한 사용 사례에서 잘 작동하는 ZGC 가비지 컬렉터를 사용합니다.
  • 레코드와 패턴 매칭을 조합한 데이터 지향 프로그래밍

Netflix가 사용하는 Spring Boot

Netflix는 Spring Boot를 사용하는 것으로 유명합니다.

지난 1년여 동안 Guice 기반의 자체 개발 Java 스택에서 완전히 벗어나 Spring Boot로 완전히 표준화했습니다. 왜 스프링 부트일까요? 가장 널리 사용되는 Java 프레임워크이며 수년 동안 매우 잘 유지 관리되어 왔기 때문입니다.

Netflix는 Spring 프레임워크의 방대한 오픈 소스 커뮤니티, 기존 문서, 쉽게 이용할 수 있는 교육 기회를 활용함으로써 많은 이점을 발견했습니다. Spring의 발전과 그 기능은 "highly aligned, loosely coupled"이라는 Netflix의 핵심 원칙에 매우 잘 부합합니다.

Netflix는 최신 버전의 OSS Spring Boot를 사용하며, 오픈 소스 커뮤니티와 최대한 긴밀하게 협력하는 것을 목표로 삼고 있습니다. 그러나 Netflix 생태계 및 인프라와 긴밀하게 통합하기 위해 Spring Boot 위에 구축된 여러 모듈로 구성된 Spring Cloud Netflix도 만들었습니다.

Spring Cloud Netflix는 다음과 같은 여러 가지를 지원합니다:

  • gRPC 클라이언트
  • AuthZ 및 AuthN을 위한 Netflix SSO 스택과 통합된 서버 지원
  • Tracing, Metric 및 분산 로깅 형태의 관찰 기능
  • mTLS를 지원하는 HTTP 클라이언트
  • Eureka를 통한 서비스 검색
  • AWS/Titus 통합
  • Kafka, Cassandra, 그리고 Zookeeper integration 통합

결론

Netflix Java 스택은 지난 몇 년 동안 사내 프레임워크에서 Groovy 시대의 마이크로서비스에 이르기까지, 그리고 최근에는 GraphQL Federation으로 이동하면서 발전해 왔습니다.

모든 변화는 이전 접근 방식의 문제를 해결하기 위해 이루어졌습니다. 예를 들어, RxJava로의 전환은 fanout을 더 나은 방식으로 처리하기 위한 것이었고, GraphQL Federation으로의 전환은 RxJava로 인한 복잡성 문제를 해결하기 위한 것이었습니다.

이러한 변화와 함께 Java 언어 버전도 Java 8에서 17로, 그리고 현재 21 이상으로 발전해 왔습니다. 이러한 변화의 대부분은 마침내 Spring Boot 버전 3이 Java 8을 넘어서면서 전체 에코시스템의 업그레이드를 유도한 결과이기도 합니다.

이러한 변화로 인해 CPU 비용을 절감할 수 있는 더 우수한 성능의 애플리케이션을 구축할 수 있게 되었습니다.

전반적으로 조직 전체에서 마이크로서비스를 구축하는 접근 방식을 표준화하는 방향으로 나아가고 있습니다. 그러나 경쟁에서 앞서 나가면서 규모에 맞게 운영해야 하는 끊임없는 과제를 고려할 때, 진화는 계속될 것입니다.

참고 자료