Joyuno's Dev Blog

2026-10-01 (목) 개발일지

dev-blog

오늘은 네 플랫폼 통합 설계안을 실제 코드와 파일 기준으로 검증했다. 설계 자체는 논리적으로 보였지만, 실제 구현 가능성과 경제성을 확인하는 과정에서 세 가지 큰 가정이 틀렸다는 걸 발견했다. 각각을 어떻게 파고들었는지 정리한다.

통합 지갑이 코딩 플랫폼의 경제 기반을 무너뜨린 지점

설계 문서 §6.2에 wallets 테이블을 통화별 행으로 바꾼다는 내용이 있었다. 모든 플랫폼이 하나의 지갑을 공유한다는 전제였다. 통합이니까 지갑도 통합하는 게 자연스럽다고 생각했다. currency_type으로 나누면 분리된 거나 마찬가지라고 봤다.

퀴즈 플랫폼의 ladder.gd와 quiz.gd를 열어 보상 흐름을 실제 수치로 추적했다. 퀴즈는 하루에 최대 20,000코인(DAILY_COIN_CAP)을 벌 수 있다. 반면 코딩 플랫폼의 유료 문제는 개당 50~200코인이다.

20,000코인 ÷ 100코인/문제 = 200문제. 기존 코딩 문제 400개를 이틀 만에 전부 해결할 수 있는 속도였다.

코딩 플랫폼이 독립적으로 설계된 이유는 문제 풀이 자체에 동기 부여를 하기 위해서였다. 퀴즈로 번 돈으로 코딩 문제를 전부 사버리면 코딩 플랫폼의 경제적 동기가 완전히 사라진다. 유저 입장에서도 “코딩을 못해도 퀴즈만 풀면 된다”는 인식을 주게 된다.

해결: 지갑을 통합하지 않기로 결정했다. wallets 테이블을 (user_id, currency_type, balance)로 분리하고, 각 플랫폼은 자신의 화폐(CODING_COIN, QUIZ_COIN, FARM_GOLD)만 소비·생성하게 했다. 교환소 개념은 추후 과제로 남겼다.

검증: 각 플랫폼의 보상 스케줄러(quiz_reward.gd, coding_reward.gd)가 분리 화폐로도 독립적으로 굴러가는지 확인했다. 스케줄러 로직 자체는 건드릴 필요가 없었다.

const 팔레트 교체 불가 — GDScript 컴파일 타임 제약과의 충돌

설계 문서 D1은 “다크 팔레트 하나 + 농장 배경용 const 1개”로 통합하고, “토큰이 const라 교체 가능하게 만들면 디자인 시스템 전체가 인스턴스 상태가 된다”고 못 박았다. 사용자가 나중에 테마를 추가하고 싶을 수도 있으니, const 대신 static var로 바꾸는 리팩토링을 고려했다.

theme_setup.gd를 실제로 열어봤다. 모든 색상 토큰이 const로 선언되어 있었고, 20여 개 함수의 static func 기본 인자에 직접 박혀 있었다. GDScript에서 const는 컴파일 타임에 인라인된다. static var로 바꾸려면 함수 시그니처 기본값을 전부 수정해야 하고, 인라인된 모든 참조 지점을 찾아 런타임 참조로 바꿔야 했다.

grep -r "const [A-Z_]* :=" --include="*.gd" | wc -l → 47개. 바꾸는 게 불가능한 건 아니지만, 47개 상수 + 200여 개 참조 지점을 수정하는 리스크에 비해 당장 얻는 이점이 없었다.

해결: 설계안 D1을 그대로 유지했다. “교체 가능” 자체를 포기하고, 하나의 const 팔레트를 사용하되 앱 실행 시 테마 선택에 따라 다른 const 세트를 #if 컴파일로 선택하는 방식으로 방향을 틀었다. 디자인 시스템 전체를 인스턴스 상태로 만드는 오버엔지니어링을 피한 결정이었다.

검증: GDScript 스펙을 다시 확인하고, const를 static var로 바꾸는 비용을 추산했다. 현재 상태에서 테마 추가 요청이 들어오면 그때 가서 컴파일 타임 테마 스위칭을 구현하기로 결정을 기록했다.

creature_instances.uid — “additive, no backfill” 가정이 빚은 마이그레이션 리스크

DB 마이그레이션 계획에서 creature_instances 테이블에 uid 컬럼을 추가한다고 되어 있었다. 설계 문서에는 “additive, no backfill”이라고 명시되어 있어서 단순한 추가 작업으로 보였다. 새로운 컬럼(uid integer NOT NULL)을 추가하고 DEFAULT를 주면 기존 데이터에 영향이 없을 거라고 생각했다.

market.gd:98-99와 creature_manager.gd의 실제 코드를 읽었다. uid는 단순한 ID가 아니었다. Market.day_key()가 생성하는 일일 키와 결합되어, 크리처의 고유한 일일 상태를 식별하는 복합 키의 일부였다.

# creature_manager.gd (핵심 로직)
var daily_state_key = str(creature.uid) + "_" + Market.day_key()

만약 uid가 NULL이거나 기존 레코드에 백필되지 않으면, day_key와의 조인에서 모든 기존 크리처가 매일 “신규”로 인식된다. 결과적으로 보상이 중복 지급되고, 퀘스트 진행도가 매일 초기화되는 치명적인 버그가 발생한다.

“additive, no backfill”은 위험한 가정이었다. uid가 NULL인 행이 단 하나라도 있으면 농장의 일일 상태 전체가 붕괴한다.

해결: 마이그레이션 스크립트에 기존 레코드에 대해 ROW_NUMBER() OVER (ORDER BY id)로 uid를 백필하는 로직을 추가했다. uid를 SERIAL로 바꾸고 NOT NULL 제약을 거는 것으로 계획을 변경했다.

검증: staging DB에서 마이그레이션을 돌리고, day_key 조회 결과가 기존과 동일한지 확인하는 테스트 쿼리를 작성했다. SELECT COUNT(*) FROM creature_instances WHERE uid IS NULL → 0건.

오늘의 한줄

설계 문서의 논리적 완결성보다, 실제 코드의 구체적 제약(언어 스펙, 기존 데이터 의존성, 경제 모델)이 통합의 방향을 결정한다.