- Riot Games는 기술 부채를 미래 개발자가 비용을 치르게 될 코드나 데이터로 보고, League of Legends 개발 사례를 통해 부채를 판단하는 공통 언어를 정리함
- 평가 기준은 영향도, 수정 비용, 전염성 세 가지이며, 특히 전염성은 부채가 시간이 지나며 다른 시스템·데이터·개발 관행으로 퍼지는 정도를 가리킴
- 부채 유형은 내부 구현에 갇힌 Local Debt, 두 시스템을 임시 연결한 MacGyver Debt, 깊은 가정이 구조에 박힌 Foundational Debt, 결함 위에 콘텐츠가 쌓인 Data Debt로 나뉨
- Jarvan의 Cataclysm, std::string과 AString 병행, BlockBuilder의 Lua 사용, block parameter naming bug가 각 유형의 구체적 사례로 사용됨
- 전염성이 낮은 부채는 오래 남겨도 괜찮을 수 있지만, 전염성이 높은 부채는 시간이 지날수록 수정 비용과 영향이 커져 조기에 확산 경로를 끊어야 함
기술 부채를 판단하는 세 가지 축
- 기술 부채는 “미래 개발자가 비용을 치르게 될 코드나 데이터”임
- 특정 부채를 지금 고칠지, 나중에 고칠지, 현실적으로 남겨둘지 판단하려면 공통 측정 기준이 필요함
- Riot은 기술 부채를 세 축으로 평가함
- impact: 플레이어와 개발자에게 미치는 영향
- fix cost: 수정에 필요한 시간과 배포 위험
- contagion: 그대로 둘 때 문제가 얼마나 퍼지는지
-
impact: 플레이어와 개발자에게 드러나는 비용
- 플레이어에게는 버그, 누락된 기능, 예상 밖 동작으로 나타남
- 개발자에게는 구현 지연, 워크플로 방해, 기억해야 할 불필요한 세부사항으로 누적됨
- 여기서 개발자는 엔지니어뿐 아니라 디자이너, VFX 아티스트 등 게임 제작에 관여하는 직군을 포함함
- 어떤 부채는 엔지니어의 새 코드 작성을 막고, 어떤 부채는 디자이너의 새 스크립트 작성이나 VFX 아티스트의 새 파티클 제작을 방해함
-
fix cost: 구현 시간과 배포 위험
- 수정 비용에는 실제 개발 시간뿐 아니라 수정사항을 배포할 때 생기는 위험도 포함됨
- 단일 함수의 단순 오류는 몇 분 만에 고칠 수 있지만, 게임 전체 코드 라인에 영향을 주는 깊은 가정은 수주 또는 수개월이 걸릴 수 있음
- “잘못된” 시스템도 이미 좋은 게임을 만드는 도구로 쓰이고 있을 수 있어, 고치는 순간 기존 콘텐츠가 깨질 수 있음
- 예를 들어 스크립팅 엔진의 오류 처리 방식이나 파티클 생성 시간 계산 방식을 바꾸면 140명 이상 챔피언의 500개 이상 스킬에 영향을 줄 수 있음
-
contagion: 시간이 지날수록 퍼지는 정도
- contagion은 기술 부채를 그대로 둘 때 다른 시스템·데이터·개발 방식으로 얼마나 전파되는지를 뜻함
- 전파는 문제가 있는 시스템과의 인터페이스, 그 위에 쌓인 데이터의 복사/붙여넣기, 새 기능 구현 방식의 변화로 발생함
- 잘 격리된 부채는 나중에 고쳐도 지금 고치는 것과 비용 차이가 크지 않음
- 전염성이 높은 부채는 시간이 지날수록 더 고치기 어려워지고, 더 많은 시스템이 핵심 타협에 감염될수록 영향도 함께 커짐
Local Debt: 내부만 지저분한 블랙박스
- Local Debt는 고전적인 블랙박스 프로그래밍 모델과 비슷함
- 외부에서 보면 시스템은 안정적으로 동작하지만, 내부 구현은 끔찍하거나 혼란스러울 수 있음
- 예: 스킬, 네트워크 계층, 스크립트 엔진
- 주변 시스템을 개발할 때 내부 부채를 의식할 필요가 없다면 전염성은 낮은 편임
-
현실 세계 비유: 인간의 눈
- 인간의 눈은 구조상 이미지를 거꾸로 받아들이고, 망막 신경은 각 눈의 중앙 근처에 맹점을 만듦
- 뇌의 시각 중추가 데이터를 뒤집고 맹점을 채워 나머지 뇌가 “올바른” 이미지와 상호작용하게 함
- 이 특이점은 눈과 시신경 시스템에 국소화되어 있고 다른 시스템이 쉽게 피할 수 있어 “충분히 좋은” 상태임
-
League 사례: Jarvan의 Cataclysm
- Jarvan의 Cataclysm은 현재까지도 minion으로 만들어져 있음
- 디자이너는 특정 위치나 위치 집합에 게임플레이 효과를 붙일 때 “invisible minion”을 생성하는 도구를 사용할 수 있음
- RiotXypherous는 Reddit 댓글에서 여기서 말하는 “minion”을 설명함
- 이런 게임 오브젝트는 스크립트 로직을 추적하고 실행하는 안정적이고 잘 이해된 방법임
- Jarvan의 벽은 플레이어가 빠져나가지 못하게 하려면 정확히 24개의 minion이 필요함
- 예전에는 12개였지만 플레이어가 가끔 벽 사이로 빠져나가 Riot Exgeniar가 24개로 늘림
- 대안은 Cataclysm의 pathability를 제어하는 단일 논리 조각인 ring-terrain 구조이며, 로직을 정리하고 계산 비용을 약간 줄일 수 있음
-
Cataclysm의 평가
- impact: 1/5
- 벽이 minion으로 만들어졌다는 사실은 새 콘텐츠를 만드는 다른 개발자에게 거의 영향을 주지 않음
- “Jarvan Ult Hitch”는 이 부채와 누락된 auto-attack 정의를 읽으려던 로딩 버그가 맞물린 결과였음
- fix cost: 2/5
- 현재는 새 코드 없이 합성 도형으로 커스텀 지오메트리를 만들 수 없음
- ring 형태의 “area trigger”를 만들려면 ring 충돌 계산을 위한 전용 수학 코드가 필요함
- Riot은 다른 목적을 위해 Constructive Solid Geometry를 탐색 중이며, 이것이 수정 비용을 크게 낮출 수 있음
- contagion: 1/5
- 기능 개발 시 Jarvan 벽 구현을 고려할 필요가 없어 잘 격리되어 있음
- 전염 위험은 다른 디자이너가 이 구현을 새 챔피언에 복사/붙여넣기하는 경우이며, 실제로 간혹 발생했음
- 구현 문제로서 Cataclysm의 잠재적 확산은 낮고 잘 이해되어 있음
- impact: 1/5
-
Local Debt를 다루는 방식
- Local Debt의 전형적인 특징은 낮은 contagion 점수임
- impact가 fix cost보다 높으면 좋은 시민 역할을 하는 개발자가 오래 지나기 전에 고치는 경향이 있음
- 정말 전염성이 없다면 필요한 만큼 오래 남겨도 안전함
- 엔지니어의 완벽주의를 자극하지만 충분히 넓은 영향이 없는 Local Debt에 즉시 달려드는 것은 큰 실수 중 하나임
- 변경 범위가 국소적이어서 수정 확인과 회귀 테스트가 보통 쉬움
- 최근 수정된 예시에는 inhibitor 관련 버그, Janna’s Monsoon, Tear of the Goddess가 포함됨
- 특정 상황에서 inhibitor가 챔피언을 0,0,0 좌표로 pathing하게 만들던 버그
- Janna’s Monsoon이 spell shield를 무시하던 문제
- Tear of the Goddess가 마나 없는 시전에 중첩되던 문제
MacGyver Debt: 두 시스템을 덕트 테이프로 붙인 상태
- MacGyver Debt는 1980년대 중반 TV 쇼 MacGyver에서 이름을 따온 유형임
- 기술 부채 맥락에서는 충돌하는 두 시스템이 코드베이스 전반의 인터페이스 지점에서 “덕트 테이프”로 붙어 있는 상태를 뜻함
-
현실 세계 비유: Seattle
- Seattle에는 과거 각자 자체 격자를 가진 두 개의 경쟁 정착지가 있었음
- 두 정착지가 현대의 Emerald City로 성장하면서 약간 다른 격자가 합쳐졌고, 어색한 모양의 블록과 건물, 비효율적인 공간 사용이 생김
-
League 사례: std::string과 AString
- League 코드베이스에는 C++ std::string과 Riot의 커스텀 AString 클래스가 함께 존재함
- 둘 다 문자열을 저장, 수정, 전달하는 방법임
- Riot은 std::string이 많은 “숨겨진” 메모리 할당과 성능 비용을 유발하고, 나쁜 코드를 쓰기 쉽게 만든다고 봄
- AString은 신중한 메모리 관리를 염두에 두고 설계됨
- 대체 전략은 두 시스템을 함께 두고
.c_str()와.Get()을 통해 서로 변환할 수 있게 하는 방식이었음 - AString에는 사용성을 높이는 개선이 추가됐고, 엔지니어가 코드를 변경할 때 자율적으로 std::string을 교체하도록 권장됨
- 이 방식으로 std::string은 천천히 단계적으로 사라지고, 두 시스템 사이의 “덕트 테이프” 인터페이스도 코드 정리와 함께 줄어듦
-
std::string vs AString의 평가
- impact: 2/5
- std::string에서 발생하던 고영향 할당 대부분은 profiling을 통해 이미 제거됨
- 현재 주요 비용은 한 시스템에서 다른 시스템으로 변환할 때 드는 작은 정신적 전환 비용임
- fix cost: 3/5
- AString 전환은 단순 find-and-replace가 아님
- AString에는 stack 메모리에 초기 할당하는 AStackString, static string 참조용 ARefString, heap 할당 기반 AString 같은 목적별 변형이 있음
- 올바른 교체를 위해 사람이 각 지점을 직접 보고 판단해야 하며, 기존 시스템을 단계적으로 제거하는 과정은 길고 느릴 것임
- contagion: -2/5
- AString을 std::string보다 사용하기 쉽게 만들어 contagion을 유리한 방향으로 뒤집음
- 엔지니어가 게임 코드 변경을 체크인할 때마다 AString이 더 퍼질 가능성이 생김
- impact: 2/5
-
MacGyver Debt를 고치는 방식
- MacGyver Debt의 큰 비용은 경계를 넘을 때 모드를 전환해야 하는 지적 비용인 경우가 많음
- 버그나 기능이 “wrong” system에 있어서 막히면, 목표 지점을 “right” system으로 옮기는 작업은 대체로 직접적임
- 새 시스템과 기존 시스템의 상대적 contagion이 핵심 지표임
- 새 시스템 쪽이 더 전염되도록 균형을 뒤집으면 더 나은 시스템이 결국 이김
- 전역적으로 더 나은 시스템이 로컬 수준에서도 더 매력적이게 만들어야 함
- 시간에 쫓기는 엔지니어가 일상 작업 중 탐욕적 최적화를 하면서도 원하는 최종 상태 쪽을 선택하면 좋은 방향으로 가고 있는 것임
- 다른 접근은 대규모 brute-force refactor이며, 시스템들이 얼마나 가깝게 매핑되는지에 따라 clever regex로 일부 또는 전체를 고칠 수도 있음
Foundational Debt: 깊은 가정이 전체 구조에 박힌 상태
- Foundational Debt는 시스템 깊숙한 곳의 어떤 가정이 전체 동작 방식에 구워져 들어간 상태임
- 시스템의 경험 많은 사용자는 이를 “그냥 그런 것”으로 보기 때문에 알아차리기 어려울 수 있음
-
현실 세계 비유: United States Customary Units
- 미국에서 자란 사람은 1마일이 5,280피트, 1쿼트가 2파인트, 1갤런이 4쿼트 같은 변환을 외우게 됨
- 미국 정부는 metric 전환을 여러 번 고려했지만, 여전히 Système International을 공식 측정 시스템으로 채택하지 않은 7개국 중 하나임
- 이 부채는 도로 표지판, 레시피, 초등학교, 사람의 머릿속에 구워져 있음
-
League 사례: BlockBuilder와 Lua
- Riot이 다뤄온 큰 Foundational Debt 사례에는 Determinism in League of Legends와 Game Data Server가 있음
- League의 Lua scripting language 사용도 Foundational Debt 예시임
- League 디자이너는 BlockBuilder라는 도구로 기능 블록을 이어 붙여 복잡한 동작을 만듦
- 기능 블록에는 점 사이 거리 구하기, minion 생성, damage 처리, 다양한 script flow control이 포함됨
- 디자이너가 고르는 연산 집합은 다양하지만 제한되어 있고, 각 연산의 매개변수도 제약됨
- League of Legends 초기에는 블록과 매개변수를 데이터에 맞는 단순하고 제약된 형식으로 저장하지 않고, 강력하지만 이 목적에는 지나치게 복잡한 Lua 언어의 arrays와 tables로 저장하기로 결정함
- 이후 약 10년의 게임 개발이 그 기반 위에서 진행됐고, Lua object 조작은 엔진에서 가장 흔한 작업 중 하나가 됨
-
BlockBuilder Lua의 평가
- impact: 4/5
- Lua와 해당 문제 공간의 불일치는 많은 비용을 유발함
- BlockBuilder 로직의 각 frame마다 callstack이 약 6개의 marshalling stack frame으로 오염됨
- marshalling 작업은 server CPU 사용량 측면에서 저렴하지 않음
- script 변경 diff를 읽는 일이 불필요하게 어려움
- 기능을 파악하기 위해 script file을 parsing/searching하려면 Lua 언어에 대한 꽤 깊은 이해가 필요함
- fix cost: 4/5
- Lua가 엔진에 깊게 박혀 있어 제거가 어려움
- 현재 제안 중 하나는 Lua object처럼 동작하지만 내부적으로는 훨씬 단순한 struct인 wrapper class를 만들어 scripting 내부를 더 적합한 형태로 천천히 바꾸는 것임
- 어떤 접근을 하든 신중하고 사려 깊게 진행해야 함
- contagion: 4/5
- 시스템이 scripting과 맞닿을 때마다 그 시스템은 Lua backend의 연산과 요구사항에 의해 형성됨
- scripting은 LoL의 핵심 logic 단위임
- Riot은 평균 약 3~4일마다 새 Building Block을 추가하며, 각 Building Block은 Lua object를 직접 조작함
- Lua를 더 오래 대체하지 않을수록 Lua를 대체하기가 더 어려워짐
- impact: 4/5
-
Foundational Debt를 줄이는 방식
- Foundational Debt는 impact, fix cost, contagion 세 축 모두에서 높은 점수를 보이는 경향이 있음
- 높은 fix cost는 불완전한 시스템을 계속 쓰게 만들며, 때로는 이것이 올바른 선택이기도 함
- 하지만 높은 impact와 높은 contagion 때문에 심각한 Foundational Debt를 고치면 큰 보상이 따를 수 있음
- Riot에서 관찰한 가장 흔한 수정 전략은 새 시스템을 기존 시스템 옆에 세우는 것임
- 가능하면 기존 foundational debt를 MacGyver Debt로 바꾸고, conversion operation을 통해 새 시스템과 기존 시스템을 오가며 천천히 포팅함
- 이 방식은 특정 영역에서 이점을 얻기 시작하면서 위험 노출을 제한함
- 이런 전환이 불가능할 때는 compile time switch, 가능하다면 loading time switch를 만들어 새 시스템에 대한 신뢰를 쌓을 수 있음
- compile time switch 방식은 GDS 전환에 사용 중임
- loading time switch 방식은 Determinism에서 효과가 있었음
Data Debt: 결함 위에 대량 콘텐츠가 쌓인 상태
- Data Debt는 다른 범주의 기술 부채 위에 많은 콘텐츠가 쌓일 때 발생함
- 시작점은 scripting system의 버그, item에 적합하지 않은 file format, 서로 잘 맞지 않는 두 시스템 등일 수 있음
- 그 코드 결함 위에 art, scripts, sounds 같은 대량의 콘텐츠가 만들어지면 초기 기술 부채를 고치는 일이 매우 위험해짐
- 시간이 지나면 무엇이 깨질지 파악하기가 고통스러울 정도로 어려워짐
-
현실 세계 비유: DNA
- 유기체의 genome은 수백만 년 동안 mutation, transcription error, evolutionary pressure를 통해 천천히 쌓임
- 어떤 복사 오류는 쓸모없지만 해롭지 않고, 어떤 오류는 해롭고, 어떤 오류는 강력한 이점을 줌
- DNA 조각이 실제로 무엇을 하는지 알아내기는 매우 어려움
- base pair가 무엇을 의미하는지, base pair 집합이 protein construction을 위한 amino acid로 어떻게 번역되는지는 완전히 이해하고 있음
- DNA의 일부 non-encoding role에 대해서도 더 많이 이해하기 시작했음
- 하지만 인간 genome의 30억 개 이상 base pair에는 아직 거의 이해하지 못한 부분이 많음
- Radiolab의 CRISPR 에피소드는 최근 풀린 그런 퍼즐 하나를 다룸
-
League 사례: block parameter naming bug
- League of Legends의 Data Debt는 원래는 사소한 수정이었을 일을 고된 작업으로 바꿀 때 가장 큰 영향을 줌
- game engineer들은 게임 시스템 구현 방식에 대한 깊은 지식을 쌓고, 어떤 코드 변경이 어떤 데이터를 깨뜨릴지 예측하는 데 숙련됨
- Data Debt는 LoL engine 변경 시 가장 중요한 고려사항 중 하나임
- 몇 년 전 수정된 Data Debt 사례는 BlockBuilder scripting language의 block parameter 관련 버그였음
- toy example에서 Owner의 armor를 변수와 상수로 증가시키려 하면 예상값은 변수 Delta 20과 상수 5를 더한 25 bonus armor임
- 변수 이름이 parameter 이름과 같으면 예전에는 결과가 40이 되었음
- 왜 45가 되지 않았는지는 글쓴이도 모른다고 함
-
실제 수정 과정
- Champions team 엔지니어 NoopMoney가 이 동작을 고치려 했을 때 실제 코드 수정은 4줄 삭제뿐이었음
- 그러나 전염성이 높은 부채는 작은 변경에도 철저한 계획이 필요했음
- LoL의 400,000줄 script 중 어느 곳의 numerical parameter라도 이 버그로 두 배가 되고 있을 수 있었음
- 더 큰 문제는 게임이 그 잠재적으로 두 배가 된 값에 맞춰 balance와 tuning이 되어 있어, 해당 script들이 “correctly” 동작하고 있었다는 점임
- NoopMoney는 예상치 못한 버그에 대비해 fix를 Live에서 toggle 가능하게 만들어야 했음
- 어떤 script가 이 버그에 의존하는지 식별하기 위해 광범위한 regex searching과 QA sweep을 수행함
- 최종적으로 수정으로 인한 문제는 비교적 작았고, 소수의 champion script만 변경이 필요했음
- Data Debt 때문에 결과 예측은 어려웠음
-
Parameter Naming Bug의 평가
- impact: 2/5
- 발생 시 영향은 작았음
- 전달된 값을 두 배로 만들고, 상수를 버릴 가능성이 있었음
- 이를 알게 된 디자이너와 엔지니어가 기억해야 하는 또 하나의 쓸모없는 tribal knowledge가 됨
- developer mindshare는 이렇게 낭비되기에는 가치 있는 자원임
- fix cost: 2/5
- 전체적으로 수정 자체는 직접적이었음
- live feature toggle을 만들어 fix의 안전성에 대한 확신을 높일 수 있었음
- 가장 비싼 부분은 테스트 타깃을 정하기 위해 문제 범위를 평가하는 초기 screening이었음
- contagion: 4/5
- 이 버그는 매우 논리적인 행동을 노렸다는 점이 불행했음
- unit에 damage를 주려면 값을 “Damage”라는 변수에 저장하는 것이 완전히 논리적임
- ApplyDamage block이 같은 이름의 parameter로 amount를 받으면서 버그가 트리거됨
- 다른 사람이 비슷한 spell을 만들기 위해 해당 block을 copy/paste하면 버그가 더 퍼짐
- impact: 2/5
-
Data Debt가 전염성이 높은 이유
- Data Debt는 변경 영향을 평가하기 어렵게 만들기 때문에 일반적으로 수정 비용이 높게 평가됨
- 더 우려되는 점은 data의 성질상 거의 항상 매우 전염성이 높다는 것임
- 기존 data를 copy/paste해 새 data를 만드는 방식이 일반적으로 허용됨
- 새 skillshot spell을 만들 때 Ezreal’s Mystic Shot에서 시작하면 시간을 크게 절약할 수 있고, 기존 data의 문제도 그 후손 data로 전파됨
- data는 code review와 유사한 기술 검토를 거의 받지 않기 때문에, 나쁜 관행이 널리 알려져 있어도 확산을 알아차리고 멈추기 어려움
- data의 문제를 고치려면 보통 눈과 뇌를 가진 사람이 직접 검증해야 하며, compiler와 formal logic만으로는 충분하지 않음
-
Data Debt를 고치는 두 접근
- 첫 번째 접근은 do it right checkbox임
- data creator를 위해 기존 “broken” behavior와 새 “fixed” behavior 사이 toggle을 만드는 방식임
- 이상적으로는 fixed version을 기본값으로 두고, old content가 broken version을 사용하게 함
- 이후 MacGyver Debt처럼 느리고 꾸준한 replacement로 새 버전으로 옮길 수 있음
- 단점은 editing UI에 점점 더 많은 불필요한 요소를 추가하는 영구 비용이 생긴다는 점임
- 두 번째 접근은 just fix the damn thing임
- NoopMoney가 parameter naming bug에 사용한 방식임
- 버그를 고친 뒤 의미 있게 영향을 받는 모든 data를 수리하려고 시도함
- 덜 무섭게 만드는 기법에는 theoretical impact를 이해하기 위한 많은 grep과 regex searching, targeted testing, ship 후 더 나쁜 누락이 발견될 경우 기존 동작으로 되돌릴 toggle 준비가 있음
- Determinism은 변경 전후 서버가 같은 결과를 생성하는지 확인할 수 있게 해 이런 변경 테스트에 많은 도움을 줌
- 첫 번째 접근은 do it right checkbox임
정리: 전염성을 비용 평가에 포함해야 함
- 기술 부채 측정 지표는 impact, fix cost, contagion임
- impact는 고객과 개발자에 대한 영향임
- fix cost는 시간과 위험임
- contagion은 문제의 전파 정도임
- 대부분의 개발자는 impact와 fix cost를 정기적으로 고려하지만, contagion 논의는 상대적으로 드묾
- contagion은 문제가 깊이 파고들어 점점 제거하기 어려워질 때 개발자의 최악의 적이 될 수 있음
- 반대로 fix를 문제보다 더 전염성 있게 만들면 contagion을 무기로 바꿀 수 있음
- League에서 본 대부분의 기술 부채는 네 범주 중 하나로 정리됨
- Local Debt: 내부가 지저분한 black box 같은 부채
- MacGyver Debt: conversion function으로 2개 이상의 시스템이 덕트 테이프처럼 붙어 있는 부채
- Foundational Debt: 전체 구조가 불행한 가정 위에 만들어진 부채
- Data Debt: 다른 유형의 부채 위에 막대한 data가 쌓여 수정이 위험하고 시간이 많이 드는 부채