게임 개발

마인크래프트 서버 플러그인

완료

지인 4~8인용 Paper 서버를 운영하며 화폐·상점·치장·미니게임 플러그인을 직접 개발했습니다. 12개 플러그인 42,500줄을 Core 하나에 인터페이스로 묶는 구조로 만들고, 운영이 끝난 뒤 그 구조가 실제로 어떻게 자랐는지 되짚었습니다.

  • Java 17
  • Paper API
  • Maven
  • SQLite
Core를 중심으로 한 플러그인 의존 관계도. Core 아래 Job과 Economy가 2차 허브를 이룬다
의도한 것은 Core 중심의 방사형이었지만, 실제로는 Job·Economy가 2차 허브가 된 3계층으로 자랐다

BACKGROUND

지인들과 스트리머 서버처럼 우리끼리의 마인크래프트 서버를 열어보자는 이야기를 예전부터 해 왔습니다. 마침 바이브 코딩을 막 알게 된 때라, 본격적인 게임 프로젝트를 시작하기 전에 이 방식이 어디까지 되는지 가늠해 볼 실험대로 삼기로 했습니다. 그래서 공개 플러그인 몇 개를 조합하는 대신, 처음부터 끝까지 직접 설계해 운영하는 것을 목표로 잡았습니다. 처음부터 설계하다 보니 공통 부분은 Core로 빼고 기능 플러그인들이 Core를 참조하는 방사형 구조로 출발했고, 서버는 개인 PC로 시작했다가 친구 소유의 서버를 빌려 24시간 운영으로 옮겼습니다.

WHAT I BUILT

Core + 기능별 플러그인으로 분리

공통 데이터와 인터페이스만 담당하는 Core 플러그인을 두고, 화폐·상점·치장·커스텀 아이템·미니게임을 각각 독립 플러그인으로 나눴습니다. 원칙은 "Core에만 의존한다"였고, 도메인이 실제로 얽히는 일부(상점→직업, 커스텀아이템→농사)에서는 직접 의존이 남았습니다. 대신 순환 의존은 끝까지 만들지 않아서 모듈 그래프는 단방향으로 유지됐습니다.

서비스 레지스트리로 플러그인 간 의존성 제거

상점 플러그인이 커스텀 아이템 플러그인을 직접 import하면 둘 중 하나만 빠져도 서버가 죽습니다. Core에 인터페이스(LampProvider 등)를 선언하고 구현체를 런타임에 등록하는 방식으로 바꿔서, 상점은 인터페이스만 알면 되고 구현 플러그인이 없으면 해당 기능만 조용히 비활성화됩니다. 최종적으로 Provider 인터페이스 17개를 선언했고, 등록 71곳·조회 135곳에서 쓰였습니다. 쓰이지 않고 남은 인터페이스는 하나도 없었습니다.

구조가 압력을 받은 지점

새 도메인을 추가할 때마다 Core의 인터페이스를 늘려야 했고, Core 버전이 오르면 12개 모듈의 의존 버전을 전부 맞춰야 했습니다. 그 비용 탓에 한 곳에서는 인터페이스를 추가하는 대신 클래스명을 문자열로 넣고 리플렉션으로 호출하는 우회를 택했습니다. 컴파일러가 잡아주지 못하는 코드가 생긴 겁니다. 올바른 해법을 비싸게 만들면 싼 우회로가 선택된다는 걸 내 코드에서 확인한 지점이었습니다.

플레이어 데이터 SQLite 통합 관리

플러그인마다 YAML 파일을 따로 쓰다 보니 같은 플레이어 정보가 여러 곳에 흩어졌습니다. Core의 PlayerDataManager로 SQLite에 일원화하고, 잔액 변경 같은 상태 변화는 이벤트로 발행해 다른 플러그인이 구독하도록 했습니다.

NMS/패킷 기반 NPC 구현

서버에 상주하는 NPC를 만들기 위해 패킷 단위로 가짜 플레이어 엔티티를 다뤘습니다. 마인크래프트 버전이 올라갈 때마다 내부 클래스명이 바뀌어서, 리플렉션으로 감싸고 버전 분기를 한 곳에 모으는 식으로 대응했습니다.

WHAT I LEARNED

"나중에 기능 추가하기 쉽게"를 이유로 미리 추상화하면 대부분 헛수고였습니다. 실제로 두 번째 사용처가 생겼을 때 인터페이스를 뽑는 편이 훨씬 정확했습니다.

플러그인 간 결합을 끊으니 하나를 껐다 켜도 서버 전체가 멈추지 않았습니다. 다만 원칙이 지켜진 건 결합을 끊기 쉬운 곳까지였고, 도메인이 실제로 얽힌 곳에서는 직접 의존이 남았습니다. 구조는 한 번 선언하고 끝나는 게 아니라 매번 지불하는 비용이라는 걸 배웠습니다.

내부 API(NMS)에 기대는 코드는 편하지만 버전 업그레이드마다 비용을 청구합니다. 격리해두지 않으면 그 비용이 코드 전체로 번집니다.

운영을 끝낸 뒤 코드를 다시 재봤습니다. 설계는 Core가 공통 기반만 갖는 것이었지만, 라이브 서비스를 운영하다 보니 새 기능을 급한 마음에 적당한 자리에 넣게 됐고 애매한 코드는 전부 Core로 흘러들었습니다. 부채는 하나로 요약됩니다 — Core가 기반과 인터페이스 창고와 잡동사니 서랍, 세 역할을 겸했습니다. 실제로 Core 3,284줄 중 공통 기반은 36%뿐이었습니다. 다시 만든다면 인터페이스만 담은 core-api를 따로 떼겠습니다. 거의 바뀌지 않는 모듈을 분리하는 것만으로 버전 동기화 비용의 대부분이 사라집니다.