플레이어 눈에 보이지 않던 AI 상대, 그리고 그들을 숨긴 한 줄
이 프로젝트의 한 축은 제가 만드는 보드게임용 AI 상대를 학습시키고 실제 플레이어 앞에 내놓는 일입니다. 이번 주에 조용한 버그를 하나 발견했습니다. YINSH용 상대를 여섯 개 학습·배포했는데, 정작 게임을 연 플레이어는 그중 셋만 고를 수 있었습니다. 가장 최신이자 가장 강한 봇들은 멀쩡히 돌아가고 있었지만 화면에는 전혀 보이지 않았습니다. 원인은 시작 스크립트의 누락된 한 줄이었고, 진짜 해결책은 새 상대를 매번 손으로 연결하는 방식 자체를 없애는 것이었습니다.
- YINSH에는 게임에 AI 상대를 초대하는 메뉴가 있습니다. 여섯이 배포됐지만 셋만 나타났습니다.
- 웹사이트에 설정을 넘겨주는 시작 스크립트가 마지막 세 상대에 대해 갱신되지 않아, 사이트는 그들의 존재 자체를 몰랐습니다. 에러도 없이 그저 짧은 메뉴만 떴습니다.
- 상대마다 손으로 연결하던 방식을 웹사이트가 읽고 검증하는 하나의 목록으로 바꿨습니다. 이제 상대 추가는 파일 여섯 개 수정이 아니라 한 줄입니다.
- 최신 상대가 번호와 함께 메뉴 맨 위에 오도록 해, 가장 강한 상대가 자연스러운 기본값이 됩니다.
- 같은 장비에서 돌던 12시간 학습을 멈추지 않고 라이브 테스트 사이트에서 검증했습니다.
플레이어가 실제로 본 것
YINSH는 육각형 보드에서 두 명이 두는 전략 게임입니다. 사이트에서 게임을 시작하면 대기실에 “AI 상대 초대” 드롭다운이 있습니다. 그 목록의 상대 하나하나는 서버에서 돌아가는 별도의 작은 프로그램이고, 그 안에 학습된 AI 모델이 하나씩 들어 있습니다. 하나를 골라 초대하면 게임에 들어옵니다.
증상은 단순했습니다. 여섯이어야 할 자리에 셋만 있었습니다. 빠진 셋은 가장 최근의 champion, 즉 제가 학습에 가장 많은 시간을 쏟은 봇들이었습니다. 멀쩡히 응답하고 있었지만 어떤 플레이어도 고를 수 없었습니다.
원인은 갱신되지 않은 시작 스크립트였습니다
같은 웹사이트가 한 번 빌드되어 두 곳에서 돕니다. 테스트본과 실제본입니다. 사이트가 부팅되면 작은 시작 스크립트가 해당 환경의 설정을 브라우저가 읽는 파일에 적어 넣습니다. 각 상대의 웹 주소가 바로 거기에 들어가야 했습니다.
기존 설계에서 상대 하나를 추가하려면 대략 여섯 군데를 손봐야 했습니다. 사이트 코드의 설정 필드, 메뉴 목록 항목, 시작 스크립트의 한 줄, 배포 파일의 한 줄, 그리고 상대 자신의 서버입니다. 시작 스크립트는 처음 세 상대의 줄만 받은 채로 멈춰 있었습니다. 그래서 배포 파일이 여섯 주소를 모두 올바르게 적어 두었어도, 스크립트가 절반을 조용히 흘려버렸고 웹사이트는 건네받은 짧은 목록을 충실히 그렸습니다.
아무것도 죽지 않았습니다. 그래서 조용했습니다. 빠진 상대는 애초에 추가된 적 없는 상대와 똑같아 보였습니다.
진짜 버그는 “여섯 군데를 고쳐야 한다”였습니다
누락된 한 줄은 증상이었습니다. 병은 새 상대마다 여섯 파일을 동시에 고쳐야 했고, 그중 하나라도 빠뜨리면 조용히 실패한다는 점이었습니다. 한 가지 일을 하는 데 여섯 번의 올바른 수정이 필요한 절차는 결국 다섯 번만 됩니다.
해결책: 웹사이트가 신뢰하고 검증하는 하나의 목록
전부를 하나의 설정으로 바꿨습니다. 상대 목록이고, 각 항목은 이름과 웹 주소뿐입니다. 웹사이트는 그 목록을 읽어 validator를 통과시킨 뒤 메뉴를 만듭니다. 이제 상대 추가는 한 곳의 한 항목입니다.
validator가 중요한 이유는 이제 웹사이트가 외부 데이터를 신뢰하기 때문입니다. 형식이 잘못됐거나 안전하지 않은 주소를 가리키는 항목은 버려서, 오타가 메뉴를 망가뜨리거나 플레이어를 엉뚱한 곳으로 보내는 일이 생기지 않습니다. 또 메뉴는 상대를 오래된 순으로 번호 매기되 최신순으로 보여 주어, 가장 최근의 강한 봇이 맨 위 기본 선택지가 됩니다.
학습은 건드리지 않고 라이브 검증
변경을 머지하고, 클러스터의 자동 배포 시스템이 스스로 다시 빌드해 롤아웃하도록 두었습니다. 몇 분 안에 라이브 테스트 사이트가 여섯 상대를 순서대로 모두 보여 주었고, 각 봇이 health check를 통과했습니다. 그동안 같은 GPU에서 12시간짜리 AI 학습이 돌고 있었지만, 이 작업 어느 것도 학습 시스템 안에 있지 않았기에 손댈 필요가 없었습니다.
핵심 용어
- YINSH — 육각형 보드 위에서 ring과 marker로 두는 2인용 추상 전략 게임입니다.
- Bot / AI 상대 — 학습된 모델 하나를 품고 사람처럼 수를 두는, 항상 켜져 있는 작은 프로그램입니다.
- Champion 모델 — 학습 중 토너먼트로 뽑은, 현재 가장 강한 봇 버전입니다.
- Client / 웹사이트 — 플레이어가 브라우저로 여는 페이지로, 보이는 메뉴를 만들어 냅니다.
- 시작(entrypoint) 스크립트 — 서버 프로그램이 부팅되는 순간 실행되는 몇 줄로, 여기서는 환경별 설정을 적어 넣는 데 쓰입니다.
- Environment variable(환경 변수) — 프로그램 밖에서 건네주는 이름 붙은 설정으로, 같은 프로그램이 테스트본과 실제본에서 다르게 동작하게 합니다.
- Config 기반 — 동작을 프로그램에 박아 넣지 않고, 바꿀 수 있는 설정 데이터로 제어하는 방식입니다.
- Validator — 들어오는 데이터를 검사해 형식이 잘못됐거나 안전하지 않은 것을 사용 전에 걸러내는 코드입니다.
- GitOps (FluxCD) — 실행 중인 시스템이 프로젝트의 Git 기록에 적힌 상태와 일치하도록 스스로 갱신하는 배포 방식입니다.
참고 자료
#1234— 해결 작업: 봇별 연결을 검증된 config 목록 하나와 번호 매긴 메뉴로 교체했습니다.#1233— 작업 도중 머지된 여섯 번째 상대(8차 세션 champion)로, 새 목록에 합쳐 넣었습니다.apps/board-game-client/src/shared/config/ai-bots.ts— parser, validator, 최신순 메뉴 정렬입니다.apps/board-game-client/build-config/docker-entrypoint.sh— 줄이 빠져 있던 시작 스크립트입니다.- YINSH 개요 — 게임을 해 본 적 없다면 규칙 참고용입니다.
- FluxCD image automation — 배포가 스스로 다시 빌드해 롤아웃하는 방식입니다.
AI 작업 노트
이 작업은 클러스터 repo 안에서 Claude Code가 처음부터 끝까지 진행했습니다. 진단의 핵심은 값싸게 먼저 얻었습니다. 코드를 고치기 전에 라이브 사이트가 실제로 내려주는 설정 파일을 가져오게 했고, 이로써 세 상대가 서버 계층이 아니라 웹사이트 계층에서 빠졌음을 증명했습니다. parser와 validator는 무언가에 연결하기 전에 단위 테스트가 있는 순수 함수로 먼저 작성하게 했습니다. 손이 필요했던 한 곳은 배포였습니다. 작업 도중 여섯 번째 상대가 main 브랜치에 머지되어, 남의 변경을 덮어쓰는 대신 그 위로 rebase해 새 봇을 목록에 합쳤습니다.
