"부장님 죄송합니다. 저희 지금 심각한 회의 중입니다."
친구들이랑 합법적(?)으로 월급루팡 하면서 놀려고 가볍게 시작한 '망고의 라이어 게임'이 드디어 세상에 나왔다!
처음엔 "텍스트 웹 게임인데 뭐 어렵겠어" 했는데...
'실시간 멀티플레이'라는 요소를 얹는 순간부터 생각보다 산이 높았다....
이 글에서는 라이어 게임을 기획하게 된 계기부터 배포,
그리고 6인 멀티 테스트에서 서버를 뻗게 만들었던 수많은 트러블슈팅을 정리해보려고 한다.
🐈⬛깃허브 링크 :https://github.com/thal070909/mango-liar-game
🎮 게임 링크:https://buly.kr/90dH5RR (무료배포기간 종료..ㅜ 추후 재 배포 예정)
참고로 망고는 나의 소중한 반려묘의 이름이다.♥️😺
프로젝트 타임라인
- 💡 기획 및 개발 시작: [2026년 06월 20일]
- 기획 배경: 모바일 앱 설치 없이 사무실 모니터에 띄워놔도 위화감 없는(나름 엑셀 테마 지원)
텍스트 기반 실시간 웹 게임을 만들고 싶었다 - 기술 스택: Java 17, Spring Boot, WebSocket (STOMP/SockJS), JavaScript(Vanilla), Railway
- 🚀 1차 배포 완료: [2026년 7월 23일] — Railway로 클라우드 배포!
좌충우돌 트러블슈팅
혼자 창 여러 개 띄워놓고 테스트할 땐 완벽해 보였던 코드가,
실제 친구 6명이 모여서 플레이를 시작하자마자 처참하게 무너져 내렸다...
그동안 겪은 치명적인 버그들과 해결 과정을 공유한다...
1. 배포하자마자 마주한 붉은 글씨, CRASHED
로컬에서는 잘만 돌아가던 스프링부트 서버가 Railway에 올리기만 하면 기절했다.
- 원인: Railway의 자동 빌드 시스템(Nixpacks)이 Java 코드를 분석해서 실행 명령어를 알아서 만들어주는데, 이 조합이 엉성하게 꼬여서 애플리케이션을 못 띄우고 있었다
- 해결: 플랫폼 자동 시스템을 믿지 않기로 했다. Deploy 설정의 Custom Start Command에 java -jar build/libs/*.jar를 직접 박아서 최종 빌드된 jar를 명시적으로 실행하게 강제했다
교훈: 클라우드의 "알아서 해드려요"는 안 될 때를 대비해서, 실행 명령 정도는 내가 직접 쥐고 있는 게 마음 편하다.
2. 6인팟 테스트 대참사: 동시성(Concurrency)의 늪
1라운드 투표 직후, 다음 단계로 안 넘어가고 게임이 그대로 멈춰버렸다. 다 같이 "??? 왜 멈췄어???" 하는 순간의 공포란...
- 원인: 혼자 테스트할 땐 버튼을 순서대로 누르지만, 실제 6명은 제한 시간 찰나에 동시에 투표 버튼을 누른다.
이때 평범한 HashMap이랑 ArrayList에 데이터를 담다가
스레드 충돌(ConcurrentModificationException)이 터졌고, 서버 내부 타이머 스레드가 그대로 사망했다. - 해결: 글로벌 방 목록, 방 내부 플레이어 목록, 투표 결과까지 - 여러 스레드가 동시에 만지는 자료구조를
전부 ConcurrentHashMap과 CopyOnWriteArrayList로 갈아엎어서 스레드 세이프 환경을 만들었다.
교훈: "혼자 테스트에서 되는 것"과 "여럿이 동시에 눌러도 되는 것"은 완전히 다른 문제다.
멀티플레이는 처음부터 동시성을 깔고 설계하자.
3. 탈주 닌자들의 습격 (이탈자 예외 처리)
게임 도중 누가 브라우저 창을 닫아버리면,
그 사람의 설명 차례나 투표를 기다리느라 남은 사람들이 30초 동안 허공을 보며 멍을 때려야 했다.
- 해결: '탈주자 즉시 패스 시스템'을 백엔드에 도입했다.
SessionDisconnectEvent랑 /chat.leave 메시지를 감지해서,
나간 사람 턴이면 0.1초 만에 다음 턴으로 스킵(Fast-forward)하고,
투표 중이었다면 남은 인원의 표만으로 즉시 강제 집계하도록 흐름을 고쳤다
4. 눈에 안 보이는 '투명 팝업창' (Z-Index 데드락)
서버는 분명 다음 라운드로 넘어갔다고 채팅창에 로그를 뿌리는데,
유저 화면에서는 아무것도 클릭이 안 되고 얼어붙는 현상이 잦았다.
- 원인: 투표 동률이나 시간 초과로 라운드가 강제 종료될 때, 이전에 떠 있던 모달창(투표창, 변론창 등)이 안 닫히고 투명한 상태로 앞을 가로막고 있었다. 범인은 Z-Index...
- 해결: 게임 국면(Phase)이 전환될 때마다 이유 불문하고 떠 있는 모든 팝업을 강제 소각하는 closeAllGameModals() 클리너 함수를 만들어서 화면 멈춤을 원천 차단했다
교훈: 상태 전환 시점엔 "정상적으로 닫혔겠지"를 믿지 말고, 무조건 청소하고 시작하는 클리너를 두는 게 안전하다.
5. 관전자 난입과 테마 증발 사건 (프론트엔드 이슈)
- 관전자 난입 버그: 방장이 투표 개시를 기다리는데 늦게 온 친구(관전자)가 입장하면, 상태가 리렌더링되면서 방장 권한 버튼들이 통째로 증발했다. 게임 진행 상태(isPlaying)를 꼼꼼히 체크해서 진행 중인 게임에서는 버튼 락이 안 풀리게 막았다
- 테마 풀림 현상: 홈에서 적용한 다크/엑셀 모드가 게임방에 들어가면 풀렸다. 범인은 Thymeleaf의 <script th:inline="javascript"> — 이 속성이 바닐라 JS의 로컬 스토리지 읽기를 방해하고 있었다. 제거하고 로컬 스토리지 변수명을 완벽히 통일해서 해결!
6. 클라우드 환경에서 진짜 IP 찾기
유저 접속 로그(IP)를 남기고 싶었는데, 접속 IP가 전부 똑같이 찍혔다.
- 원인: Railway 같은 클라우드는 프록시 서버를 거쳐 들어오기 때문에 기본 getRemoteAddr()로는 진짜 IP를 알 수 없다
- 해결: 웹소켓 연결의 최초 진입점인 HandshakeInterceptor를 구현해서 X-Forwarded-For, Proxy-Client-IP 같은 헤더를 분석해 찐 IP를 낚아채고 세션에 저장하는 로직을 넣었다
✍️ 마무리하며
단순해 보이는 텍스트 웹 게임도 '실시간'과 '멀티플레이'가 결합되는 순간,
동시성 제어부터 비동기 상태 관리, 예외 처리까지...
백엔드와 프론트를 아우르는 깊은 고민이 필요하다는 걸 뼈저리게 느꼈다.
특히 클라이언트와 서버의 타이밍이 0.1초만 어긋나도 터지는 데드락을 추적하고 잡아가는 과정은 힘들었지만...
그게 다 경험치다.
인프라도 단단해졌으니, 이제 친구들이랑 완벽한 눈치 게임을 즐길 일만 남았다.
부장님, 저 오늘도 심각한 회의 있어서요..^^;;!!!
'끄적끄적 개인 개발' 카테고리의 다른 글
| 토스 미니앱 "우산챙겨!" 기획부터 검수 요청까지 🌂 (0) | 2026.07.23 |
|---|