이번에 Dev-log를 신설했습니다.
이 블로그를 만들어가는 과정, 그리고 이 서버를 어떻게 만들고 운영해나가는지를 일종의 일기처럼 적어보고자 합니다.
그 첫번째 주제로 팩토리오 헤드리스 서버 운영을 가져왔습니다.
Factorio 서버를 실행하는 것 자체는 생각보다 어렵지 않다. Linux용 headless server를 설치하고 세이브 파일과 모드를 준비한 뒤 서버를 실행하면 기본적인 멀티플레이 환경은 만들 수 있다. 그치만 이전의 플레이에서는 이마저도 귀찮고 번거로워서 피씨에서 서버를 실행하고 컴퓨터를 켜놓고 다녔었다. 내 팩토리오 플레이타임이 1,000시간을 넘어가는것은 내가 굇수라서가 아니다..
그렇다보니 이번에 만들고 싶었던 것은 단순히 24시간 켜져 있는 Factorio 서버가 아니었다.
Space Age이후 에란델의 우탐모호환이 늦어지면서 새로운 모드팩 컨셉을 잡기가 사실 어려운 상황이였는데, 여러모로 탐색 결과 Krastorio2를 SA에 맞도록 호환한 KSO라는 모드팩을 중심으로 여기에 호환이되는 행성 추가모드들이 많이 나왔다는 사실을 알게되었다. 그래서 이들을 중심으로 여러 전투·환경·편의성 모드를 조합한 장기 플레이용 모드팩을 운영하고, Discord에서 게임 상황을 확인하고, 플레이 기록을 남기고, 브라우저에서는 현재 개척 상황을 지도처럼 살펴볼 수 있는 환경을 만들고 싶었다.
말하자면 단순한 게임 서버보다는 작은 게임 서비스에 가까운 구성이었다.
그런데 프로젝트는 서버를 설치하기도 전부터 막히기 시작했다.
우리가 사용하고 싶었던 모드 중 일부가 Factorio 2.1에서 더 이상 정상적으로 동작하지 않았기 때문이다.
호환되지 않는 모드를 포기하는 대신 몇몇 모드는 직접 비공식 2.1 호환 버전을 만들어 사용했고, 이후 headless server, Discord Bridge, Chronicle, Mapshot과 웹 게시 자동화까지 하나씩 붙여갔다.
그리고 이 과정 대부분에서 AI를 적극적으로 활용했다.
예전에 팩토리오-디스코드 브릿지를 운영해 본 경험으로는 모드를 다운받고, 설정파일의 설명이 적힌 개발문서를 읽고, 복잡한 설명파일 내의 주석을 하나하나 다 해석해가며 어렵사리 얼렁뚱땅 셋업했던 기억이다. 그런데 이젠 지금버전에서 작동도 되지 않는다니!
하지만 AI는 오래된 Lua 코드에서 변경된 API를 찾아내고 수많은 로그와 소스를 조사하는 데 상당한 시간을 절약해줬다.
반대로 꽤 그럴듯한 잘못된 가정을 제시하거나, 실제 동작을 확인하지 않은 상태에서 문제를 해결했다고 판단하면서 오히려 문제 해결을 어렵게 만든 경우도 있었다.
그래서 이번 기록은 단순한 Factorio 서버 설치기가 아니다.
Factorio 2.1 대형 모드 서버를 실제 운영 가능한 서비스로 만들면서, AI를 어디까지 활용할 수 있었고 어디부터는 실제 시스템으로 검증해야 했는지에 대한 기록이기도 하다.
1. 우리가 만들고 싶었던 서버
이번 서버의 기본 환경은 Ubuntu 24.04 LTS와 Factorio 2.1 계열이다.
게임 자체는 Krastorio 2 Spaced Out을 비롯해 행성 확장, 적대 환경 강화, 전투 및 QoL 계열 모드를 조합해 장기간 플레이하는 것을 목표로 했다. 특히나 바닐라 몹의 경우 고인물 몇명이 등장하면 너무나 무기력하게 정리가 가능한데다, 이미 기존의 Rampant모드를 플레이 해 본 경험을 발판삼아 우주방어를 수행하며 힘겹게 하나씩 확장할때마다 유저들이 희열을 느끼며 플레이하기를 원했다. 자연스럽게 길어지는 플레이 경험을 제공하는게 목표. 이 구성을 Hostile Worlds라고 부르고 있다.
여기에 기존 유저들이 디스코드를 통해 마인크래프트 인게임 채팅과 직접 소통할 수 있었던 기억, Dynmap을 활용한 실시간 웹맵 제공으로 접속중이지 않더라도 실시간으로 유저들에 의해 변해가는 지도를 감상하는 재미, 여기에 살짝 추억 한스푼을 얹으면 BOINC.kr 운영할때 만들었던 주간랭킹같은 소소한 재미까지 한번에 버무리면 재밌겠다! 라고 구상을 시작했다.
하지만 실제 구축을 시작하고 보니 이 모든 기능을 Factorio 하나에 모두 붙이는 것은 좋은 구조가 아니었다.
그래서 점차 기능을 분리해서 현재는 아래와 같은 구조로 운영중이다.

여기서 가장 중요한 원칙은 하나였다.
게임 서버는 가능한 한 게임만 담당하게 한다.
Discord 기능이 고장 났다고 Factorio를 재시작하거나, 지도 렌더러가 죽었다고 실제 플레이 세션까지 함께 종료되는 구조는 장기 운영에 적합하지 않다고 판단했다.
이 원칙은 이후 여러 문제를 해결할 때 꽤 큰 도움이 됐다.
2. 시작부터 막힌 Factorio 2.1 모드 호환성
서버를 운영하기 전에 먼저 해결해야 할 문제가 있었다.
사용하고 싶었던 모드 중 일부가 Factorio 2.1에서 정상적으로 동작하지 않았다.
Factorio 2.1이 아직 Experimental branch이기때문에 최신 버전 대응이 늦어진 모드도 있고 아직 출시가 안된 경우도 있었다.
선택지는 세 가지였다.
1. 모드를 포기한다.
2. 비슷한 대체 모드를 찾는다.
3. 직접 Factorio 2.1에 맞게 수정한다.
가능한 경우에는 당연히 대체 모드를 먼저 찾았다.
하지만 Hostile Worlds의 성격을 만드는 데 중요하거나 적절한 대체재가 없는 경우에는 결국 세 번째 방법을 선택했다.
원본을 기반으로 Factorio 2.1 및 현재 모드팩 조합에서 동작하도록 비공식 호환 버전을 제작했다.
여기서부터 AI가 본격적으로 개발 도구로 들어오기 시작했다.
참고로 이런 호환판을 외부에 배포하려면 원본 모드의 라이선스와 재배포·수정 허용 범위를 반드시 별도로 확인해야 한다. 이번 작업의 목적은 우선 우리가 운영할 서버의 호환성 확보였다.
3. 처음에는 AI에게 “2.1에서 돌아가게 고쳐줘”라고 했다
처음에는 꽤 단순하게 생각했다.
Factorio 구버전에서는 동작하던 Lua 코드가 있고 2.1에서 오류가 발생한다면 오류 로그와 소스를 AI에게 주고,
Factorio 2.1에서 동작하도록 수정해줘.
라고 하면 상당 부분 자동으로 해결할 수 있을 것 같았다.
실제로 상당히 잘하는 부분도 있었다.
예를 들어 AI는 다음 작업을 매우 빠르게 처리했다.
- 구버전 API가 사용된 위치 탐색
- 변경되거나 제거된 prototype 확인
- stack trace에서 실제 오류 발생 위치 추적
- data stage와 runtime stage 오류 구분
- 다른 모드와의 dependency 및 조건문 추적
- 동일한 구버전 패턴이 사용된 다른 파일 탐색
- 최소 수정 후보 작성
- 수정 후 확인해야 할 regression 항목 정리
특히 처음 보는 오래된 모드를 사람이 처음부터 모두 읽는 것보다 조사 범위를 좁히는 속도가 훨씬 빨랐다.
하지만 곧 한 가지를 알게 됐다.
AI에게 가장 잘 맞는 역할은 반드시 “코드를 대신 작성하는 프로그래머”만은 아니었다.
오히려 더 강력했던 것은 코드베이스 탐색기와 조사 보조자로 활용했을 때였다.
4. “고쳐라”보다 “왜 깨졌는지 조사하라”
작업 방식도 조금씩 바뀌었는데, 코드 수정 > 오류 확인 > 수정의 순서로 계획은 했으나 오래된 모드에서는 한 줄의 오류가 전체 문제를 설명하지 않는 경우가 많았다.
그래서 이후에는 먼저 이런 식으로 요청했다.
이 오류가 발생할 수 있는 코드 경로를 조사하고, Factorio 2.1의 변경사항과 비교한 뒤 근본 원인과 최소 수정 범위를 먼저 설명하라.
이렇게 하면 과정이 달라진다.
오류 재현 > 로그 확보 > 관련코드 탐색 > 2.1변경 API/Prototype확인 > 영향범위 조사 > 최소수정 > 인게임 검증
가능하면 원본 모드 전체를 새로 작성하지 않고 2.1에서 깨지는 경계만 보수하는 방식을 택했다.
기존 동작을 최대한 유지하고 변경 범위를 줄이면 다른 모드와의 예상하지 못한 충돌 가능성도 줄어들기 때문이다.
AI는 이 과정에서 굉장히 유용했다.
하지만 중요한 조건이 하나 있었다.
AI가 “수정됐다”고 말하는 것으로 작업을 끝내면 안 됐다.
결국 Factorio가 실제로 모드를 로드하는지를 확인해야 했다.
5. AI가 꽤 그럴듯하게 틀릴 때
프로젝트가 진행될수록 AI의 약점도 분명해졌다.
AI는 정보가 부족한 상태에서도 상당히 그럴듯한 원인을 만들어낼 수 있다.
그리고 그 원인이 실제로 과거에 겪었던 장애와 비슷하면 사람도 쉽게 그 가설에 끌려간다.
뒤에서 다시 이야기할 Mapshot 문제가 대표적이었다.
새 월드의 지도가 정상적으로 보이지 않았을 때 여러 후보가 등장했다.
Nginx 문제? rsync 권한? Mapshot metadata? surface 선택? 탐사 영역? 브라우저 cache?
각각 충분히 가능한 이야기였다.
특히 실제로 과거에 rsync 권한 문제가 있었기 때문에 더욱 설득력 있게 들렸다.
하지만 최종 원인은 전혀 다른 곳에 있었다.
AI가 틀렸다는 사실 자체가 문제라기보다, 그럴듯한 가설을 사실처럼 받아들이는 순간부터 문제가 커졌다.
그래서 프로젝트 후반부로 갈수록 질문도 달라졌다.
처음에는: “원인이 뭐야?” 라고만 물어봤지만
이후에는: “가능한 원인을 조사하고, 각 가설이 맞는지 실제 환경에서 어떻게 검증하거나 반증할 수 있는지 제시해줘.
라고 요청하는 방식이 됐다.
더 복잡한 문제에서는 아예 소스와 로그를 직접 조사하도록 맡기고, 최종적으로 관찰 가능한 결과를 요구했다.
이 변화는 뒤의 Mapshot 문제에서 특히 효과적이었다.
6. Headless 서버와 지도 Renderer를 분리하다
본격적으로 서버가 안정된 뒤 웹에서 월드의 현재 상황을 확인할 수 있도록 Mapshot을 붙이기 시작했다.
여기서 또 하나의 문제가 등장했다.
실제 Factorio 서버는 그래픽 출력이 필요하지 않으므로 headless build를 사용한다.
그런데 Mapshot은 Factorio의 screenshot 기능을 이용한다.
즉,
Factorio Headless만으로 운영되는 서버와 game.take_screenshot() 을 사용해야 하는 Mapshot의 요구사항이 서로 맞지 않았다.
해결책은 지도 전용 GUI Factorio를 별도로 운영하는 것이었다.
GUI 환경이 없는 Ubuntu 서버에서는 Xvfb를 사용해 가상 디스플레이를 제공했다.
여기서 앞서 본 전체구조도를 다시 한번 보자.

위 구조를 채택한 덕분에 Mapshot은 실제 서버 save를 직접 건드리지 않고 별도의 renderer 영역으로 복사한 파일을 사용한다. 덕분에 지도 렌더가 실패하더라도 실제 Factorio 서버에는 영향을 주지 않는다.
7. pkill factorio를 사용하지 않기로 했다
같은 서버에 다음 두 프로세스가 함께 존재하기 때문이다.
Renderer를 별도로 운영하면서 프로세스 관리에도 원칙이 필요했다.
Factorio Headless Server
Factorio GUI Mapshot Renderer
따라서 이런 명령은 위험하다.
pkill factorio
Mapshot 하나를 종료하려다 실제 플레이 중인 서버까지 함께 종료할 수 있다.
그래서 자동화에서는 Mapshot용 GUI Factorio의 정확한 실행 경로와 PID만 추적하도록 했다.
실제로 이후 여러 차례 renderer를 강제로 종료해야 할 일이 있었지만 headless server에는 신호를 보내지 않았다.
이번 프로젝트에서 자주 반복해서 등장한 원칙 중 하나다.
운영 환경에서는 “관련된 것을 전부 죽인다”보다 “내가 시작한 것만 정확하게 종료한다”가 중요하다.
8. 첫 번째 Mapshot 실패 — 창이 떴다고 준비된 것은 아니었다
초기 Mapshot 자동화는 다음과 같은 구조였다.
GUI Factorio 실행 > Factorio Window 탐지 > (잠시 대기) > /mapshot 입력
처음엔 이 시작시간만 조절하면 될 것으로 생각했으나 Hostile Worlds처럼 많은 모드를 사용하는 환경에서는 GUI window가 생성되는 시점과 실제 save가 모두 로딩되는 시점 사이에 꽤 큰 차이가 있었다. 특히나 이 시간은 게임이 진행 될 수록 더 길어질 터였다.
실제 로그를 조사해보니 Factorio의 창은 일찍 나타났지만 Mapshot runtime이 준비되기까지는 약 45초가 걸렸다.
기존 자동화는 그보다 훨씬 일찍 /mapshot을 보내고 있었다.
0초 Factorio 실행
1초 Window 발견
<약 10초>
/mapshot 입력
…. 아직 로딩 중
<약 45초>
Mapshot 초기화 완료
Mapshot 모드가 초기화 되기도 전에 명령이 전달되고 있었으므로, 가장 단순한 해결책은
sleep 10을 60으로 수정하는 것.
하지만 이 방법은 사용하지 않았다.
서버 부하와 모드 구성에 따라 60초가 충분할 수도 있고 부족할 수도 있기 때문이다.
대신 시간이 아니라 상태를 기다리도록 수정했다.
Factorio 로그에서 Mapshot의 control.lua startup이 실제로 완료된 것을 확인한 뒤에만 /mapshot을 보내도록 했다.
Waiting for Mapshot runtime readiness...
Mapshot runtime ready.
Starting Mapshot render...
첫 번째 문제는 해결됐다.
그런데 이후 훨씬 흥미로운 문제가 나타났다.
9. 새 지도는 만들어졌는데 아무것도 보이지 않았다
새로운 월드로 서버를 시작한 뒤 Mapshot을 다시 돌렸다.
기존 월드 지도는 사라졌고 새로운 snapshot metadata도 정상적으로 생성됐다.
새로운 map ID, seed, Nauvis surface, 탐사 영역, 플레이어 정보까지 모두 들어 있었다.
하지만 브라우저에서 지도를 열어보면 거의 아무것도 보이지 않았다.
Mapshot 원본과 웹 서버의 타일 수를 확인했다.
SOURCE : 48
WEB : 48
처음에는 웹 게시 문제를 의심했다.
실제로 이전에는 rsync 과정에서 소유권과 그룹 권한 때문에 실패한 적도 있었다.
그래서 publish 단계는 다음처럼 정리했다.
rsync -a --no-owner --no-group --delete \
SOURCE/ \
WEBROOT/
수동 복사와 rsync 역시 정상적으로 동작했다. 그런데 결과는 그대로였다.
이제 한 가지가 확실해졌다.
웹 서버가 48개만 받은 것이 아니라 Mapshot 원본부터 48개밖에 없었다.
문제는 Nginx나 rsync보다 더 앞에 있었다.
10. 89장을 만들기로 했는데 실제로는 48장
Factorio 로그에는 Mapshot의 렌더 계획이 남아 있었다. 이것과 실제 파일을 zoom별로 확인하니 결과는 이랬다.
| Zoom | 예상 | 실제 |
| s1zoom_0 | 4 | 4 |
| s1zoom_1 | 4 | 4 |
| s1zoom_2 | 6 | 6 |
| s1zoom_3 | 15 | 15 |
| s1zoom_4 | 60 | 19 |
| 합계 | 89 | 48 |
상당히 좋은 단서였다.
앞쪽 zoom은 모두 정상적으로 완료됐다.
그런데 가장 많은 screenshot이 필요한 마지막 zoom은 60장 중 19장에서 정확히 끊겨 있었다.
이제 단순히 “지도 생성이 실패했다”가 아니었다.
정상적으로 렌더링을 진행하다가 중간에 무엇인가가 Factorio를 종료시키고 있었다.
그리고 로그에서 결정적인 줄을 발견했다.
Mapshot: all screenshots started, might take a while to render
그로부터 약 2초 뒤:
Received SIGTERM, shutting down
SIGTERM을 보낸 주체는 우리가 만든 자동화였다.
11. started는 completed가 아니었다
Mapshot의 control.lua까지 직접 조사한 뒤 원인이 확정됐다.
일반 /mapshot 명령을 실행하면 Mapshot은 필요한 각 타일에 대해 Factorio의:
game.take_screenshot()
을 연속해서 호출한다.
중요한 점은 이 요청들이 그 자리에서 모두 JPG 파일로 만들어지는 것이 아니라 Factorio의 비동기 screenshot/render queue에 등록된다는 것이다.
따라서 Mapshot 로그의:
Mapshot: all screenshots started, might take a while to render
는: “모든 screenshot이 완성되었다.”라는 뜻이 아니었다.
실제로는:
필요한 screenshot 요청을 모두 등록했다. 실제 파일 생성은 아직 진행 중이다.
라는 의미였다.
하지만 당시 자동화는 Mapshot 실행 직후 생성되는 mapshot.json을 발견하면 렌더가 끝난 것으로 판단하고 있었다.
결과적으로 이런 일이 벌어졌다.
/mapshot
mapshot.json 생성
89개 screenshot 요청 등록
자동화: "완료됐다!"
GUI Factorio SIGTERM
랜더 큐에 남은 타일 유실
실제 결과가 48장이었던 이유도 설명된다.
앞쪽 29장은 모두 끝났고 마지막 zoom에서 19장까지 처리된 시점에 Factorio가 종료됐다.
29 + 19 = 48
단순한 우연의 숫자가 아니었다.
12. 여기서 AI도 한 번 더 검증해야 했다
처음에는 89 expected / 48 actual이라는 결과를 보고 바로:
89개가 될 때까지 기다리면 된다.~ 라고 결론 내릴 수도 있었다.
하지만 이것 역시 가정이다.
Mapshot이 로그에 표시한 tiles to generate가 반드시 최종 JPG 수와 1:1로 대응한다는 보장은 없기 때문이다.
그래서 이번에는 AI에게도 정답을 요구하지 않고 가정 자체를 검증하도록 했다.
질문은 다음처럼 바뀌었다.
tiles to generate가 현재 설정에서 실제 take_screenshot() 호출 수 및 최종 JPG 수와 1:1로 대응하는지 Mapshot 소스 코드에서 확인하라.
소스를 조사한 결과 Mapshot에는 minjpgquality 설정에 따라 일부 타일 생성이 생략될 수 있는 코드가 있어 일반적으로는 단순한 격자 수와 JPG 파일 수가 항상 같다고 단정할 수 없었다.
하지만 현재 서버 설정은:
- jpgquality = 75
- minjpgquality = 75
이므로 이번 렌더에서는 모든 격자에 take_screenshot()이 호출된다.
따라서 현재 환경에서는:
예상 89 = screenshot 요청 89 = 정상 완료 시 JPG 89
라는 것을 코드 수준에서 확인할 수 있었다.
이 단계가 꽤 중요했다.
결과를 우리가 원하는 숫자에 억지로 맞춘 것이 아니라, 그 숫자가 정말 정답인지부터 검증한 것이다.
13. 완료 조건을 다시 만들다
Mapshot 일반 /mapshot 명령에는 우리가 바로 사용할 수 있는 공식적인 render completion event나 done marker가 없었다.
조사한 범위에서는 다음과 같은 구조였다.
- 일반 /mapshot에 완료 marker 없음
- on_screenshot_taken 사용 없음
- on_player_render 사용 없음
- startup 방식에는 별도의 screenshot 완료 대기 처리 존재
- 일반 명령 방식은 렌더 요청 후 실제 screenshot queue 완료를 직접 알려주지 않음
그래서 자동화에서 직접 완료 여부를 검증하기로 했다.
현재 흐름은 다음과 같다.
GUI Factorio 실행 → Mapshot runtime readiness 확인 → /mapshot 실행 → 이번 실행의 예상 타일 수 계산 →
새 d-xxxxxxxx snapshot 확인 → 렌더 시작 이후 생성·갱신된 JPG 감시 →
89 / 89 ?
│
├── NO → 계속 대기
│
└── YES
↓
Render 검증
↓
Publish
↓
SOURCE / WEB 검증 > save SHA 기록 > GUI Factorio 종료
이제 mapshot.json이 존재한다고 성공하지 않는다.
실제 결과물이 완성되어야 다음 단계로 넘어간다.
14. 실패한 작업을 성공했다고 기억하면 안 된다
Mapshot은 세이브가 바뀔 때마다 매번 전체 지도를 다시 만들 필요가 없다.
현재 live save의 SHA-256을 계산해 이전에 성공적으로 렌더한 세이브와 동일하다면 작업을 생략한다.
Live Save SHA → last-rendered.sha256 비교 → 같음 → SKIP
다름 → RENDER
그런데 기존 자동화에는 치명적인 문제가 하나 더 있었다.
불완전한 지도를 성공으로 판정한 뒤 현재 세이브의 SHA까지 last-rendered.sha256에 기록해버린 것이다.
그러면 다음 실행에서는: “이 세이브는 이미 처리했음” 이라고 판단한다.
실패한 작업이 스스로 재시도를 막아버리는 셈이다.
그래서 SHA 갱신을 가장 마지막 단계로 옮겼다.
Render 완료 → Render 검증 → Publish → Publish 검증 → SHA 기록
즉 last-rendered.sha256은 단순한 캐시 파일이 아니라:
“이 세이브는 렌더와 게시까지 검증된 상태로 완전히 처리되었다.“는 commit marker 역할을 한다.
이번 작업에서 얻은 교훈 중 다른 자동화에도 그대로 적용할 만한 부분이었다.
15. 실제 서버에서 다시 검증하다
수정 후에는 추측으로 끝내지 않고 실제 서버 경로에서 한 차례 전체 렌더를 실행했다.
새 snapshot에서 다음과 같은 결과가 나왔다.
Waiting for render completion...
Mapshot render completed.
Verified tiles: 89/89
Publishing...
Publish verified: SOURCE=357 WEB=357
SUCCESS
Stopping Mapshot Factorio...
새 snapshot의 zoom별 결과도 확인했다.
Mapshot 전체 데이터도:
| Zoom | SOURCE | WEB |
| s1zoom_0 | 4 | 4 |
| s1zoom_1 | 4 | 4 |
| s1zoom_2 | 6 | 6 |
| s1zoom_3 | 15 | 15 |
| s1zoom_4 | 60 | 60 |
| 합계 | 89 | 89 |
SOURCE : 357
WEB : 357
로 일치했다.
추가로 다음을 확인했다.
- 0 byte JPG 없음
- SOURCE와 WEB 상대경로 일치
- 파일 크기 일치
- live save SHA와 성공 상태 SHA 일치
- 실제 Factorio headless server 재시작 없음
- 실제 서버 PID에 signal 전달 없음
- 자동화가 직접 실행한 GUI renderer만 종료
- SIGTERM은 89/89 검증 이후 발생
이번에는 SUCCESS라는 문자열뿐 아니라 실제 산출물이 성공했다는 증거까지 확보했다.
16. Discord와 Chronicle — 게임에 접속하지 않아도 살아 있는 서버
지도 외에 구축한 또 다른 축은 Discord 연동이다.
Factorio와 Discord 사이에 Bridge를 두고 게임 채팅과 주요 정보를 외부에서 확인할 수 있도록 구성했다.
그 위에는 플레이 기록과 서버 상황을 조금 더 구조적으로 다루기 위한 Chronicle 계층도 별도로 만들었다.
목표는 단순한 채팅 중계를 넘어:
- 서버 상태 확인
- 주요 이벤트 기록
- 플레이 기록 축적
- 지도 링크 제공
- Discord에서 서버 상황 확인
등을 게임에 접속하지 않고도 사용할 수 있게 하는 것이었다.
이 계층 역시 Factorio와 직접 강하게 결합하지 않았다.
Factorio → RCON → Bridge → Chronicle / Discord
Bridge나 Chronicle을 수정하고 재시작해도 실제 Factorio 서버는 계속 플레이를 유지할 수 있다.
Mapshot renderer와 같은 이유다.
부가 서비스가 고장 나더라도 본 서버까지 함께 쓰러지지 않게 한다.
17. AI의 역할도 프로젝트와 함께 바뀌었다
돌이켜보면 이번 서버를 구축하는 동안 AI를 사용하는 방식 자체가 상당히 달라졌다.
지금은 오히려 다음과 비슷하다.

Factorio 2.1 비공식 호환 모드를 만들 때도 이 방식이 효과적이었고, Mapshot 장애를 해결할 때도 마찬가지였다.
AI에게 상당히 많은 일을 맡길 수 있다.
코드를 읽고, 로그를 비교하고, 문제가 될 API를 찾고, 수정 코드를 만들고, 테스트 방법까지 설계할 수 있다.
하지만 AI의 최종 응답을 성공 조건으로 삼지는 않는다.
Factorio가 실제로 모드를 로드했는가?
서버에 실제로 접속할 수 있는가?
Discord 메시지가 실제로 오가는가?
Mapshot 타일이 실제로 89장 존재하는가?
SOURCE와 WEB 파일이 실제로 일치하는가?
기존 게임 서버는 여전히 살아 있는가?
결국 작업의 마지막 단계는 항상 이런 관찰 가능한 결과였다.
18. 이번 구축에서 얻은 운영 원칙
여러 문제를 겪고 나니 앞으로 다른 서비스를 만들 때도 그대로 사용할 만한 원칙들이 몇 가지 남았다.
시간을 기다리지 말고 상태를 기다린다 60초 지났으니 준비됐겠지 보다,
실제로 준비됐다는 증거가 있는가? 를 확인하는 편이 훨씬 안전하다.
Mapshot에서는 window 생성이 아니라 실제 startup 로그가 readiness 조건이 됐다.
started와 completed를 구분한다
이번 작업의 가장 상징적인 문제였다.
비동기 작업에서는 특히 요청 등록과 실제 작업 완료를 구분해야 한다.
SUCCESS
는 실행 결과가 아니라 검증 결과여야 한다
스크립트가 마지막 줄까지 도달했다는 이유만으로 SUCCESS를 출력하지 않는다.
현재 Mapshot에서 SUCCESS는 사실상:
렌더 완료 AND 산출물 검증 AND publish 성공 AND SOURCE/WEB 검증 AND 상태 기록 성공
을 의미한다.
상태파일은 가장 마지막에 갱신한다
last-rendered.sha256을 먼저 기록했다가 이후 작업이 실패하면 자동 재시도가 막힌다.
상태는 실제 작업이 모두 완료된 뒤 commit한다.
내가 만든 프로세스만 종료한다
pkill factorio처럼 이름이 같은 프로세스를 모두 종료하는 방법보다 정확한 실행 경로와 PID를 추적한다.
특히 한 머신에서 실제 게임 서버와 GUI renderer를 함께 운영하는 경우에는 필수적이다.
AI의 답보다 검증 방법이 더 중요할 때가 있다
AI는 빠르게 좋은 가설을 만들 수 있다.
그리고 빠르게 잘못된 가설도 만들 수 있다.
그래서 “원인이 뭐야?” 보다
“가설을 세우고, 각 가설을 검증하거나 반증할 방법을 찾아라.”
라는 질문이 훨씬 유용했다.
이번에는 AI의 실수도 결국 개발 과정의 일부가 됐다.
19. 지금의 Hostile Worlds
현재는 처음 계획했던 형태에 꽤 가까워졌다.
Factorio 2.1에서 사용할 수 없던 일부 모드는 호환성을 보완했고, headless server는 장기간 실행할 수 있는 상태가 됐다.
Discord 서비스는 게임 서버와 분리되어 있고, Mapshot renderer 역시 실제 플레이 서버와 분리되어 있다.
지도는 세이브가 변경된 경우에만 처리하고, 렌더와 publish가 실제로 성공한 경우에만 해당 세이브를 완료 상태로 기록한다.
처음에는 친구들과 사용할 Factorio 서버 하나를 만들 생각이었다.
그런데 하나씩 필요한 기능을 붙이다 보니 어느새 작은 게임 서비스 운영에 가까운 구조가 되었다.
그리고 그 과정에서 꽤 많은 시간을 잡아먹은 문제를 한 줄로 줄이면 의외로 단순하다.
started ≠ completed
그 차이를 제대로 검증하기 시작하면서 서버 자동화도, AI를 사용하는 방식도 훨씬 안정적으로 변했다.
20. 앞으로 기록할 것들
이번 첫 글에서는 Hostile Worlds 서버가 현재 구조에 도달하기까지의 전체 흐름과, 그 과정에서 가장 인상적이었던 문제들을 중심으로 정리했다.
세부 구현은 각각 따로 기록할 가치가 충분히 있을 것 같다.
향후 Dev-log에서는 다음과 같은 내용을 하나씩 다뤄볼 예정이다.
- Factorio 2.1 대형 모드팩 구성과 서버 배포
- 구버전 Factorio 모드를 2.1에 맞게 호환시키면서 겪은 문제
- Hostile Worlds의 적대 환경과 QoL 모드 구성 기준
- Factorio Headless Server 장기 운영 구성
- Discord Bridge와 Chronicle 구축
- Xvfb + GUI Factorio를 이용한 Mapshot renderer
- Mapshot 자동화 전체 구조와 89 → 48 → 89 장애 분석
- systemd를 이용한 부가 서비스 자동 실행
- 세이브 백업과 장애 복구
- AI를 이용한 실제 서버 개발에서 효과적이었던 작업 방식과 실패 사례
아직 서버 플레이가 계속되고 있으니 새로운 문제도 분명 생길 것이다. 그러므로 그것도 전부 기록 해 볼 요량이다.
완벽한 서버를 한 번에 만든 기록보다는,
실제 서비스를 운영하면서 문제가 생기고, 잘못된 가설을 버리고, 증거를 찾아 수정하고, 다시 검증해 나가는 과정
자체가 Dev-log를 시작한 가장 큰 이유이기 때문이다.
지금은 적어도 처음 목표했던 “서버가 켜져 있다”를 넘어 “운영할 수 있는 서버” 단계에는 도달한 것 같다.
그리고 드디어 이 기록을 첫 글로 남길 수 있게 됐다.