기술

MMORPG를 JVM 기반 환경에서 운영한다고??

AAnonymous
4분 읽기

들어가며

요즘 많이 나태해졌습니다. 다시 정신을 단디잡을 필요가 있습니다. ㅠㅠ 꽤 오랜만에 블로그 글을 씁니다.

지난 글인 게임 제작 계획에서 MMORPG를 마지막 도전으로 두겠다고 썼습니다. 이에 대해서 예상되는 반응은 아마 이쪽일 것입니다. 자바? JVM으로 실시간 MMORPG를 구현하고 운영한다고? 아주 소규모에서나 가능한 일 아닌가?

이번 글에서 제가 말하고 싶은 핵심은 하나입니다. 요즘 JVM으로 높은 Hz의 서버 틱이 현실적인 선택지가 된 가장 큰 이유는 GC의 발전입니다. Netty도 중요하고 Kotlin도 중요하고, 존 분리나 AOI 같은 MMO 설계도 중요합니다. 하지만 그 모든 이야기를 하기 전에 먼저 봐야 하는 것은 pause time입니다.

이 장르에서 진짜 질문은 평균 성능이 아니라 GC입니다

예전 자바를 기억하는 사람이라면, 실시간 네트워크 게임에서 가장 먼저 떠오르는 걱정은 GC였을 겁니다. 정확히는 처리량이 아니라 tail latency였습니다. 평균적으로 빨라도 한 번의 긴 stop-the-world가 들어오면 그 틱은 바로 밀리고, 입력 반응성과 전투 감각도 함께 무너집니다.

이건 게임 서버에서 특히 치명적입니다.

  • 20Hz 서버는 틱 예산이 50ms입니다.
  • 60Hz 서버는 틱 예산이 16.6ms입니다.
  • 128Hz 서버는 틱 예산이 7.8ms입니다.

여기서 GC pause가 수십 밀리초 단위로 튀면 평균 TPS가 아무리 멀쩡해도 플레이어는 바로 이상함을 느낍니다. rubber banding, 입력 지연, 전투 판정 어긋남, 패킷 flush 지연은 대체로 이런 tail 문제에서 시작합니다. 그래서 JVM으로 실시간 게임이 되느냐는 질문은 사실 JVM 평균 성능이 충분한가보다 GC가 얼마나 예측 가능하게 짧아졌는가에 더 가깝습니다.

최신 JVM GC는 예전 JVM이 아닙니다

바로 여기서 상황이 많이 달라졌습니다. OpenJDK 흐름만 봐도 변화가 분명합니다.

  • ZGC는 JDK 15에서 production 단계로 올라왔습니다.
  • Generational ZGC는 JDK 21에 들어왔습니다.
  • JDK 23에서는 Generational ZGC가 기본 모드가 됐습니다.
  • JDK 24에서는 비세대 ZGC가 제거됐습니다.
  • 따라서 Java 25는 generational-only ZGC 시대의 첫 LTS라고 볼 수 있습니다.
  • Shenandoah도 이미 production GC였고, Java 25에서는 generational mode가 product feature로 승격됐습니다.

이 흐름이 중요한 이유는 단순히 새로운 옵션이 하나 늘었기 때문이 아닙니다. OpenJDK 문서 기준으로 Generational ZGC는 young object를 더 자주 회수하면서 allocation stall 위험, 필요한 heap 오버헤드, GC CPU 오버헤드를 함께 낮추는 방향으로 설계됐습니다. 그리고 핵심 pause 특성은 그대로 유지됩니다. OpenJDK JEP 439는 application pause가 보통 1ms 미만을 목표로 유지된다고 설명합니다.

이 말은 곧, 과거에 자바 게임 서버를 가로막던 가장 큰 정서적 장벽이었던 GC가 한 번씩 세상을 멈춘다는 인식이 더 이상 기본 전제가 아니라는 뜻입니다. 물론 GC가 사라진 것은 아닙니다. 하지만 이제는 JVM을 논외로 치게 만드는 결정적인 이유가 GC 자체는 아니게 됐습니다.

실제 운영 사례도 이 방향을 뒷받침합니다

가장 흥미로운 사례 중 하나는 Netflix입니다. Netflix는 JDK 21 이상의 환경에서 Generational ZGC를 기본 GC로 전환했고, 자사 설명에 따르면 현재는 핵심 스트리밍 비디오 서비스의 절반 이상이 JDK 21 위에서 Generational ZGC로 운영되고 있습니다.

더 중요한 것은 결과입니다. Netflix는 처음에는 ZGC가 약간의 처리량 손해를 감수하고 latency를 사는 선택이 될 것으로 예상했다고 설명합니다. 그런데 실제 결과는 더 좋았습니다. 평균 레이턴시와 P99 레이턴시가 모두 개선됐고, CPU 사용률도 동등하거나 더 나은 결과가 나왔습니다. 초기의 비세대 ZGC에서는 최악의 경우 G1보다 CPU가 36% 더 들던 케이스가 있었지만, Generational ZGC에서는 같은 워크로드에서 오히려 거의 10% 가까운 CPU 개선도 관측했다고 밝히고 있습니다.

물론 Netflix는 게임 회사가 아닙니다. 하지만 우리가 여기서 봐야 하는 것은 서비스 종류가 아니라 pause와 tail latency를 얼마나 안정적으로 다루는가입니다. 실시간 게임 서버도 결국 같은 문제를 더 빡빡한 예산 안에서 다루는 시스템이기 때문입니다.

그래서 JVM으로 실시간 게임을 만들 수 있느냐는 질문의 답도 달라졌습니다

이제는 JVM으로 실시간 게임이 가능한가를 너무 뭉뚱그려서 말하면 안 됩니다. 영역을 나눠서 봐야 합니다.

충분히 현실적인 영역은 이미 꽤 넓습니다.

  • 틱 기반 MMORPG
  • 샌드박스형 월드 서버
  • 로비, 매치메이킹, 상태 동기화 서버
  • 20~60Hz 정도의 서버 틱을 요구하는 멀티플레이어 게임

대표적인 대중 사례로는 Minecraft Java Edition 서버가 있습니다. 이 사실 하나만으로도 Java는 실시간 멀티플레이어 월드를 못 굴린다는 식의 단정은 더 이상 유지하기 어렵습니다.

반대로 여전히 더 조심해야 하는 영역도 있습니다.

  • 128Hz 경쟁형 FPS처럼 틱 예산이 극단적으로 빡빡한 장르
  • 극단적인 deterministic latency가 필요한 서버
  • 메모리 여유가 거의 없는 환경
  • GC 스레드와 애플리케이션 스레드가 경쟁할 정도로 CPU가 이미 꽉 찬 환경

즉, 요지는 JVM이면 다 된다가 아닙니다. 다만 대부분의 게임 서버 시나리오에서 JVM을 탈락시키던 가장 큰 이유였던 GC pause 문제는 이제 많이 해소됐다는 쪽에 가깝습니다.

그리고 그다음에야 MMO 특유의 설계가 의미를 갖습니다

여기서 순서를 분명히 해야 합니다. 저는 zone tick, 시스템 cadence, AOI 기반 동기화가 중요하다고 계속 말해왔습니다. 지금 구현 중인 mmorpg-service도 실제로 그 구조를 따릅니다.

  • 기본 루프는 10Hz
  • 월드/존별 설정은 1~15Hz 범위에서 조정 가능
  • 몬스터 AI는 상태에 따라 cadence를 달리 적용
  • 드롭 디스폰 체크와 자동 회복 재시도도 더 느린 간격으로 처리
  • Netty I/O 쓰레드는 패킷만 받고, 실제 게임 규칙은 존별 루프가 처리
  • AOIManager가 거리와 이벤트에 따라 동기화 빈도를 다르게 조절

하지만 이건 어디까지나 GC가 이미 한 단계 해결된 뒤의 설계입니다. pause time이 여전히 수십 밀리초씩 튀는 환경이라면, 그 위에서 zone tick을 나누고 AOI를 정교하게 짜도 결국 바닥이 흔들립니다. 높은 Hz를 감당할 수 있게 된 가장 큰 이유는 GC 발전이고, MMO 설계는 그 위에 쌓이는 두 번째 레이어입니다.

결론적으로, 이 글의 핵심은 이 문장입니다

요즘 JVM에서 높은 Hz 처리가 가능해진 가장 큰 배경은 GC의 발전입니다.

과거에는 자바 = stop-the-world가 무섭다는 인식이 너무 강했습니다. 지금은 low-pause GC가 훨씬 성숙해졌고, Generational ZGC와 최신 Shenandoah 흐름까지 포함하면 JVM은 더 이상 실시간 서버 논의에서 자연스럽게 제외되는 선택지가 아닙니다.

그 위에 Kotlin과 Netty를 올리고, zone loop와 cadence, AOI 같은 MMO 설계를 얹으면 이야기는 완전히 달라집니다. 이제 질문은 JVM이라서 안 되지 않나?가 아니라 우리 게임의 latency budget 안에서 GC와 tick을 어떻게 설계할 것인가?가 되어야 합니다.

저는 바로 그 지점에서 JVM 기반 MMORPG 운영이 충분히 현실적인 선택지가 되었다고 봅니다.