지배천체
삼체 문제는 특수해만 구할 수 있을 뿐이지만 제가 만든 우주에서는 별의 움직임을 끝까지 예측할 수 있습니다. 왜소행성과 우주선의 질량을 0으로 두고 지배천체를 정하는 단순화 덕분입니다. 그 단순화가 우주를 저장 대신 계산으로 재현하게 했고 결국 게임 하나를 혼자 서비스하게 된 이야기를 정리해봤습니다.
삼체인들은 사실 별의 움직임을 예측할 수 있었습니다. 류츠신의 소설 '삼체'에서 삼체 문명이 겪는 고통은 세 개의 태양이 언제 온화한 안정기를 내주고 언제 문명을 통째로 불태우는 혼돈기로 돌변할지 도무지 알 수 없다는 데서 옵니다[^Liu Cixin]. 그 예측 불가능은 작가가 지어낸 설정이 아니라 실제 물리에서 나옵니다. 세 개 이상의 중력체가 서로를 끌어당기는 다체 문제에는 일반해가 없습니다. 앙리 푸앵카레가 19세기 말에 이 문제가 적분 불가능함을 보인 뒤로 인류가 얻은 것은 오일러의 일직선 배치나 라그랑주의 정삼각형 배치 같은 몇 개의 특수해와 초기 조건이 아주 조금만 어긋나도 궤적이 전혀 다른 곳으로 갈라지는 혼돈뿐입니다[^Three-body problem]. 삼체인이 하늘을 읽지 못한 것은 그들이 무능해서가 아니라 우주가 원래 그렇게 생겼기 때문입니다. 세 개의 태양이 뜬 하늘은 원리적으로 예언할 수 없습니다.
그런데 지난 '관측 가능한 우주'에서 만든 우주에서는 삼체인이라 해도 별의 움직임을 완벽히 내다볼 수 있습니다. 그 게임의 삼체 항성계에 접속하면 세 별과 그 행성들이 지금부터 며칠 뒤 어디에 있을지가 처음부터 정해져 있어서 접속을 끊고 3일 뒤에 다시 들어와도 하늘은 계산해 둔 그대로입니다. 소설 속 삼체인이 그토록 원했던 안정된 달력이 이 우주에는 이미 갖춰져 있습니다. 이유는 단순합니다. 제가 다체 문제를 풀지 않고 피해 갔기 때문입니다. 우주를 실제 물리대로 굴리겠다고 마음먹었지만 곧이곧대로 다체 문제를 적분하는 순간 게임은 성립할 수 없었습니다. 그래서 두 가지 단순화를 넣었습니다. 첫째로 왜소행성과 소행성, 그리고 플레이어의 우주선까지 모두 질량을 0으로 뒀습니다. 이들은 다른 천체를 끌어당기지 않고 오직 끌려다니기만 하는 시험입자입니다. 둘째로 지배천체라는 개념을 넣었습니다. 우주선은 자기를 감싼 가장 국소적인 중력권의 천체 하나를 기준으로 삼고 그 바깥은 태양과 큰 행성 열 개 남짓의 중력 합만 받습니다. 천체들끼리는 서로를 아예 느끼지 않고 각자 케플러 타원 위를 홀로 돕니다. 다체 문제가 제한된 문제로 단순해지고 그마저도 해석적으로 풀립니다[^n-body problem].
이렇게 하면 천체의 위치는 더 이상 시뮬레이션할 대상이 아니라 계산할 대상이 됩니다. 실제 행성의 궤도요소를 천문학자 스탠디시가 정리한 표에서 가져와[^Approximate Positions of the Planets] 평균근점이각을 구하고 닫힌 해가 없는 케플러 방정식을 뉴턴법으로 여덟 번만 반복해 풀면 임의 시각의 위치가 나옵니다. 서버도 클라이언트도 서울에서 접속한 사람도 다른 대륙에서 접속한 사람도 같은 시각을 넣으면 같은 위치를 얻습니다. 그래서 이 우주는 예측 가능합니다. 삼체인의 비극이 다체 문제의 혼돈에서 왔다면 이 게임의 평온은 그 문제를 질량 0과 지배천체로 단순화한데서 옵니다. 지배천체를 정한다는 건 결국 '지금 무엇이 중요하고 나머지는 무시해도 되는가'를 정하는 일입니다. 이 결정이 게임 안의 물리에서 끝나지 않고 게임을 만들고 운영하는 내내 똑같은 모양으로 반복되었다는 이야기를 하려고 합니다.

우주를 저장하지 않고 재현한다는 원칙도 같은 단순화의 연장입니다. 우주를 통째로 저장하는 건 물리적으로 불가능합니다. 별만 해도 수천억 곱하기 수천억 개라 어떤 스토리지에도 저장할 수 없습니다. 그래서 실측 데이터가 있으면 데이터를 쓰고 없으면 결정론적 절차 생성으로 만든다는 규칙을 세웠습니다. 밝은 별과 행성 궤도, 유명한 외계행성과 관측된 블랙홀은 실제 카탈로그에서 가져오고 먼 별 하나하나와 은하 대부분과 오르트 구름 같은 것들은 같은 시드에서 매번 똑같이 다시 만들어냅니다. 시드 하나만 공유하면 서버와 클라이언트가 각자 계산해도 정확히 같은 우주가 나오므로 무거운 우주를 주고받을 필요 없이 오가는 것은 시드와 우주선 몇 개의 상태뿐입니다. 압축이 아니라 재현입니다. 이 대목에서 사람들이 가장 자주 던지는 질문은 '그럼 그 많은 그래픽 에셋은 다 어떻게 만들었느냐'입니다. 답은 '거의 만들지 않았다'입니다. 행성 표면도, 성운도, 은하도, 우주선도 전부 기본 도형과 노이즈 함수와 셰이더로 그 자리에서 계산합니다. 실제로 게임 안에 그려 넣은 아트 에셋이라 부를 만한 건 6킬로바이트 남짓한 지구 해안선 비트맵 하나뿐입니다[^Natural Earth]. 재미있는 사실을 하나 덧붙이면 이 게임 전체에서 가장 용량이 큰 단일 파일은 우주와 아무 상관이 없습니다. 콕핏 안에 무중력으로 띄워 둔 스팀덱에서 돌아가는 둠의 엔진 바이너리가 4.5메가바이트로 관측 가능한 우주 전체를 그리는 코드보다도 그 우주의 유일한 아트 자산인 7킬로바이트짜리 해안선보다도 훨씬 큽니다. 둠은 엔진 소스만 공개되어 있고 레벨과 그래픽 데이터는 여전히 저작권이 살아 있어서 자유 배포가 허용된 부분만 동봉했습니다[^Doom engine][^Freedoom]. 그러고 나니 제가 만든 우주보다 30년 된 남의 게임 엔진이 더 무거운 저장소가 됐습니다.
그런데 지난 글 끝에 이미 적어 둔 것처럼 코드를 짜서 이 우주를 돌아가게 하는 건 알고 보니 절반의 일이었습니다. 이걸 인터넷에 띄워 여러 사람이 함께 들어오는 라이브 서비스로 유지하는 나머지 절반은 전혀 다른 종류의 다체 문제였습니다. 게임 서버가 있고, 그 앞에 소개 웹사이트가 있고, 사용법을 적은 가이드가 있고, 소식을 알리는 페디버스 계정이 있고, 누가 어떻게 노는지 보는 접속 통계가 있고, 성능과 보안을 검사해 막는 일이 있고, 무엇 하나 깨지지 않았는지 확인하는 테스트가 있습니다. 이것들이 전부 서로를 끌어당깁니다. 그래픽을 한 번 손보면 웹사이트의 스크린샷이 죄다 옛것이 되고, 앱에 기능이 하나 늘면 가이드가 거짓말을 하기 시작하고, 물리 한 줄을 고치면 며칠짜리 무인 항해의 궤적이 어긋납니다. 혼자서 이 모든 인력을 다 계산하려 들면 삼체인의 하늘처럼 손을 쓸 수 없어집니다. 그래서 저는 게임 바깥에서도 정확히 같은 결정을 내려야 했습니다. 무엇을 질량 0으로 둘 것인가, 무엇을 지배천체로 삼을 것인가 하는 것들입니다.
운영에서 제가 발견한 가장 큰 지배천체는 '앱과 웹사이트는 하나의 제품'이라는 사실이었습니다. 처음에는 게임과 소개 사이트를 별개의 두 물건으로 여겼지만 그렇게 두면 둘이 끊임없이 서로를 어긋나게 만듭니다. 그래서 아예 하나로 묶었습니다. 게임 서버와 클라이언트, 웹사이트, 가이드, 그리고 뒤에 이야기할 리플레이까지 전부 한 저장소에 두고 배포 명령 한 번이 이들을 도커 이미지 하나로 구워 통째로 올립니다. 장점은 분명합니다. 콕핏을 여섯 개 모니터로 바꾸면 게임과 가이드 설명이 같은 변경과 같은 배포로 함께 나가기 때문에 문서가 게임과 어긋나지 않습니다. 단점도 분명히 있습니다. 가이드의 오타 한 글자를 고쳐도 클라이언트가 이미지에 통째로 들어가는 구조라 전체 이미지를 다시 빌드해 재배포해야 합니다. 문서 한 줄과 물리 시뮬레이션 서버가 같은 배포 수명주기를 공유하는 대가입니다. 혼자 운영하는 이 규모의 단일 서버에서는 일관성이 주는 이득이 리빌드 비용을 넘어선다고 판단했습니다. 그래서 그래픽을 손볼 때마다 사이트와 가이드의 스크린샷을 전부 다시 찍고, 클라이언트를 고칠 때마다 캐시버스터 번호를 하나씩 올리고, 배포가 끝날 때마다 라이브 사이트의 모든 링크가 실제로 살아 있는지를 자동으로 검사합니다. 이 링크 검사기는 페이지를 진짜 브라우저로 렌더해서 링크가 가리키는 앵커가 정말 그 자리에 있는지까지 확인합니다. 한번은 갤러리 렌더가 바깥의 변수를 조용히 덮어써서 링크가 죽은 경로로 향한 적이 있었고 정적 검사만이었다면 결코 못 잡았을 그 버그를 이 실측 클릭 검사가 잡아냈습니다. 앱과 사이트를 하나의 제품으로 취급한다는 건 이렇게 배포마다 둘의 정합성을 한 관문으로 지킨다는 뜻이었습니다.
지배천체를 정했으면 그다음은 나머지의 질량을 0으로 만드는 일입니다. 라이브 서비스에는 게임과 무관한 살림이 끝없이 따라붙습니다. 이것들을 하나하나 제 손으로 감당하면 다체 문제로 돌아갑니다. 그래서 반복되는 것은 전부 기계에게 넘겨 제 손에서 질량을 없앴습니다. 데이터베이스는 하루 두 번 스스로 일관된 스냅샷을 떠서 버전 관리에 올립니다. 서버가 한창 쓰고 있는 파일을 안전하게 복사하려고 SQLite가 제공하는 무결한 스냅샷 기능을 쓰고[^VACUUM] 직전 백업과 내용이 같으면 아무 리비전도 남기지 않고 조용히 넘어갑니다. 소식을 알리는 일도 사람이 매번 하지 않습니다. 게임 안의 사건과 그날 방문된 천체 사진 한 장을 자동으로 골라 페디버스 계정에 올리는 봇을 만들어[^ActivityPub] 운영체제의 스케줄러에 걸어 뒀습니다. 자유 텍스트는 한 글자도 싣지 않고 미리 정해 둔 닫힌 템플릿만 쓰고 한국어 조사가 어색해지지 않게 받침 유무를 따져 '이'와 '가'를 골라 붙이는 정도의 잔손질을 넣었습니다. 이 봇은 시뮬레이션과 완전히 격리되어 있어서 게시가 실패해도 게임의 틱에 영향을 주지 않습니다. 이런 자동화가 늘 안전하기만 했던 건 아닙니다. 용량 측정 스크립트에서 임시 파일 이름이 빈 문자열로 넘어오는 바람에 삭제 명령이 엉뚱하게 번져 측정 도중 코드 저장소의 설정 파일이 지워진 적도 있었습니다. 무인 자동화는 손이 덜 가는 만큼 한 번 어긋나면 조용히 크게 어긋납니다. 그래도 원칙은 바뀌지 않았습니다. 반복되는 살림은 지배천체가 아니므로 질량을 0으로 두고 기계에 맡기되 그 기계가 폭주하지 않도록 가드만 단단히 채워 둔다는 것입니다.
게임을 서비스하다 보면 사용자들이 자연스럽게 품질 확인을 요구합니다. 이것도 다체 문제처럼 다뤘습니다. 어느 날 '대기가 있는 행성이든 없는 행성이든, 큰 천체든 아주 작은 소천체든, 정말 다 착륙이 되느냐'는 질문을 받았습니다. 이런 확인은 이미 알려진 버그가 다시 나지 않게 막는 회귀 테스트로는 부족합니다. 모르는 버그를 잡으려면 카테고리 전체를 빠짐없이 훑어야 합니다. 그래서 반지름 6킬로미터짜리 소천체부터 지구만 한 행성까지 열네 개의 대표 천체에 실제로 강하를 시켜 봤더니 화성의 위성 포보스와 데이모스처럼 아주 작은 천체에서만 우주선이 초속 몇 킬로미터로 표면에 처박히는 버그가 드러났습니다. 대기가 있는 천체와 없는 천체는 물리적으로 다르게 접근해야 합니다. 무대기 천체는 표면에 닿는 속도가 초속 0.2킬로미터 이하라야 착륙이고 그 위는 충돌입니다. 대기가 있는 천체는 진입각이 6도보다 가파르면 타 버리고 얕으면 대기를 스치고 튕겨 나갑니다[^Atmospheric entry]. 이걸 태양계 안의 천체뿐 아니라 프록시마 센타우리 같은 실제 외계행성계의 행성에까지 자동으로 확인하도록 테스트를 넓혔습니다. 여기서도 지배천체의 논리가 작동합니다. 이 자동 착륙 검증이 지키려는 진짜 기준점은 무대기 연착륙의 결과를 해시로 얼려 둔 골든 테스트지만 오토파일럿 착륙 로직 자체는 골든에 해시하지 않았습니다. 그래서 착륙 알고리즘을 마음껏 고치면서도 그 결과가 골든을 깨지 않으면 거동이 보존됐다는 뜻이고 깨지면 의도치 않은 변경이라는 신호가 됩니다.

그런데 접근할 수 있는 우주가 넓어지고 기능이 서로 얽히면서 대표 천체 몇 개만 골라 보는 것으로는 더 이상 안심할 수 없어졌습니다. 표본은 언제나 자기가 들여다보지 않은 곳에서 깨지기 때문입니다. 그래서 저는 최근 며칠 사이에 품질 검사에서만은 지배천체를 고르는 일을 포기하고 무식하게 카탈로그 전체를 매 배포마다 전수로 훑기로 했습니다. 태양계 천체부터 실제 탐사선과 혜성, 먼 표적과 성간 차트 88개, 다른 항성계의 행성 389개까지 게임이 목적지로 삼을 수 있는 모든 천체를 각 목적지에 걸 수 있는 모든 오토파일럿 행동과 곱해 2385가지를 남김없이 돌립니다. 여기서 잡으려는 것은 요란한 오류가 아니라 조용한 무반응입니다. 화면의 버튼은 멀쩡히 눌리지만 서버는 그 목적지에 그 기동이 성립하지 않는다며 아무 말 없이 명령을 무시합니다. 사용자만 겪고 저는 모르는 그 어긋남을 배포 전에 드러냅니다. 성간 목적지 139개가 목적지 선택에서 항로 계획과 순항을 지나 도착까지 조용한 막다른 길 없이 이어지는지, 점화와 정렬과 궤도 진입과 착륙 방어와 블랙홀 실비행이 미리 선언해 둔 기대값과 어긋나지 않는지도 같은 관문에서 전수로 확인합니다. 마지막 한 겹은 퍼저입니다. 시드를 고정한 난수로 서른네 가지 명령을 정상값부터 쓰레기값까지 섞고 시간마저 마구 감아 함선을 900번 두들겨 그때마다 모든 함선의 상태 스냅샷이 한 번도 예외를 던지지 않는지 봅니다. 이 퍼저가 실제로 두 개의 크래시를 배포 전에 잡아냈습니다. 하나는 거대 블랙홀의 사건의 지평선 안쪽에 우주선을 주차시키던 것이었고, 다른 하나는 국제우주정거장의 궤도 계산이 극단적인 시각에서 실패해 연쇄로 무너지던 것이었습니다. 이 전수 검사들은 골든과 나란히 매 배포마다 돌고 그중 하나라도 어긋나면 이미지를 굽기도 전에 배포가 멈춥니다. 게임의 물리에서는 지배천체 하나만 남기고 나머지 질량을 0으로 접었지만 품질 검사에서만은 정반대였습니다. 어느 천체가 안 깨질지, 어느 기능이 안 무너질지를 미리 알 수 없으니 무엇도 질량 0으로 두지 않고 전부를 셈했습니다.
방금 골든을 말했습니다. 이것이야말로 이번에 가장 크게 배운 지배천체입니다. 여러 업데이트를 하루에 100건 넘게 올리면서도 4일 전 맞춰 둔 물리를 깨지 않으려면 기준이 필요합니다. 골든 테스트는 대표적인 항해 시나리오 몇 개를 똑같이 돌린 다음 그 결과 전체를 해시해 둡니다. 그러고 나면 모든 변경이 두 종류로 갈립니다. 그래픽 손질이나 스크린샷 재촬영, 가이드 수정, 픽션 우주선 외형, 리플레이, 접속 통계처럼 시뮬레이션의 핵심 물리 바깥에 있는 변경은 이 지문을 건드리지 않아야 정상이고 그래서 라이브 위험이 낮아 마음 편히 밀어붙일 수 있습니다. 반대로 궤도나 적분기나 지배천체 판정처럼 물리를 건드리는 변경은 해시를 바꿉니다. 이때 해시가 바뀌면 게임 전체의 거동이 바뀐다는 뜻입니다. 무엇을 지배천체로 삼아 지문에 넣고 무엇을 질량 0으로 두어 뺄지를 정확히 아는 것이 며칠 전의 물리를 깨지 않는 안전벨트였습니다. 우주론 상수를 최신 관측값으로 정밀화하는 작업조차 그 값이 우주선의 상태에 실리지 않고 화면 라벨만 구동한다면 골든에는 아무 값도 치르지 않고 지나갑니다[^Planck 2018 results. VI. Cosmological parameters].
이 기간에 그래픽을 강화하면서는 지난 글에서 예고했던 대로 문제에 하나씩 부딪혀 파이프라인을 배웠습니다. 이번의 구체적인 문제는 '어두운 우주에서 작은 우주선이 안 보인다'는 것이었습니다. 수 미터에서 수십 미터짜리 우주선을 행성 스케일의 화면에 정직한 크기로 그리면 화면의 한 점보다 작아져 사라집니다. 처음엔 그냥 실제보다 수천 배 크게 그려 놓고 멀리 있는 다른 우주선에는 옅은 후광 스프라이트를 입혀 또렷한 점으로 읽히게 했습니다. 그런데 멀리 위치한 탐사선이 여전히 거의 새까만 실루엣으로 나오는 걸 보고 우주선을 비추는 보조광이 거리에 따라 세기가 붕괴하고 있다는 걸 알게 됐습니다. 그래서 거리와 무관하게 일정한 밝기를 내는 조명을 전용 레이어에 더해 우주선만 밝히고 근처 행성에는 새지 않게 했습니다. 이렇게 가시성 하나를 붙들고 늘어지다 보니 자연스럽게 후처리 파이프라인 전체를 통과하게 됐습니다. 밝은 곳이 부드럽게 번지는 블룸의 문턱값을 콕핏 계기판의 글자는 안 번지고 별과 강착원반만 번지도록 잡는 법을 배웠고[^Next Generation Post Processing in Call of Duty: Advanced Warfare], 눈이 멀 듯 밝은 것과 칠흑같이 어두운 것을 한 화면에 담는 톤매핑을 영화용 방식에서 가져다 맨 끝에 거는 이유를 배웠고[^ACES Filmic Tone Mapping Curve], 밝은 별에 십자 광선을 다는 회절 효과가 왜 실제 망원경 사진에 늘 나타나는지를 배웠습니다[^Diffraction spike]. 그러면서 몇 번이고 확인한 건 사실적으로 만든다고 다 좋은 게 아니라는 점이었습니다. 필름 그레인도 렌즈 글레어도 사용자 반응을 보면서 계속 덜어냈고 스스로 그 보정을 '예쁜 거짓말'이라고 주석에 적어 두었습니다.

이번에 새로 붙인 것 중 저 개인적으로 가장 좋아하는 기능은 리플레이입니다. 널리 알려진 실제 우주선의 발사부터 착륙까지 전 과정을 라이브 플레이와는 별개로 게임과 거의 같은 우주 표현으로 감상하는 시네마틱입니다. 팰컨 9의 첫 지상 착륙, 팰컨 헤비의 사이드 부스터 동시 착륙, 발사탑이 부스터를 팔로 낚아채는 스타십, 그리고 새턴 V가 다단으로 분리되어 4일을 날아 고요의 바다에 내리는 아폴로 11호와 앨런 셰퍼드의 준궤도 비행 프리덤 7까지 다섯 임무를 넣었습니다. 여기서 핵심은 물리를 다시 적분하지 않는다는 것입니다. 공개된 텔레메트리를 결정론적 키프레임으로 재생하되 지구와 달을 그리는 표현과 좌표 관례는 게임의 것을 그대로 물려받습니다. 특히 아폴로의 지구에서 달로 넘어가는 4일의 순항 구간은 생략하지 않고 게임의 바디 상대 렌더러를 그대로 써서 지구가 멀어지고 달이 다가오는 실제 세그먼트로 그렸습니다. 이 리플레이를 다듬는 과정이 그래픽 파이프라인 학습의 정점이었습니다. 유닛 스케일의 세계에 카메라를 극단적으로 붙이자 실제 GPU의 부동소수 정밀도가 무너져 로켓이 덜덜 떨리고 화면이 번쩍였습니다. 매 프레임 장면 전체를 피사체가 원점에 오도록 옮기는 부동 원점으로 해결했습니다. 로그 깊이 버퍼는 오히려 역효과였습니다. 카메라의 기준 축을 속도에서 뽑으면 수직 발사 초기에 축이 퇴화해 브라우저마다 이상한 각도가 나와서 발사장의 궤도평면 법선처럼 언제나 잘 정의되는 기하학에서 축을 뽑도록 바꿨습니다. 무엇보다 이런 지터와 번쩍임은 헤드리스 캡처 봇의 소프트웨어 래스터라이저에서는 깨끗하게 감춰져서 실기기를 쓰는 사용자의 증언만이 유일한 오라클이었습니다. 자동 검증이 만능이 아니라는 걸 인정하게 된 대목입니다.

사람들이 실제로 어떻게 플레이하는지도 알고 싶었습니다. 그런데 이 게임은 화면 주소가 바뀌지 않는 단일 페이지라 방문 통계 도구는 모두가 그저 한 페이지에 머문 것으로만 봅니다. 그래서 인게임 행동과 우주선의 사건을 직접 계측해 자체 호스팅한 통계 서버로 보냈습니다[^Matomo]. 어느 콕핏 모니터의 어느 버튼을 눌렀는지, 어느 천체 궤도에 진입하고 어느 천체에 착륙했는지, 어느 블랙홀의 사건의 지평선을 넘었는지가 가상의 페이지 주소와 이벤트로 남습니다. 개인을 식별하는 정보는 한 글자도 보내지 않고 계측이 막혀도 게임 동작에는 아무 영향이 없게 했습니다. 이것도 게임 자체는 아니지만 이게 없으면 사람들이 콕핏의 어느 화면을 실제로 쓰는지, 어디에서 우주선을 잃는지를 알 길이 없습니다. 게임 아닌 일이 대부분이라는 말은 이런 뜻입니다.
공유된 세계를 인터넷에 열어 두면 보안은 선택이 아닙니다. 여기서 지배천체는 명확합니다. 서버가 유일한 권위이고 클라이언트가 보내는 것의 질량은 0입니다. 웹브라우저에서 도는 클라이언트는 누구나 열어 볼 수 있으니 그게 보내는 값을 단 하나도 믿지 않습니다. 모든 판단은 서버에 있고 클라이언트가 보내는 명령은 전부 '이런 걸 하고 싶다'는 요청일 뿐이라 미리 정해 둔 목록에 있는 명령인지, 숫자 범위가 정상인지, 정말 그 천체에 갈 자격이 있는지를 서버가 일일이 다시 검사한 다음에야 받아들입니다. 심지어 '이 천체에 착륙했다'거나 '여기에 다녀갔다'는 것조차 클라이언트가 주장할 수 없고 서버가 거리와 상대속도와 궤도권을 재검증한 뒤에만 인정합니다. 봇이 페이지도 열지 않고 소켓만 두드리는 걸 막기 위해 서버에서 서명된 토큰을 발급하고 그 토큰을 위조할 수 없게 열쇠 있는 해시로 묶었고[^RFC 2104: HMAC: Keyed-Hashing for Message Authentication] 그 앞에 사람인지 확인하는 절차를 추가했습니다[^Cloudflare Turnstile]. 이 둘은 서로를 대체하지 않고 곱셈으로 봇의 비용을 올립니다. 우주선의 소유권은 남에게 노출되는 이름이 아니라 비밀 토큰에 묶고 그 토큰마저 서버에는 해시로만 저장해 평문은 디스크에 남기지 않고 그 위에 개인정보 없는 복구 코드와 패스키를 얹었습니다[^Web Authentication: An API for accessing Public Key Credentials - Level 2]. 완벽한 차단은 공개 웹에서 불가능하므로 모든 관문의 목적은 차단이 아니라 비용 상승이고 그래서 곱셈으로 놔둡니다.
성능도 결국 지배천체를 정하는 문제였습니다. 홈랩의 맥미니 한 대에서 이 게임의 권위 시뮬레이션은 단일 스레드로 돕니다. 이 한 스레드로 활발하게 자유비행하는 우주선을 400대쯤은 실시간으로 감당하고 대략 600대 언저리가 한계지만 목표는 만 대였습니다. 24시간 도는 운영 장비를 건드리지 않으려고 부하 없는 별도 머신에서 여러 방법을 벤치마크했습니다. 처음 기대를 건 건 한 지점에 몰린 우주선 무리의 적분을 여러 코어로 쪼개는 방식이었습니다[^Worker threads]. 실측해 보니 오히려 손해였습니다. 우주선은 질량 0인 시험입자라 적분 자체가 워낙 싸서 여러 워커에 일을 흩는 부대비용이 이득을 삼켜 버렸습니다. 직관적으로 병렬화가 이득일 것 같아도 재 보면 아니더라는 이번에 가장 의미있는 확인이었습니다. 실제로 만 대까지 실시간을 지킨 유일한 방법은 우주선을 여러 항성계로 나눠 각각 다른 코어에서 처리하는 샤딩이었습니다. 여기서 결정적 단순화는 항성계를 1차 분할 기준으로 삼은 것입니다. 광속의 벽 덕분에 다른 항성계는 서로 광년 단위로 격리되어 있으니 같은 항성계 안의 상호작용만 한 코어에 모으면 코어 사이에 오갈 일이 거의 없습니다. 같은 2000대를 한 지점에 몰아넣으면 틱이 무너지지만 여덟 개 항성계로 흩으면 실시간으로 돌아오고 그대로 만 대까지 버팁니다. 무엇을 한 덩어리로 묶고 무엇을 남으로 둘지 다시 지배천체의 문제였습니다.
이 모든 단순화를 관통하는 원칙이 하나 있습니다. 경험을 실제로 만들어내는 것은 절대 질량 0으로 두지 않는다는 것입니다. 추력은 실제의 100배 넘게 과장했고, 연료는 아예 없앴고, 항성계 사이를 건너뛰는 편도 웜홀도 허락했습니다. 이것들은 다 기다림의 비용일 뿐이라 마음껏 깎았습니다. 그러나 광속만은 절대 넘지 않습니다. 광속을 넘는 순간 이 게임이 주려는 경험을 만드는 장치가 무너지기 때문입니다. 다른 우주선은 지금 모습이 아니라 빛이 출발한 과거의 유령으로 보이고 신호는 빛의 속도로 나아가 도착할 즈음이면 상대가 이미 다른 곳에 가 있어 빗나갑니다. 이 군중 속의 고독이 전부 광속 지연 위에 서 있습니다. 광속을 넘게 두면 누구에게든 즉시 도달하고 실시간으로 보게 되어 그 고독 자체가 사라집니다. 인과율이 깨지는 물리법칙의 붕괴는 오히려 그다음 문제입니다. 수학적으로도 광속은 그저 빠른 속도의 상한이 아니라 식이 실수로 정의되는 경계입니다. 로런츠 인자는 속도가 광속에 다가가면 무한대로 발산하고 광속을 넘으면 음수의 제곱근이 되어 허수로 넘어가 시간도 에너지도 정의되지 않습니다[^The Relativistic Rocket]. 게다가 아무리 엔진이 강력해도 등가속으로는 광속에 점근할 뿐 결코 도달하지 못하므로 추력을 100배 과장하는 정도의 양보로는 애초에 넘을 수가 없습니다. 그래서 진짜 초광속은 편도 웜홀 하나로 좁게 격리하고 벽 자체는 끝까지 지켰습니다.

블랙홀에서도 같은 태도를 지켰습니다. 게임적으로 편하게 하는 대신 똑같이 흉내 내기로 했습니다. 사건의 지평선으로 떨어지는 우주선은 밖에서 지켜보는 사람에게는 점점 느려지고 붉어지고 어두워지다가 지평선에서 영원히 얼어붙은 채 서서히 사라집니다. 넘었는지 죽었는지 끝내 확인할 수 없습니다. 이 관측자 비대칭은 일반상대성에서 지평선에 다가갈수록 시계가 멈추고 적색편이가 무한대로 가는 식에서 나옵니다[^Gravitational redshift]. 이 현상을 게임 장치로 그대로 옮겼습니다. 지평선을 넘은 우주선은 영원히 소실되고, 그 자리에 영구 추모비가 남고, 옛 이름은 영영 은퇴하고, 조종사는 새 이름으로 다시 시작합니다. 그래서 훗날 그 블랙홀 곁을 지나는 다른 우주선들은 지평선을 넘으려 한 우주선을 언제까지나 볼 수 있습니다. 넘는 순간이 영원히 기록됩니다. 지난 글에서 이 비대칭을 군중 속의 고독이 도달하는 가장 먼 자리라고 적었지만 그 자리는 이번에도 옮기지 않았습니다.
지우지 않고 흉내 낸 것이 하나 더 있습니다. 우주선의 크기입니다. 가시성을 위해 실제보다 엄청나게 크게 그리되 접근하는 천체에 따라 조용히 다시 축척을 맞춥니다. 고정된 과장으로 두면 행성에 다가갈 때 행성만 한 우주선이 되어 어색해지므로 지배천체를 물리 스케일로 두고 우주선의 렌더 크기를 그 맥락에 맞춰 조정해서 거대한 행성 위의 작은 물체로 보이게 합니다. 절대 크기는 거짓말이지만 맥락에 적응하는 거짓말이라 작은 우주선과 거대한 우주라는 상대적 감각만은 어디서든 지켜집니다. 여기서도 원칙은 같습니다. 경험을 만드는 상대적 크기감은 지키고 그와 무관한 절대 수치는 양보합니다. 무엇을 지키고 무엇을 굽힐지의 이 경계가 만드는 내내 그리고 운영하는 내내 흔들리지 않은 유일한 상수였습니다.

우주가 계속 새로 관측되니 게임도 계속 자라야 합니다. 매달 공개되는 천문 논문을 훑어 게임에 반영할 만한 특징을 골라 넣는 파이프라인을 돌렸습니다. 이 인제스트는 개발 시점에만 바깥과 통신하고 결과는 정적인 데이터로 커밋되므로 라이브 서버가 외부에 호출을 내는 일은 여전히 ISS의 실시간 궤도 정보 하나뿐입니다[^Two-line element set]. 이렇게 관측된 유일한 거울 행성 LTT 9779 b, 별이 되다 만 갈색왜성이 통과하는 TOI-6884, 세 번째 성간 침입자 혜성 3I/ATLAS, 가장 가까운 단독성 바너드별의 네 행성계 같은 것들을 반영했습니다. 특히 세 번째 성간 혜성은 이미 만들어 둔 쌍곡선 궤도 기계 위에 카탈로그 한 줄과 출처 주석만 더하면 되는 골든을 건드리지 않는 가장 값싼 형태로 들어왔습니다. 반영한 것들은 가이드 웹사이트의 '게임에 반영한 연구' 페이지에 논문 소개와 파라미터 표, 그리고 그곳으로 곧장 날아가는 링크로 묶어 두었습니다. 이 딥링크의 주소 문법과 페디버스 봇이 다는 링크의 문법이 같아서 새로 반영한 발견 하나가 연구 페이지에서도 봇의 게시글에서도 같은 좌표로 이어집니다.
돌아보면 이 우주를 만들고 서비스한 과정은 처음부터 끝까지 다체 문제를 정면으로 풀지 않은 이야기였습니다. 삼체인이 별을 예측하지 못한 것은 세 개의 태양을 다 계산하려 했기 때문이고 제가 우주를 예측할 수 있게 한 것은 왜소행성과 우주선의 질량을 0으로 두고 지배천체 하나만 남겼기 때문입니다. 게임을 혼자 서비스할 수 있었던 것도 같은 이유입니다. 웹사이트와 앱을 하나의 지배천체로 묶고, 반복되는 살림의 질량을 0으로 만들어 기계에 넘기고, 골든이라는 기준점 하나만 흔들리지 않게 지키고, 광속이라는 벽만은 끝까지 붙들었습니다. 다체 문제를 푸는 유일한 방법은 다 풀려고 하지 않는 것이었습니다. 물론 이렇게 접어 둔 것들이 언젠가 다시 질량을 얻고 저를 끌어당길지도 모릅니다. 진짜 만 대가 한 항성계에 몰려 콜드스타트로 접속하는 날이 오면 지금의 샤딩은 무력하고 무로그인으로 열어 둔 문은 언젠가 더 무거운 인증을 요구할 것입니다. 그래도 지금은 별이 예측되는 이 조용한 우주에 가끔 들어가 제 작은 점이 그동안 얼마나 멀리 왔는지를 확인하고 다시 접속을 끊습니다. 여전히 어딘가 다른 우주선이 빛의 지연 너머 흐릿한 유령으로 스칩니다. 관측은 됩니다. 상호작용은 여전히 되지 않습니다. 그 거리가 궁금하시면 space.woojinkim.org에 방문해 보세요.