· 16 min read
루니아 원정대 — 5년 후의 회고
루니아 원정대 — 5년 전의 판단들을 돌아보며.

루니아 원정대

루니아 원정대는 올엠(allm)에서 만든 모바일 액션 던전 크롤러다. PC MMO였던 루니아 온라인의 IP를 모바일로 가져온 프로젝트로, 2019년 7월부터 2020년 12월까지 약 1년 반 동안 개발했다. 1–3인 협동 플레이, 랜덤 생성 스테이지, 로그라이크 요소가 있는 웨이브 전투 기반의 게임이었다.
프로그래밍 리드로서 핵심 아키텍처 대부분을 직접 설계했다. 커스텀 ECS, 커맨드 기반 동기화, 5종 분산 서버 구조, 상태 직렬화와 재접속 시스템 등. 올엠에서의 마지막 프로젝트이기도 했다 — 그 전에 크리티카 온라인(2016–2017)과 캡슐몬 파이트(2017–2019)를 거치며 쌓은 경험이 이 프로젝트에 녹아들었다.
지금은 서비스가 종료된 상태다.
5년 전의 글
2020년 10월, 블로그에 “개발중인 프로젝트 구성안”이라는 시리즈를 4편 올렸다. 프로젝트 이름도 밝히지 않고, 아키텍처 결정들만 담담하게 적었다. 회사 프로젝트라 조심스러웠고, 당시엔 아직 개발 중이었으니까.
최근 당시 작성해뒀던 설계 문서와 기록들을 정리하다가, 예전 블로그 글도 다시 읽게 됐다. 5년 전의 나는 어떤 생각으로 이런 결정들을 내렸는지, 그리고 그 판단들이 지금의 나에겐 어떻게 보이는지 — 기록을 따라가며 회고해보려 한다.
최우선은 “생산성”이었다
당시 블로그 첫 번째 글에 이렇게 적었다.
이번 프로젝트 리드를 하며 아키텍쳐를 잡을 때 최우선 사항은 생산성이었다.
유려한 아키텍처, 강력한 보안, 고성능 — 다 중요하지만 전부 충족시키긴 현실적으로 어려웠다. 개발자가 평균 3–4명인 소규모 팀에, 기획은 계속 바뀌고, 일정은 짧았다. 선택과 집중이 필요했고, 생산성을 택했다.
결과적으로 맞는 판단이었다고 생각한다. 팀 규모에 비해 생산성이 잘 나왔다. 특히 서버-클라이언트 프로토콜 변경이나 기능 구현에서 — 게임 로직은 서버/클라 구분 없이 한 번만 작성하면 됐고, 아웃게임 쪽도 비슷한 수혜를 봤다.
5년이 지난 지금도 본질적으로 같은 방향을 추구하고 있다. 당시엔 “코드와 구조의 유려함보다 빠르게 만들고 고치는 것”에 집중했고, 지금은 “기술보다 서비스 자체”에 집중하려 한다. AI가 구현을 너무 잘 해주는 시대가 오면서, 설계와 판단에 더 집중할 수 있게 된 것도 있다. 결국 본질에 집중하자는 같은 맥락이다.
기술 스택 — C# 통일과 라이브러리 선택
프로젝트를 거칠수록 기술 스택이 수렴했다.
크리티카는 자체 엔진에 전부 C++이라 모듈 공유가 잘 됐지만, Unity로 넘어오면서 상황이 달라졌다. 캡슐몬은 클라(C#), 로비(Java), 게임서버(C#)로 파편화되면서 언어가 다르면 모델 공유조차 어렵다는 걸 체감했다. 루니아 원정대에서는 클라이언트, 서버, 관리툴, 더미 클라이언트 전부 C#으로 통일하고, Submodule로 소스를 통째로 공유했다. 전 프로젝트에서 dll 빌드 → 복사하던 걸 소스 직접 공유로 바꾸면서 빌드가 2번에서 1번으로 줄었다.
직렬화는 MessagePack을 택했다. Protobuf의 IDL-Compile 워크플로우가 귀찮았고, C#을 통일한 이상 IDL 자체가 필요 없었다. 네트워킹은 DotNetty — deprecated인 걸 알면서 썼다. 블로그에도 “신규 프로젝트에선 쓰지 말아야 할 듯 하다”라고 적어놓고. 캡슐몬에서 잘 써봤고 서버 프레임워크를 새로 짤 여유가 없었다. 현실적 선택이었고, 실제로 큰 문제는 없었다. 중간에 DotNetty 소스를 Unity에도 넣어서 네트워킹 레이어를 완전히 통일하기도 했다.
5년 후 OFF를 만들고 있는 지금 — MessagePack은 MemoryPack으로, DotNetty는 SuperSocket으로. “C# 코드가 곧 스키마”라는 방향은 여전하다.
커스텀 ECS를 직접 만든 이유
게임 로직을 클라이언트와 서버에서 완전히 공유해야 했다. Unity ECS(DOTS)를 공부하고 고려도 해봤지만, 두 가지 이유로 선택하지 않았다. 첫째, DOTS는 패러다임 자체가 너무 어렵고 다른 개발자들이 이해하기 힘들었다. 둘째, Unity 종속적이라 서버에서 돌릴 수 없었다.
그래서 직접 만들었다. 리플렉션 기반으로 시스템을 자동 발견하고, 턴 기반 시뮬레이션을 돌리는 구조. 직접 만든 것 자체는 후회하지 않는다. 다만 솔직히 말하면 — 그냥 객체지향으로 짜는 것과 크게 다르지 않은 구조가 됐다. 아키타입, 청크 같은 DOD(Data-Oriented Design)의 핵심 개념이 없었으니까. ECS라는 이름을 붙였지만 DOD의 성능 이점은 없었다.
5년 후 — OFF에서는 Arch ECS라는 오픈소스를 채택했다. 이건 진짜 DOD 기반이라 좀 어렵긴 하지만, 직접 만드는 대신 잘 만들어진 걸 가져다 쓰는 쪽으로 생각이 바뀌었다. 그리고 그때 직접 만들어본 경험이 있었기에, 뭘 선택해야 하는지 판단할 수 있었다.
코드 생성에 대한 집착
블로그 4편 전체가 코드 생성 이야기였다. MessagePack AOT, 게임 상태 시리얼라이저, 다형 타입 정의, EF DB-First — 반복 작업을 자동화하기 위해 적극적으로 코드 생성을 도입했다.
특히 게임 데이터 쪽 고민이 깊었다. 100개가 넘는 데이터 타입(캐릭터, 스킬, 버프, 몬스터 등)을 JSON으로 관리했는데, 다형성 처리를 위해 @type 필드 + 타입 자동 수집 제너레이터 + 커스텀 데이터 에디터까지 만들었다. 동작은 했지만, 수동 타입 등록, 런타임 리플렉션, Unity 종속이라는 한계가 있었다.
당시 글을 이렇게 끝냈다.
아직까지는 Code 생성에 대한 큰 단점을 느껴보지 못했다. 계속해서 더 Code 생성을 도입할 만한 부분을 찾고 있다.
그 답이 5년 후에 나왔다. 이 프로젝트에서 겪었던 데이터 관리의 고통을 해결하기 위해 Datra를 만들었다 — C# Source Generator 기반으로 어트리뷰트 하나면 컴파일 타임에 전부 생성된다. 리플렉션도, Unity 종속도 없다.
5종 서버 분산 구조
기록을 보면 Lobby, Match, Battle, Social, Relay 5종의 서버가 있었다. 처음부터 5개는 아니었고, 배틀 서버에서 시작해서 필요에 따라 하나씩 늘어났다. 로비는 처음부터 계획에 있었고, 나머지는 기능이 추가되면서 분리됐다.
- Lobby — 게임의 입구. 계정, 로그인, 장비, 상점, 메일. 여기서 매치를 요청하면 Match로 넘어간다
- Match — 매치메이킹. 플레이어를 모아서 적절한 Battle 서버에 배정한다. 실제 게임은 돌리지 않는다
- Battle — 실제 전투가 일어나는 곳. 커맨드 기반 시뮬레이션, 재접속, 리플레이 녹화
- Social — 친구, 길드, 파티, 채팅
- Relay — 서버 간 메시지 중계. 게임 로직 없이 순수 패싱만
특이했던 건 서버 프로젝트가 하나였다는 점이다. 로컬 개발 환경에서는 프로세스 하나만 띄우면 모든 서버 기능이 전부 올라왔다. 서버를 따로따로 띄우고, 고치고, 재시작하는 게 기존에 불편했기 때문이다. 디버깅할 때 별도 프로세스로 나뉘어 있으면 추적이 어려운 것도 있었고. 배포 환경에서만 특정 서버 기능을 선택적으로 켜는 방식이었다.
커맨드 기반 동기화
게임 로직을 클라이언트와 서버에서 공유하되, “누가 실행하는가”를 Authority로 결정하는 구조였다.
게임 로직 코드는 Bridge가 뭔지 모른다. 그냥 bridge.Put(command)를 호출할 뿐이다. 싱글이든 멀티든, 클라이언트든 서버든 같은 코드가 돌아간다. 환경에 따라 Bridge 구현체만 바뀐다.
게임 상태 직렬화와 재접속

모바일 게임이라 네트워크가 불안정한 상황이 잦았다. 재접속 시스템은 개인적으로 당연히 있어야 한다고 생각했다.
핵심은 프레임워크 레벨에서 미리 깔아둔 것이다. 게임 상태를 스냅샷으로 저장하고 복원하는 구조를 처음부터 설계에 포함시켰다. 재접속이나 리플레이 같은 기능은 나중에 얹었는데, 그때 프레임워크가 이미 준비되어 있으니 훨씬 수월했다.
커맨드 기반 결정론적 시뮬레이션이었기에 — 같은 커맨드를 같은 순서로 실행하면 같은 결과가 나오므로 — 이런 접근이 가능했다.
리플레이와 안티핵
리플레이도 같은 프레임워크 위에 만들었다. 라이브 환경에서 플레이 기록을 전부 AWS(S3 + DynamoDB)에 저장해뒀다. 리플레이 데이터를 다운로드해서 Unity 클라이언트에 넣으면 해당 플레이를 그대로 재현할 수 있었고, 이걸 활용해서 유저의 플레이 기록을 검토하거나 핵·부정행위를 검증하는 환경도 구축했다.
이 프로젝트에서 OFF로
5년 사이에 바뀐 것들을 정리하면 이렇다.
| 루니아 원정대 (2020) | OFF (2026) |
|---|---|
| DotNetty | SuperSocket |
| MessagePack | MemoryPack |
| 커스텀 ECS | Arch ECS |
| 역할별 5서버 | 공간별 무한 분산 |
| Unity 종속 | 엔진 독립 |
| Submodule 공유 | 처음부터 .NET 단일 솔루션 |
당시엔 “이렇게 할 수밖에 없었다”고 생각한 것들이, 지금은 “그래서 이렇게 바꿨다”가 되어 있다. DotNetty가 deprecated인 걸 알면서 썼던 경험이 SuperSocket을 선택하게 했고, 커스텀 ECS의 한계를 체감했기에 Arch ECS를 채택할 수 있었다. 돌아가기보다 앞으로 간 것이다.
맺으며
오래된 기록을 다시 보는 건 묘한 경험이다. 기술적으로는 당시 기대 이상으로 많은 걸 준비했다고 생각한다. 프레임워크 레벨에서 재접속과 리플레이를 미리 깔아둔 것, 언어 통일로 생산성을 높인 것, 코드 생성을 적극 도입한 것 — 이런 판단들은 지금 봐도 맞았다.
아쉬운 점이 있다면, 기술에 너무 집중했다는 것이다. 개발자로서 개발에 몰두했지만, 프로젝트 후반에는 개발할 것이 그렇게 많지 않았다. 그때 좀 더 열정적으로 게임 자체 — 재미 요소라든가, 기획 방향이라든가 — 에 다양한 측면에서 기여했으면 좋았을 것 같다.
5년 전이나 지금이나 변하지 않은 건 있다. 본질에 집중하자는 것. 당시엔 그게 “생산성”이었고, 지금은 “서비스 자체”다. 도구와 환경은 바뀌어도 방향은 같다.

