
엔지니어링의 본질은 무엇일까요. 누군가는 화려한 최신 프레임워크를 능수능란하게 다루는 것이라고 말하고, 누군가는 수백 대의 분산 서버를 오케스트레이션하는 것이라고 말합니다. 틀린 말은 아닙니다. 하지만 제가 생각하는 '진짜' 엔지니어링의 시작은 하부 구조, 즉 0과 1이 물리적인 실리콘 위에서 어떻게 춤추는지 이해하는 순간부터입니다. 오늘은 잠시 GPU 클러스터와 H100의 굉음을 뒤로하고, 조금 더 원초적인 이야기를 해볼까 합니다. 바로 고전 게임 카트리지의 롬(ROM)을 뜯어보고 번역하는 과정에 대한 이야기입니다.
솔직히 고백하자면, 요즘 주니어 개발자들의 코드를 리뷰하다 보면 종종 한숨이 나옵니다. Python 라이브러리 import 몇 줄로 세상 모든 문제를 해결하려 들기 때문이죠. 메모리 레이아웃이 어떻게 잡히는지, 캐시 히트율(Cache Hit Ratio)이 왜 떨어지는지는 고민하지 않습니다. 그저 "요즘 이 라이브러리가 핫하니까요"라고 대답하면 할 말이 없습니다. 그런 분들에게 이 고전 게임 롬 해킹 과정은 일종의 충격 요법이 될지도 모르겠습니다. 여기엔 OS도, 가비지 컬렉터도, 친절한 디버거도 없습니다. 오직 헥사(Hex) 코드와 하드웨어의 제약만이 존재하니까요.
롬 번역, 단순히 텍스트만 바꿔치기하면 되는 거 아니냐고 묻는 분들이 있습니다. 천만에요. 그건 마치 비트맵 이미지를 메모장에 열어놓고 픽셀 값을 텍스트로 수정하겠다는 말과 같습니다. 게임 카트리지는 그 자체로 하나의 완결된 임베디드 시스템입니다. 개발 당시의 빡빡한 리소스 제약 속에서 탄생한 결과물이죠. 예를 들어, 1990년대 게임기들은 텍스트를 저장할 때 ASCII 코드를 쓰지 않는 경우가 태반입니다. 용량을 1비트라도 줄이기 위해 독자적인 인코딩 방식을 사용하거나, 자주 쓰는 단어를 딕셔너리 형태로 압축해 둡니다.

영어를 한글로, 혹은 일본어를 영어로 바꾸는 과정은 단순 번역이 아닙니다. 이것은 '메모리 관리'의 영역입니다. 원본 텍스트보다 번역된 텍스트의 길이가 길어지면 어떻게 될까요? 뒤에 이어지는 실행 코드를 덮어쓰게 됩니다. 그러면 게임은 멈추거나 깨지겠죠. 이를 해결하기 위해 해커들은 롬 파일 내의 빈 공간(Padding 영역)을 찾아내고, 포인터 테이블을 수정하여 텍스트가 저장된 주소를 재매핑(Remapping)해야 합니다. 이 과정은 제가 삼성전자에서 SSD 펌웨어를 짤 때, 제한된 낸드 플래시 블록을 효율적으로 쓰기 위해 고민했던 웨어 레벨링(Wear Leveling) 로직과 본질적으로 다르지 않습니다.
더 흥미로운 건 그래픽 폰트(Font) 처리입니다. 고전 게임은 폰트조차도 타일(Tile) 형태의 이미지로 저장합니다. 일본어 게임을 영어로 번역하려면, 한 글자가 차지하는 타일의 크기부터 다시 계산해야 합니다. 8x8 픽셀 안에 알파벳을 욱여넣어야 하는데, 가변 폭 폰트(VWF)를 지원하지 않는 엔진이라면 그야말로 지옥이 펼쳐집니다. 이 제약 사항을 우회하기 위해 어셈블리어 레벨에서 렌더링 루틴을 뜯어고치는 것, 이것이 진정한 최적화(Optimization)입니다. 요즘 개발자들이 툭하면 "서버 스펙 좀 올려주세요"라고 말할 때 느끼는 답답함이 여기서 해소되는 기분이랄까요.

하드웨어 스펙에 대한 이해 없이는 절대 해결할 수 없는 문제들도 있습니다. 대표적인 것이 '뱅크 스위칭(Bank Switching)'입니다. 8비트나 16비트 CPU가 주소 지정할 수 있는 메모리 공간은 한계가 있습니다. 하지만 게임 용량은 그보다 훨씬 크죠. 그래서 카트리지 내부에는 메모리 뱅크를 갈아 끼우는 하드웨어 로직이 들어있습니다. 번역 데이터가 특정 뱅크의 용량을 초과하면? 단순히 데이터를 뒤로 밀어내는 게 아니라, 아예 다른 뱅크로 옮기고 뱅크 전환 코드를 삽입해야 합니다. 이건 마치 제가 네이버에서 NSML을 설계할 때, GPU 메모리가 부족해서 모델 파라미터를 호스트 메모리와 스와핑(Swapping)하던 상황과 놀랍도록 닮아 있습니다.
물론 지금 시대에 롬 해킹 기술이 직접적으로 돈이 되지는 않습니다. 하지만 이 과정에서 배우는 '바이트 단위의 감각'은 여전히 유효합니다. 데이터가 메모리에 어떻게 적재되는지, 포인터가 가리키는 실제 주소가 물리적으로 어디인지, 리소스가 한계에 다다랐을 때 어떻게 우회할 것인지. 이런 고민을 해본 사람과 안 해본 사람의 코드는 결정적인 순간에 차이가 납니다. 대규모 트래픽이 몰려와 레이턴시가 튀는 상황에서, "DB 증설합시다"라고 외치는 사람과 "쿼리 실행 계획(Explain Plan)부터 봅시다"라고 말하는 사람의 차이 같은 것이죠.
저도 주니어 시절엔 무거운 툴이 주는 편안함에 안주하려 했습니다. 하지만 새벽 3시, 터져버린 커널 패닉 로그 앞에서 저를 구원한 건 화려한 대시보드가 아니라 덤프 파일 속 헥사 코드였습니다. 롬 번역이라는 마이너한 취미의 세계를 들여다보면서, 저는 다시금 엔지니어링의 기본을 되새깁니다.
여러분도 가끔은 추상화된 계층(Layer) 아래로 내려가 보셨으면 합니다. 편안한 IDE와 가상 머신 밖, 거친 하드웨어의 세계에서 데이터가 어떻게 살아 숨 쉬는지 느껴보세요. 그 험난한 여정이 끝나고 다시 본업으로 돌아왔을 때, 여러분이 작성하는 코드 한 줄, 변수 선언 하나가 이전과는 완전히 다르게 보일 것입니다. 그게 바로 엔지니어로서의 '성장'이니까요.


