이번 프로젝트에서는 Unity로 만든 3D 공간을 브라우저에서 접속할 수 있도록 구성했다.
그런데 일반적인 웹 3D 방식은 아니었다.
Unity를 사용자 PC에서 실행하는 게 아니라 서버에서 실행하고, 서버에서 렌더링한 화면을 브라우저로 보내는 방식이었다. 흔히 말하는 SSR(Server-Side Rendering) 구조인데, 여기서는 웹 페이지를 서버에서 만들어주는 형태가 아니라 Unity 렌더링 자체를 서버에서 처리한다.
처음부터 이 방식을 생각했던 건 아니다.
처음에는 그냥 웹에서 Unity 콘텐츠를 실행하면 되지 않을까 싶었다. 그런데 그렇게 가면 결국 접속하는 기기의 GPU 성능을 따라가야 한다. 그래픽 품질을 올리면 저사양 PC에서는 프레임이 떨어지고, 반대로 사양을 낮추면 보여주려던 3D 화면의 품질을 포기해야 한다.
이번 프로젝트에서는 그 부분을 타협하고 싶지 않았다.
그래서 아예 렌더링을 서버로 옮겼다.
Unity를 서버에서 실행하기
구성 자체는 생각보다 단순했다.
Unity에서 씬을 실행하고 Unity Render Streaming을 이용해서 브라우저와 연결했다. 브라우저와 Unity 사이의 연결 과정에는 Signaling 서버를 사용했고, 실제 영상 전송은 WebRTC로 처리했다.
대략적인 흐름은 다음과 같다.
Unity Server -> Render Streaming -> WebRTC -> Browser
반대로 사용자의 마우스나 키보드 입력은 브라우저에서 받아 Unity 쪽으로 넘겼다.
Browser Input -> WebRTC Data Channel -> Unity Input System
처음 화면을 브라우저에 띄우는 것까지는 크게 어렵지 않았다.
문제는 그 다음이었다.
직접 움직여보니까 마우스 입력이 바로바로 반영되지 않는 느낌이 있었다.
로컬에서 Unity를 실행할 때와 비교하면 당연한 결과였다. 브라우저에서 입력을 받고, 서버로 보내고, Unity에서 처리하고, 다시 렌더링한 다음 영상을 인코딩해서 브라우저로 보내야 한다.
대충 보면 별거 아닌 것 같은데 이게 계속 반복된다.
특히 카메라를 마우스로 돌릴 때 지연이 꽤 잘 느껴졌다.
입력 지연을 직접 맞춰보기
처음에는 네트워크 쪽을 먼저 의심했다.
패킷 처리나 전송 부분을 확인하고 입력값이 너무 늦게 들어가는 건 아닌지 봤다. 그런데 네트워크만 조정한다고 해결되는 문제가 아니었다.
구조상 입력이 한 번 서버를 거쳐야 하기 때문이다.
Browser Input -> Network -> Unity Input -> Rendering -> Encoding -> WebRTC -> Browser
여기서 어느 한 부분만 조금 느려져도 최종적으로는 사용자가 느끼게 된다.
그래서 마우스 좌표를 그대로 적용하는 것보다는 움직임을 조금 보정하는 쪽으로 수정했다. 입력 자체의 지연시간을 없애는 건 아니지만, 카메라가 따라오는 방식을 조절하니까 체감은 조금 나아졌다.
이 작업을 하면서 SSR에서는 프레임만 높다고 끝나는 게 아니라는 걸 확실히 느꼈다.
화면이 60FPS로 잘 나오더라도 입력이 늦으면 조작감은 이상하다.
결국 렌더링 성능과 별개로 입력 -> 렌더링 -> 전송까지 한 사이클을 같이 봐야 했다.
UI는 웹으로 따로 만들지 않았다
UI는 처음에 조금 고민했다.
브라우저에서 Unity 화면을 영상으로 받아서 보여주고 있으니까 버튼이나 메뉴를 HTML로 만들어야 하나 싶었다.
그런데 굳이 그럴 필요가 없었다.
Unity Render Streaming에서 브라우저의 마우스, 키보드 같은 입력을 Unity Input System으로 넘길 수 있었고 Unity UI도 같은 입력을 사용할 수 있었다.
그래서 UI를 전부 Unity Canvas로 만들었다.
EventSystem의 입력 모듈도 InputSystemUIInputModule을 사용했다.
이렇게 하니까 브라우저에서 버튼을 클릭하면 입력이 Unity까지 들어오고, Unity 쪽에서 기존 UI 이벤트처럼 처리할 수 있었다.
이 방식이 생각보다 편했다.
웹에서 UI를 따로 만들었다면 Unity의 게임 상태와 웹 UI 상태를 계속 맞춰줘야 한다. 메뉴가 열렸는지, 버튼이 활성화됐는지, 특정 UI가 표시되어야 하는지 등을 별도로 전달해야 한다.
이번에는 그런 작업을 만들지 않았다.
Unity가 게임 상태도 가지고 있고 UI도 가지고 있으니 그냥 Unity 안에서 처리하면 됐다.
프로토타입에서는 이런 차이가 꽤 컸다.
결국 서버 GPU가 문제였다
SSR을 선택하고 나니까 문제가 사라진 게 아니라 위치가 바뀌었다.
원래는 사용자의 GPU가 3D 씬을 렌더링해야 했다.
이제는 서버 GPU가 해야 한다.
그래서 서버 사양을 잡을 때 GPU를 꽤 높게 잡았다. 단순히 웹 서버를 하나 띄우는 것과는 요구사항이 달랐다.
사용자가 접속해 있는 동안 Unity가 계속 실행되고 있어야 하고, 각 세션의 화면을 계속 렌더링해야 한다. 거기에 영상 인코딩과 WebRTC 스트리밍도 같이 돌아간다.
처음부터 “이 GPU면 몇 명까지 된다”고 정확하게 계산한 건 아니다.
실제로 여러 명을 붙여봤다.
SSR 클라이언트를 하나씩 늘려가면서 GPU 사용량과 전체적인 상태를 확인했다. 그 과정에서 하나의 컴퓨팅 서비스가 대략 10~15명 정도의 SSR 클라이언트를 처리할 수 있는 수준이라는 결과를 얻었다.
이 숫자가 이후 서버 구성을 잡는 기준이 됐다.
약 100명 정도를 기준으로 잡았다
전체적으로는 약 100명 동시 접속을 목표로 잡았다.
여기서 100명이 무조건 접속 가능하다는 의미는 아니다. 실제 테스트 환경에서 컴퓨팅 서비스 하나가 10~15명 정도를 처리할 수 있었고, 그 결과를 바탕으로 여러 서버에 세션을 분산시키는 방향으로 생각했다.
SSR은 일반적인 웹 요청처럼 요청이 끝나면 서버 리소스가 바로 빠지는 구조가 아니다.
사용자가 접속하고 있는 동안 Unity 인스턴스가 계속 돌아간다.
그래서 사용자가 하나 늘어날 때마다 렌더링 환경도 하나씩 늘어난다고 생각해야 했다.
GPU만 보면 되는 것도 아니었다.
CPU 사용량이나 메모리도 같이 봐야 하고, 영상 인코딩과 네트워크 대역폭도 영향을 준다. 같은 서버라도 Unity 씬이 얼마나 무거운지, 어떤 해상도로 스트리밍하는지에 따라서 수용 가능한 세션 수가 달라질 수 있다.
그래서 10~15명이라는 숫자도 절대적인 값이라기보다는 실제 테스트에서 확인한 기준값에 가깝다.
그래도 직접 클라이언트를 붙여보고 나온 결과라는 점에서는 의미가 있었다.
처음부터 서버 몇 대가 필요한지 계산하는 것보다, 실제로 한 대에서 몇 세션까지 버티는지 확인하고 그 결과를 가지고 규모를 잡는 편이 현실적이었다.
일반적인 게임 최적화와는 조금 달랐다
게임 개발을 하다 보면 보통 클라이언트 성능을 먼저 생각하게 된다.
GPU 사용량을 줄이고, 드로우콜을 줄이고, 텍스처를 줄이고, 필요하면 그래픽 옵션을 낮춘다.
이번에는 방향이 반대였다.
사용자 PC의 성능을 최대한 덜 타게 만드는 대신 서버에서 그 비용을 부담했다.
그렇다고 해서 서버에서 아무런 비용 없이 처리할 수 있는 건 아니었다.
클라이언트에서 빠졌던 비용이 서버 GPU 비용으로 넘어갔다.
결국 성능 문제를 없앤 게 아니라 어디에서 부담할지를 바꾼 셈이다.
이번 프로젝트의 경우에는 이 선택이 맞다고 봤다.
브라우저로 접속하는 기기의 사양을 일일이 맞추는 것보다 서버에서 일정한 환경으로 Unity를 실행하는 쪽이 목표에 더 가까웠기 때문이다.
아쉬웠던 부분
프로토타입 단계라서 실제 서비스에서 필요한 것까지 전부 구현한 건 아니다.
예를 들어 접속자가 갑자기 늘어났을 때 어느 컴퓨팅 서비스에 세션을 넣을지, 사용자가 나갔을 때 해당 Unity 세션을 언제 정리할지 같은 부분은 더 다듬을 수 있다.
GPU가 부족해졌을 때 새로운 컴퓨팅 서비스를 추가하는 구조도 필요하다.
서버 하나가 장애가 났을 때 그 서버에서 실행 중이던 세션을 어떻게 처리할지도 실제 서비스에서는 중요할 것이다.
이번에는 여기까지 가지 않고, 우선 실제 환경에서 SSR 클라이언트를 여러 개 붙여보면서 어느 정도까지 수용할 수 있는지를 확인하는 데 집중했다.
그 결과 컴퓨팅 서비스 하나당 대략 10~15명 정도라는 기준을 잡을 수 있었고, 이를 바탕으로 약 100명 규모를 목표로 서버 구성을 생각할 수 있었다.
마무리
처음에는 Unity 화면을 서버에서 렌더링해서 브라우저로 보내면 끝이라고 생각했다.
실제로 해보니 그렇지는 않았다.
화면을 보내는 것 자체보다 입력 지연이 더 신경 쓰였고, 사용자가 늘어나니까 GPU 사용량도 바로 문제가 됐다. UI 역시 웹으로 빼는 게 꼭 좋은 방법은 아니었고, Unity의 입력 시스템을 그대로 사용하는 편이 더 단순했다.
결국 이번 프로젝트에서 가장 크게 바뀐 생각은 이것이었다.
SSR은 클라이언트 성능 문제를 해결해주는 기술이라기보다, 그 부담을 서버로 옮기는 방법에 가깝다.
그래서 서버 비용과 네트워크 지연이라는 다른 문제가 생긴다.
그래도 프로젝트의 목적이 저사양 클라이언트에서도 일정한 수준의 Unity 3D 콘텐츠를 보여주는 것이었다면 충분히 선택할 만한 구조였다.
실제로 여러 클라이언트를 붙여보면서 컴퓨팅 서비스당 10~15명 정도의 기준을 확인했고, 약 100명 규모를 기준으로 인프라를 구성하는 방향까지 잡을 수 있었다.
개발하면서 처음 생각했던 것과 마지막에 보고 있던 문제가 달라졌다는 점도 꽤 기억에 남는다.
처음에는 “브라우저에서 고사양 Unity를 돌리는 방법”이라고 생각했는데, 끝날 때쯤에는 “Unity 렌더링 세션을 서버에서 얼마나 효율적으로 운영할 것인가”에 더 가까운 문제가 되어 있었다.