복잡한 서비스 흐름을 안정적으로 운영 가능한 구조로 설계하고 구현해 온 백엔드 엔지니어입니다. 특히 상태 전이와 동시성, 트랜잭션 정합성이 중요한 시스템을 다루며, 다양한 예외 상황과 변화 속에서도 신뢰할 수 있게 동작하는 플랫폼을 만들어 왔습니다.
대규모 데이터 처리 환경에서는 병목을 계측 기반으로 분석하고, 코드와 데이터 구조를 함께 개선하며 성능과 안정성을 지속적으로 높여 왔습니다. 이러한 경험을 바탕으로 결제처럼 실패하면 안 되는 흐름을 더 정확하고 견고하게 만들고, 사용자와 서비스가 모두 신뢰할 수 있는 시스템을 만드는 데 기여하고자 합니다.
Career Projects
(주)드림어스컴퍼니(FLO) 재직 중(2021.12 – 2025.2) 수행한 프로젝트입니다. 전체 경력은 별도로 전달드리는 이력서를 참고해 주세요. 각 항목은 팀 규모와 기간, 맡은 역할, 그리고 바뀐 수치를 함께 봐 주세요.
복잡한 상태전이와 검증규칙을 가진 콘텐츠 플랫폼을 도메인 중심 아키텍처로 재설계하여 성능·정합성·확장성 개선
역할
백엔드 리딩(3인 팀) — 배포 전략·도메인 규칙 설계 및 공유, 공통 기능·ArchUnit 아키텍처 규칙·핵심 도메인 API 개발
기술
JavaSpring BootJPAMapstructMySQLRabbitMQArchUnit
화면 단위로 흩어진 API를 도메인 책임 기반으로 재설계해 응답 시간을 1.5초→300ms(80%) 단축했습니다. 시퀀스 채번 구조도 개선해 동시 요청 시 계약 생성 대기시간을 2분→10초로 줄였습니다. 직전 MDS 프로젝트에서 시니어가 만든 ArchUnit 계약을 따라 개발했던 경험을 바탕으로 이번엔 직접 아키텍처 규칙을 설계해 정적 분석 위반을 480건→75건(84%) 감소시켰습니다.
글로벌 유통 시스템(Music Distribution System) 구축
2023.1 – 2024.1(주)드림어스컴퍼니(FLO)
요약
글로벌 파트너사가 앨범을 등록하고 관리자가 검수하는 음원 유통 시스템을 구축하고, DDEX 기반 음원 플랫폼 자동 전송 기능을 구현하여 해외 플랫폼과의 직접 유통·정산 구조를 지원
역할
시니어 개발자가 설계한 ArchUnit 기반 공통 인터페이스 계약에 따라 콘텐츠·라이선스 관리 API 개발, 이후 계약관리까지 인수인계받아 정담당자로 유지보수
기술
JavaSpring BootJPASpring BatchMySQLSQSArchUnit
정산 대상 단위에 상태 플래그 기반 경량 락을 설계해 정산 중 메타데이터 변경을 사전 차단했습니다. 폴링 기반 배치를 Amazon SQS 이벤트 파이프라인으로 전환해 평균 처리 대기 시간을 3~5분→1분 내외로 단축했습니다.
대용량 배치·Excel 처리 최적화
2022.9 – 2022.12(주)드림어스컴퍼니(FLO)
요약
대용량 Excel 및 배치 처리 과정에서 발생하던 메모리 급증과 장기 트랜잭션 문제를 청크 기반 배치와 스트리밍 처리 구조로 개선하여 OOM을 제거하고 안정적인 배치 처리 환경을 구축
역할
단독 수행
기술
JavaSpring BatchJPAMySQLApache POI (SXSSF)
Heap Dump·GC 로그·Thread Dump 분석으로 원인을 확인한 뒤 배치 구조를 청크 기반 트랜잭션으로 전환해 메모리 피크를 3.8GB→1.6GB(58%)로 줄였습니다. Excel 생성도 SXSSF Streaming으로 전환해 메모리 사용량을 1.2GB→180MB(85%)로 줄였습니다.
지분율 시스템 트랜잭션 안정성 및 동시성 개선
2022.8 – 2022.11(주)드림어스컴퍼니(FLO)
요약
저작권·실연권 지분율을 대량으로 처리하는 정산 시스템에서 동시성 문제와 대량 배치 병목을 개선하여 데이터 정합성과 처리 성능을 향상
역할
단독 설계 및 구현
기술
JavaSpring BootRedisMySQLAWS
인덱스 재설계와 청크 기반 트랜잭션 전환으로 대량 삭제 처리 시간을 2시간→5분(24배) 단축했습니다. Redis 분산락을 SETNX+Lock Token 기반으로 재설계하고 상태 전이 모델을 도입해 Race Condition을 제거했으며, 병렬 파이프라인 적용으로 대량 등록 처리 시간도 40분→12분으로 줄였습니다.
Personal Projects
FLO 퇴사(2025.2) 후 육아를 전담하며 진행한 개인 프로젝트를 최신순으로 정리했습니다. 전부 GitHub에 공개되어 있으며, 표기된 커밋 수·파일 수는 2026-08-31 기준 리포지토리를 직접 확인한 수치입니다(ParityPay는 이후 시작). 이 밖에 redis-lite-java(2025.8) 등 작은 학습용 구현도 GitHub에 있습니다. 팀 성과가 아니므로, 어떤 설계 판단을 했고 그것을 무엇으로 확인했는지를 봐 주세요.
ParityPay — 결제·원장 백엔드
2026.9 – 진행중개인 프로젝트 · 1인 개발
소개
미리 충전한 잔액으로 결제하는 선불 지갑의 백엔드입니다. 결제 내역과 돈의 이동 기록인 원장을 함께 관리합니다. 같은 결제가 두 번 들어오거나 은행의 응답이 끊겨도 돈이 중복 차감되지 않도록 만드는 데 집중했습니다. 실제 금융기관 대신 은행과 결제대행사 역할을 하는 테스트 서버를 따로 만들어 장애 상황을 재현했습니다.
여러 결제가 같은 잔액을 읽은 뒤 각각 차감하면 보유 금액보다 많이 결제될 수 있습니다. 잔액이 충분할 때만 차감하는 UPDATE와 DB 잠금 등 세 가지 방식을 비교했습니다. 40건 중 20건만 승인할 수 있는 잔액을 넣고 동시에 요청해, 세 방식 모두 20건 승인과 최종 잔액 0을 확인했습니다.
처음에는 조건부 UPDATE가 더 느렸습니다. 확인해 보니 한쪽에만 JPA의 추가 처리 비용이 들어가 있었습니다. DB 호출 방식을 JDBC로 맞춰 다시 비교한 뒤 조건부 UPDATE를 선택했습니다.
돈을 차감하는 순서를 바꿔 대기 시간을 줄임
같은 지갑에 요청이 몰릴 때 DB 대기 표본의 약 84%가 잔액 잠금 때문이었습니다. 잔액을 먼저 차감한 뒤 결제 내역과 원장을 저장하는 동안 다른 요청이 계속 기다리고 있었습니다.
잔액 차감을 저장 작업의 마지막으로 옮겨 잠금 시간을 줄였습니다. 잔액이 부족하면 앞서 저장한 내용도 모두 취소합니다. 거절될 결제에도 일부 작업을 먼저 하는 비용은 있지만, 잠금을 오래 잡는 문제를 줄일 수 있었습니다.
개인 실험에서 초당 결제 처리량은 183.5→320.9건으로, 요청의 95%가 완료되는 시간은 274→144ms로 바뀌었습니다. 각 34초씩 3회 측정한 중앙값입니다. 비교용 대조군도 1.30배 변동했으므로, 개선 폭은 이 실험 조건에서의 결과로 봐야 합니다.
결제 응답이 끊겼을 때의 처리
응답이 없다고 결제 실패로 처리하지 않기
은행은 승인했는데 응답만 끊긴 경우, 실패로 보고 다시 결제하면 중복 차감될 수 있습니다. 응답이 없으면 결과 미확정 상태로 보관하고, 별도 작업이 은행에 상태를 조회해 결론을 내리도록 했습니다.
결제와 원장, 후속 처리 요청은 함께 저장합니다. 같은 요청이 다시 전달돼도 한 번만 반영하고, 잘못된 원장 기록은 삭제하는 대신 취소·보정 내역을 추가해 변경 과정을 남깁니다.
장애를 직접 만들어 확인
프로세스 종료, 외부 응답 유실, 중복·순서가 뒤바뀐 결제 알림 등 11개 장애 상황을 재현했습니다. 잔액과 원장이 맞는지, 같은 거래가 두 번 반영되지 않는지를 확인했습니다. 테스트 서버를 사용하는 개인 개발 환경의 결과입니다.
소프트웨어 질문을 한 번 올리고 끝나는 게시물이 아니라, 버전이 쌓이고 다른 질문과 연결되며 계속 진화하는 "Living Question"으로 다루는 개발자 지식 플랫폼
기술
KotlinSpring BootPostgreSQLMongoDBRedisReact
질문이 "죽지" 않게 만든다는 것
보통 Q&A 사이트에 올린 질문은 한 번 게시되면 그대로 고정됩니다. 나중에 상황이 바뀌거나 질문자가 더 잘 이해하게 돼도 질문 자체는 갱신되지 않고, 결국 비슷한 질문이 새 게시물로 계속 늘어납니다. quno는 반대로 질문 하나를 "버전이 있는 문서"처럼 다룹니다. 질문을 고치면 기존 글을 덮어쓰는 게 아니라 새 버전(QuestionVersion)을 이어 붙이고, 리비전 이력 전체를 남깁니다. 어느 버전에서 무엇이 달라졌는지는 LCS(최장 공통 부분열) 알고리즘으로 줄 단위 diff를 직접 계산해 보여주는데, 이건 git이 파일 변경을 비교할 때 쓰는 것과 같은 방식을 질문 텍스트에 적용한 것입니다.
Revision + Outbox Notification Flow
동시에 수정해도 꼬이지 않게
같은 질문을 여러 사람(혹은 같은 사람이 여러 탭)이 동시에 고치려고 하면 마지막 저장만 남고 나머지가 사라질 수 있습니다. 이를 막기 위해 리비전을 저장할 때 DB에 "지금 이 행은 내가 쓰고 있다"는 잠금(SELECT ... FOR UPDATE)을 걸고, 추가로 중복 저장 자체를 DB 유니크 제약으로 한 번 더 막는 이중 안전장치를 뒀습니다. 답변 채택이나 새 답변 등록처럼 다른 사람에게 알려야 하는 일은, 데이터가 실제로 저장되는 것과 같은 트랜잭션 안에서 "나중에 알림을 보내야 할 일" 목록(outbox_events)에 함께 기록해두고, 별도 스케줄러가 2초마다 그 목록을 확인해 처리하는 방식(Outbox 패턴)으로 알림 유실 없이 안정적으로 전달되게 했습니다. 이벤트 종류에 따라 추가로 알려야 할 대상(예: 새 답변 → 질문 작성자, 답변 채택 → 답변 작성자)과, 자기 자신이 일으킨 이벤트는 스스로에게 알리지 않는 예외 규칙도 함께 반영했습니다.
실제로 겪은 버그: 대소문자만 다른 태그
태그를 "Kotlin"과 "kotlin"처럼 대소문자만 다르게 두 번 등록하면 내부적으로는 같은 slug로 취급되어 DB 유니크 제약을 위반합니다. 그런데 이 예외가 스프링의 기본 에러 처리 경로(/error)로 넘어가면서, 보안 설정(SecurityConfig)이 인증 안 된 요청으로 오인해 401(인증 필요) 오류로 위장돼 나타나는 문제를 실제로 만났습니다. 원인을 추적해보니 "새 태그면 만들고 있으면 재사용한다"는 로직이 이름 그대로 비교하고 있었던 것이 문제였고, 이를 slug 기준으로 먼저 조회하도록 고치고 /error 경로를 인증 없이도 통과하도록 설정을 조정해 해결했습니다. 겉으로 보이는 오류 메시지(401)와 실제 원인(유니크 제약 위반)이 다를 수 있다는 걸 직접 부딪혀 확인한 사례입니다.
"Redis를 캐시로 쓸 줄 안다"와 "캐시 미스율이 치솟거나 특정 키에 요청이 몰릴 때(hot key) 어떻게 판단할지 안다" 사이에는 큰 차이가 있습니다. 이런 실무 감각은 책이나 강의로 채우기 어렵고, 회사에서 일부러 장애를 재현해줄 수도 없습니다. sys-drill은 이 공백을 통제된 시뮬레이션으로 반복 훈련할 수 있게 만든 플랫폼입니다.
직접 푸는 실습 챌린지
Circuit Breaker, 분산 락, 이벤트 버스, 큐, Rate Limiter, 재시도/백오프 6가지 주제를 실제로 코드로 구현해보는 챌린지를 만들었습니다. 각 챌린지는 단계별(대부분 4단계, Rate Limiter는 6단계)로 난이도가 올라가는 자동 테스트로 구성돼 있어, 앞 단계를 통과해야 다음 단계로 넘어갈 수 있습니다 — 예를 들어 분산 락 챌린지라면 "단순 락이 걸리는가"부터 시작해 "락 소유자가 죽었을 때 자동 해제되는가", "여러 프로세스가 동시에 요청해도 정확히 하나만 획득하는가"까지 단계적으로 검증합니다.
Rule + AI Hybrid Evaluation
판단 흔적을 전부 남긴다
시뮬레이션은 문제은행처럼 "문제 하나 풀고 끝"이 아니라 Scenario → Session → SimulationState라는 상태 머신으로 동작합니다. 사용자의 액션과 시나리오 이벤트가 다음 상태를 결정하고, 모든 의사결정은 Decision Log(applied_actions)에 기록됩니다. 이렇게 쌓인 기록은 나중에 리플레이(그때 무슨 판단을 했는지 되짚어보기), 포스트모템(장애 대응 훈련 이후 회고), 장기 약점 프로필(어떤 유형의 판단에서 반복적으로 틀리는지)을 만드는 데 그대로 재사용됩니다. Scenario·Rubric(채점 기준)·PromptTemplate에도 전부 버전을 강제해, 같은 세션을 나중에 다시 채점해도 같은 결과가 나오도록 재현성을 보장합니다.
Spring Framework/Boot를 블랙박스로 쓰지 않고, 공식 소스코드와 디버깅으로 "왜 이렇게 동작하는가"를 20주 로드맵을 따라 직접 검증하고 축소 재구현한 학습 저장소
기술
JavaKotlinSpring FrameworkSpring Boot
블랙박스로 안 쓰기로 했습니다
"IoC 컨테이너는 정확히 언제 빈을 만드는가", "AOP 프록시는 왜 같은 클래스 안에서 자기 메서드를 호출하면 안 걸리는가" 같은 질문은 Spring을 오래 써도 소스코드 수준으로 답하기 어려운 경우가 많습니다. spring-lab은 매주 하나씩 이런 질문을 골라, 정해둔 절차를 그대로 반복하는 방식으로 20주에 걸쳐 답을 채워나간 기록입니다.
주차별 반복 절차
20주에서 멈추지 않았습니다
원래 계획했던 20주(핵심 IoC·AOP·트랜잭션·MVC 16주 + Spring Boot 내부 4주)를 마친 뒤에도, 애플리케이션 이벤트·MVC 예외 처리 우선순위·트랜잭셔널 아웃박스까지 3주 분량을 더 진행하고, 미리 정해둔 32개 구현 과제 카탈로그를 전부 완주했습니다. 배운 걸 모두 모아 하나의 주문 처리 애플리케이션(Mini Order Platform)으로 통합하는 캡스톤까지 완주한 뒤에는, 카탈로그 범위를 완전히 벗어난 확장(상품 도메인 추가)까지 스스로 이어갔습니다. 217개 자동화 테스트가 전부 통과하는 상태를 계속 유지하며 진행했습니다.
반복해서 발견한 설계 원칙
20주를 거치며 서로 다른 주제에서 같은 원칙이 계속 다시 나타났습니다. 대표적으로 "정교한 판단"보다 "예측 가능한 순서"를 택하는 경향입니다 — 생성자가 여러 개고 어떤 걸 쓸지 애매하면 Spring은 "가장 그럴듯한" 것을 추측하는 대신 기본 생성자로 물러나고, 여러 처리기(HandlerMapping)가 등록되면 "더 적합한" 것을 고르지 않고 등록된 순서대로 첫 매칭을 채택합니다. 자동 설정끼리 순서를 정할 때도 마찬가지로, 서로 의존 관계가 없어도 선언적 힌트(@AutoConfiguration(before/after))만으로 순서를 정합니다. 이런 반복을 "왜 매번 같은 방식을 택할까"라는 질문으로 좇다 보면, 코드베이스가 커질수록 "왜 이번엔 다르게 골랐지"라는 디버깅 비용을 줄이는 게 똑똑한 자동 선택보다 더 중요하다는 Spring의 일관된 철학이 보입니다.
IoC/DI, 빈 생명주기, AOP, `@Transactional`, MVC 디스패처까지 Spring의 핵심 원리를 처음부터 직접 구현한 미니 프레임워크
기술
JavaKotlinGradle 멀티모듈H2
이해한 것을 직접 동작하게 만들기
spring-lab이 "공식 소스를 읽고 이해를 검증"하는 프로젝트라면, spring-lite는 "이해한 원리로 실제로 동작하는 프레임워크를 완성"하는 프로젝트입니다. 컴포넌트를 자동으로 찾아 등록하는 기능, 프록시를 이용해 메서드 호출 앞뒤에 로직을 끼워 넣는 AOP, 데이터베이스 작업을 하나의 단위로 묶고 실패하면 되돌리는 트랜잭션, HTTP 요청을 알맞은 코드로 연결해주는 MVC 구조까지, 겉으로 쓰는 애노테이션뿐 아니라 그 뒤에서 실제로 동작하는 부분까지 전부 직접 만들었습니다.
모듈로 나눠 관심사를 분리
실제 Spring도 그렇듯, 기능 하나를 통째로 만들지 않고 계층별로 모듈을 나눴습니다. 아래 모듈은 서로 의존 방향이 정해져 있어, 위쪽 모듈이 아래쪽 모듈을 가져다 쓰는 구조입니다.
모듈 의존 구조 (아래 → 위)
실제로 동작하는지 검증한 방법
H2 데이터베이스를 연결해 주문을 처리하는 예제 애플리케이션(example-app)을 만들어 8개 모듈이 실제로 맞물려 동작하는지 확인했고, JDBC/트랜잭션 계층은 H2 기반 통합 테스트로, MVC 계층은 자체 구현한 @SpringLiteTest·MockMvc 스타일 테스트로 검증했습니다. @Transactional이 걸린 테스트는 끝나면 자동으로 롤백되도록 지원해, 테스트가 서로 데이터를 오염시키지 않게 했습니다. 코드와 텍스트 설명만으로는 동작을 이해하기 어렵다고 판단해, 브라우저에서 바로 열어볼 수 있는 시각적 학습 도구(Visual Lab Hub)와 런타임 시퀀스 다이어그램 문서도 별도로 만들어 뒀습니다.
이력서 한 버전을 질문 트리로 확장해 모든 가지를 끝까지 연습하고 근거를 방어하도록 돕는 면접 준비 플랫폼. 프로필·이력서 등록 → 매일 문제 카드 수신 → 답변 제출·채점 → 낮은 점수는 재시도 큐, 높은 점수는 아카이브로 이동하는 학습 루프 제공
기술
KotlinSpring BootTypeScriptFeature-Sliced Design
무엇을 만들었나
매일 새로운 면접 질문 카드를 받고, 답을 제출하면 채점되고, 점수가 낮으면 나중에 다시 풀어보도록 대기열에 쌓이고, 점수가 높으면 완료 보관함으로 넘어가는 학습 루프를 만들었습니다. 영단어 암기 앱들이 흔히 쓰는 "아는 건 나중에, 모르는 건 자주 반복해서 보여준다"는 방식(간격 반복 학습)을 면접 준비에 적용한 것입니다.
도메인 중심 백엔드 + 계층형 프론트엔드
백엔드는 "질문", "답변", "이력서", "로그인" 같은 기능 영역(도메인)별로 코드를 완전히 나눴습니다. 각 영역은 다른 영역의 내부 구현을 몰라도 되고, 미리 정해둔 창구(인터페이스)로만 소통하도록 규칙을 정했습니다 — 한 영역을 고치다가 다른 영역이 실수로 같이 망가지는 일을 줄이기 위한 구조입니다. 프론트엔드는 FSD(Feature-Sliced Design)라는 방식을 따랐는데, 화면(pages) → 화면을 구성하는 조각(widgets) → 기능 단위(features) → 가장 작은 데이터 단위(entities) 순으로 계층을 나누고, 상위 계층은 하위 계층을 가져다 쓸 수 있지만 반대 방향으로는 의존할 수 없도록 규칙을 정한 구조입니다.
Backend Domains + Frontend FSD Layers
코드보다 설계 문서를 먼저 썼습니다
기능을 바로 코드로 옮기지 않고, "이 기능이 왜 필요한지" → "전체 구조" → "데이터베이스 설계" → "화면과 서버가 주고받는 약속(API)" → "구현 순서" → "완료 기준"까지 문서로 먼저 정리한 뒤 개발하는 방식으로 진행했습니다. 실제로 홈 화면, 문제 풀이 화면, 문제 상세·목록, 답변 작성기, 결과 분석, 실력 대시보드, 지난 기록 보관함, 이력서 버전 관리 등 MVP(최소 기능 제품) 범위의 화면들이 이 과정을 거쳐 실제로 동작하는 코드로 구현돼 있습니다.
채점을 규칙과 AI, 두 단계로 나눴습니다
답변이 합격선(PASS)인지 아닌지는 AI에 묻지 않고, 단어 수·문장 구조·기술/역할/회사 키워드 매칭 같은 규칙으로 직접 계산해 점수를 냅니다 — 같은 답변을 넣으면 항상 같은 점수가 나오도록 하기 위해서입니다. 반면 "어디가 강점이고 어디를 더 구체적으로 말해야 하는지" 같은 정성적인 설명은 정답이 하나로 정해져 있지 않기 때문에 AI(OpenAI)에게 맡겨 자연어 피드백을 생성하도록 역할을 나눴습니다.
대부분의 주식 앱은 "지금 가격이 얼마"라는 숫자와 차트만 보여줍니다. 왜 그 가격이 됐는지는 사용자가 직접 뉴스를 찾아봐야 합니다. monticker는 반대로 접근했습니다 — 가격이나 거래량이 평소와 다르게 움직이는 순간을 시스템이 먼저 알아채고, "지금 이 종목에 무슨 일이 일어나고 있다"는 걸 타임라인 형태로 보여주는 것을 목표로 삼았습니다.
평소와 다른 움직임을 어떻게 알아채나
여기서 쓴 방법은 EMA(지수이동평균)라는 통계 기법입니다. 쉽게 말하면 "최근 흐름의 평균"을 계속 갱신하면서, 지금 값이 그 평균에서 얼마나 벗어났는지를 보는 방식입니다. "가격이 5% 이상 움직이면 알림"처럼 고정된 숫자로 기준을 정하면, 시장 전체가 출렁이는 날엔 알림이 너무 많이 오고 조용한 날엔 진짜 중요한 신호도 놓치기 쉽습니다. 반면 EMA 기반으로 "평소" 자체를 계속 다시 계산하면, 시장 상황에 맞춰 기준이 자동으로 넓어지거나 좁아져서 이런 오차가 줄어듭니다.
Event Detection Pipeline
흐름은 4단계입니다. 먼저 별도로 동작하는 Worker가 1초마다 가격·거래량을 수집합니다. 그 값을 앞서 설명한 EMA 방식으로 "평소와 얼마나 다른가"를 판단하고, 이상하다고 판단되면 하나의 "이벤트"로 기록을 남깁니다. 마지막으로 이 이벤트들이 웹 화면의 타임라인에 시간 순서대로 표시돼, 사용자가 "이 종목이 언제 왜 움직였는지"를 한눈에 볼 수 있습니다.
개발은 Claude Code라는 AI 코딩 도구를 페어 프로그래머처럼 활용했습니다. 무엇을 어떤 순서로 만들지, 어떤 구조로 설계할지는 직접 판단했고, 실제 코드를 작성하는 속도를 높이는 데 AI의 도움을 받았습니다. 이 방식으로 백엔드(API·Worker)와 웹 화면 전체를 8주 만에 실제로 동작하는 수준까지 완성했습니다.
fixed-tick 결정론적 시뮬레이션 루프 위에 이동·전투·안개·건설·생산·연구·자원 채집까지 구현한 RTS(실시간 전략) 게임 코어. sim(규칙 엔진)/tools는 Kotlin, server/client는 Go로 재작성해 5개 모듈을 폴리글랏으로 분리 설계
기술
KotlinGoGradlelibGDXGitHub Actions
"결정론적"이 왜 중요한가
스타크래프트 같은 실시간 전략 게임을 여러 명이 함께 플레이하려면, 모든 사람의 화면에서 게임이 정확히 똑같이 진행돼야 합니다. 이걸 구현하는 대표적인 방법이 "결정론적 시뮬레이션"입니다 — 쉽게 말해 "똑같은 입력이 들어오면 언제 어디서 계산하든 항상 똑같은 결과가 나오도록" 게임 로직을 만드는 것입니다. 이렇게 만들어두면, 서버가 매 순간 전체 게임 상태를 모든 플레이어에게 전송할 필요 없이 "누가 언제 무슨 명령을 내렸는지"만 전달해도 모든 화면이 알아서 똑같은 결과를 계산해낼 수 있습니다. 이 프로젝트는 그 원리를 이동·전투·시야(안개)·건설·생산·연구·자원 채집 같은 실시간 전략 게임의 핵심 로직에 직접 적용해본 게임 코어입니다.
5-Module Architecture
5개 모듈로 나눈 이유
게임의 핵심 규칙(sim)과, 그 규칙을 실제로 돌리는 서버(server), 플레이어가 보고 조작하는 화면(client)을 서로 분리했습니다. 서버와 화면이 데이터를 주고받을 때 쓰는 "공통 언어"는 shared-protocol이라는 별도 모듈로 빼서 문서(rts-protocol-v1.schema.json)로 버전 관리했습니다 — 서버와 클라이언트를 각각 따로 업데이트해도 서로 대화가 어긋나지 않도록 하기 위해서입니다. 리플레이(지난 경기 재생), 벤치마크(성능 측정), 스냅샷(특정 시점 저장) 같은 개발 지원 도구는 tools 모듈로 따로 뒀습니다.
게임 코드만 짜고 끝내지 않았습니다
결정론이 실제로 깨지지 않는지 확인하는 자동 검증(같은 리플레이를 다시 돌렸을 때 결과가 정확히 같은지 매번 체크), 코드가 바뀔 때마다 자동으로 빌드·테스트하는 파이프라인(CI), 배포 파일이 중간에 손상되거나 변조되지 않았는지 확인하는 체크섬, 운영자가 문제 상황에서 참고할 수 있는 매뉴얼(런북)까지 실제 서비스에 준하는 과정을 함께 만들었습니다. 대신 그래픽·사운드 리소스, 스토리 모드, 완전한 온라인 대전 기능은 처음부터 범위 밖으로 정해뒀습니다 — 게임 엔진의 핵심 로직과 운영 프로세스를 제대로 만드는 데 집중하기 위해서입니다.
네트워크 계층을 Go로 다시 쓴 이유
처음엔 server·client를 포함한 5개 모듈 전부를 Kotlin으로 만들었지만, 이후 게임 규칙을 계산하는 sim은 Kotlin/JVM에 그대로 두고 네트워크를 주고받는 server·client만 Go로 다시 작성했습니다. "게임의 규칙을 판정하는 부분"과 "여러 플레이어의 연결을 관리하는 부분"이 서로 다른 종류의 작업이라는 점에 착안해, 각각에 더 맞는 언어로 나눈 것입니다.