[4개월 개발기] 에피소드 6 — 모든 아키텍처 결정을 압도한 버그
EN English version →“4개월간의 금융 AI 개발기” 6편. 에피소드 5에서 어려운 데이터 무결성의 늪을 간신히 빠져나온 뒤에도, 여전히 심각한 함정이 입을 벌리고 있었다. 모델 훈련 환경 자체에 숨어 있던 예상치 못한 버그 하나가, 몇 주 동안 우리가 그토록 치열하게 내렸던 아키텍처적 결론들을 조용하고 철저하게 오염시키고 있었던 것이다. 이번 편은 그 중요했던 발견의 과정과, 뼈에 새겨진 방법론적 교훈에 대한 이야기다.
이미 확정된 결론이라고 믿어 의심치 않았던 것
에피소드 2에서 Sigmoid 게이트를 발견하고 환호했던 과정을 짧게 짚은 바 있다. PLE 모델의 검증 손실(Validation Loss)이 좀처럼 수렴하지 않던 꽉 막힌 상황에서, 우리는 Opus와의 깊은 대화를 거쳐 NeurIPS 2024에 발표된 Sigmoid 게이트 관련 논문을 발굴해 냈고, 즉시 이를 시스템에 구현했다. 결과적으로 실험 결과는 우수했다. Sigmoid가 기존의 Softmax를 일관되게, 그리고 성공적으로 압도한 것이다.
연구와 개발의 세계에서 ‘일관된 결과’란 그 무엇보다 강력한 증거다. 우리는 다섯 번의 일반적인 제거 실험(Ablation Run), 매번 달라지는 시드(Seed) 값, 각기 다른 데이터 분할(Split) 환경에서도 어김없이 Sigmoid가 NDCG와 F1-Macro 지표에서 Softmax를 0.02–0.04 포인트 앞서는 것을 두 눈으로 똑똑히 확인했다. 이 시점에서 우리는 이 승리를 ‘불변의 결론’으로 공식 문서화했고, 다음 단계의 아키텍처 결정(adaTT 설계 등)을 내릴 때 주저 없이 이 결과를 든든한 전제로 깔고 들어갔다.
결론이 활자로 굳어지고 나면 걷잡을 수 없는 후폭풍이 이어진다. 논문 초안(Paper 1)에는 “Sigmoid 게이트가 이종 전문가 아키텍처에 매우 적합하다”라는 확신에 찬 문장이 당당하게 박혔다. adaTT 모듈의 세부 설계 역시 Sigmoid 게이트가 굳건히 자리를 지키고 있다는 전제하에 진행되었다. *“이건 이미 검증이 끝난 팩트다”*라는 맹목적인 전제가 후속 의사결정 수십 개에 조용히, 그리고 치명적으로 스며든 것이다.
몇 주 뒤 터져버린 불확실성 가중치(Uncertainty Weighting) 버그
그렇게 몇 주가 평화롭게 흐른 뒤, adaTT를 한창 구현하던 엔지니어 2팀이 불확실성 가중치(Uncertainty Weighting, Kendall et al., 2018) 코드를 샅샅이 점검하다가 우연히 버그 하나를 발견했다. 사실 코드 자체만 놓고 보면 정말 매우 작고 사소한 버그였다. 태스크별 손실(Task Loss)에 학습 가능한 파라미터 을 곱해주는 공식에서, 부호 하나가 정반대로 뒤집혀 있었던 것이다.
원래 의도했던 올바른 수식은 다음과 같아야 했다. \mathcal{L}_{\text{total}} = \sum_t \frac{1}{2\sigma_t^2} \mathcal{L}_t + \log \sigma_t
그러나 버그가 숨어 있던 실제 구현 코드는 다음과 같았다. \mathcal{L}_{\text{total}} = \sum_t \frac{1}{2\sigma_t^2} \mathcal{L}_t - \log \sigma_t
단 하나의 부호 오류. + log_sigma가 들어가야 할 자리에 - log_sigma가 떡하니 자리 잡고 있었다. 이 앙증맞은 부호 하나가 뒤집히자, 정규화(Regularization) 항이 원래의 의도와 완전히 정반대로 엇나가기 시작했다. 학습 과정이 불확실성 를 ‘키우는’ 방향이 아니라, 오히려 비정상적으로 ‘쥐어짜서 작게 만드는’ 방향으로 모델을 거칠게 밀어붙인 것이다.
그 결과는 예상 밖이었다. 이진 분류(Binary Classification)처럼 본래 손실(Loss) 값 자체가 태생적으로 작은 태스크들의 가 극단적으로 0에 가깝게 쪼그라들었고, 반대급부로 이 이진 분류 태스크들이 시스템 전체의 유효 가중치(Effective Weight)를 빠르게 집어삼키며 다른 복잡한 태스크들을 완전히 압도해 버린 것이다.
요약하자면, 우리가 그토록 신뢰했던 불확실성 가중치 모듈이 “오로지 이진 분류 태스크에만 비정상적으로 가중치를 몰아주는” 주요한 편향 버그 덩어리가 되어, 무려 수 주일 동안 우리의 실험실을 지배하고 있었던 것이다.
버그 수정 직후, 모든 성능 우위가 뒤집히다
오싹한 식은땀을 닦아내며 우리는 급히 버그를 수정했고, 초조한 마음으로 5번의 제거 실험(Ablation Run)을 처음부터 다시 돌렸다. 그리고 모니터에 찍힌 결과는 우리의 눈을 의심하게 만들었다. NDCG 지표에서 버려졌던 Softmax가 화려하게 부활하며 Sigmoid를 앞선 것이다.
첫 번째 실행에서는 0.02 포인트 앞섰다. 두 번째는 0.03, 세 번째는 0.01 포인트. 나머지 두 번의 실행에서는 엇비슷하게 맞섰다. 성능 우위의 방향이 180도 완전히 뒤집혀 버렸다.
처음엔 그저 운이 나쁜 측정 오차이거니 애써 현실을 부정했다. 시드(Seed) 값을 바꿔가며 무려 10회를 추가로 지속적으로 돌려보았다. 하지만 결과는 너무나도 일관되고 잔인했다. Sigmoid가 천하무적이라 믿었던 시절의 그 ‘일관된 우위’가, 이제는 Softmax의 완전히 다른 일관성으로 완전히 교체되어 버린 것이다.
도대체 왜 이런 일이 벌어졌는가 — 근본 원인 분석
충격을 추스르고 실험 로그를 차근차근 역추적해 보니, 이 유의미한 반전의 논리는 너무나도 깔끔하고 완벽했다.
버그로 깨져 있던 불확실성 가중치 환경에서는: 13개의 태스크가 정상적으로 균등하게 가중치를 나눠 갖지 못했다. 개수가 많은 7개의 이진 분류(Binary) 태스크들이 뿜어내는 거대한 그래디언트(Gradient)가, 상대적으로 미세한 3개의 다중 분류(Multiclass)와 3개의 회귀(Regression) 태스크를 그야말로 넘어서 압도해 버렸다. 이렇게 기울어진 운동장 속에서 Softmax 게이트 특유의 ‘경쟁적 라우팅’ 방식은, 이진 분류 태스크 한쪽으로만 전문가의 용량(Capacity)을 몰아주는 심각한 부작용을 낳았고, 나머지 태스크들은 굶어 죽어버렸다. 반면 Sigmoid의 ‘비경쟁적 라우팅’ 방식은 모든 전문가를 억지로라도 활성화 상태로 유지시켰기 때문에, 특정 태스크가 전문가를 독식하는 현상을 막아주는 ‘우연하고도 훌륭한 방화벽’ 역할을 해 주었던 것이다.
버그가 수정된 올바른 불확실성 가중치 환경에서는: 마침내 다중 분류와 회귀 태스크의 그래디언트가 원래 의도했던 건강한 크기를 회복했다. 운동장이 평평해지자 상황이 완전히 역전되었다. 이제는 Softmax의 치열한 ‘경쟁적 라우팅’ 방식이 전문가들의 날카로운 전문화(Specialization)를 강제하면서, 성격이 판이한 태스크 유형들 사이에 그래디언트가 마구잡이로 섞여 오염되는 것을 막아주는 강력한 구조적 장벽으로 찬란하게 작동하기 시작했다. 반대로 Sigmoid는 아무런 장벽 없이 모든 그래디언트를 밍밍하게 섞어버리는 바람에, 각 태스크 유형에 덜 특화된 멍청하고 둥글둥글한 결과물만을 뱉어내게 된 것이다.
즉, 아키텍처의 우위라는 방향성 자체가 ‘훈련 조건(Training Condition)‘에 성공적으로 종속되어 있었던 것이다. “Sigmoid가 무조건 더 낫다”라는 우리의 섣부른 결론은, 아키텍처 본연의 절대적인 우수성이 아니라 버그로 망가져 있던 훈련 환경 속에서 모델이 살아남기 위해 찾아낸 **‘특이하지만 유효했던 적응(Adaptation)‘**에 불과했다.
동질적 MTL vs 이질적 MTL — 맹목적인 문헌 인용의 함정
돌이켜보면, 우리가 금과옥조처럼 참고했던 NeurIPS 2024의 Sigmoid 게이트 논문(“Sigmoid Gating is More Sample Efficient than Softmax Gating in Mixture of Experts”)은 MoE 레짐에서 sample efficiency 관점의 이론적 우위를 보여준 결과였다. 우리가 처한 것과는 다른, 상대적으로 소규모이고 태스크 간 이질성이 낮은 실험 조건 위에서 도출된 결론이다. 경쟁적인 Softmax가 구조적으로 엇비슷한 전문가들 사이에서 붕괴(Collapse)를 일으키기 쉬운 조건에서는 비경쟁적인 Sigmoid가 우월하게 작동하는 것이 맞다.
하지만 우리 프로젝트의 전장은 전혀 달랐다. 무려 13개의 태스크가 얽혀 있고, 이진 분류, 다중 분류, 회귀라는 3가지 완전히 다른 태스크 유형이 난투극을 벌이는 극단적인 이질적(Heterogeneous) MTL 환경이었다. 이 야생의 레짐(Regime)에서는 경쟁적인 라우팅이 전문가 붕괴를 일으키는 것이 아니라, 오히려 섞이지 말아야 할 태스크 유형 간의 그래디언트 오염을 막아주는 든든한 **‘구조적 장벽’**으로 기능한다. 요컨대, 논문의 훌륭한 결과가 우리의 현실에는 1%도 전이되지 않는(Non-transferable) 완전히 다른 경계 조건(Boundary Condition)이었던 것이다.
이 서늘한 에피소드가 남긴 교훈은 너무나도 단순하고 명확하다. “레퍼런스로 삼은 논문의 실험 조건이 현재 내 프로젝트의 전장과 성공적으로 일치하는지 의심하고 또 의심하지 않으면, 너무나도 옳은 결론을 완전히 틀린 환경에 맹목적으로 이식하는 대참사를 낳게 된다.” 게다가 우리의 경우엔 훈련 환경의 심각한 코드 버그까지 겹쳐버렸으니, 그야말로 완벽한 ‘이중 오염’ 상태였던 셈이다.
이 아찔한 발견을 가능하게 한 일등 공신
이 유의미한 진실이 백일하에 드러나는 전체 과정이야말로, 에피소드 2에서 입이 마르도록 칭찬했던 **“Claude Code의 중요한 역량”**을 증명하는 가장 성공적인 사례다.
몇 주 전에 우리가 도대체 왜 Sigmoid를 채택했는지에 대한 치열했던 맥락 — 왜 그 논문을 찾아 헤맸는지, 당시 실험 결과의 숫자들은 정확히 어땠는지, 그리고 이 결정이 어떤 후속 설계들의 뼈대(전제)로 작용했는지 — 이 모든 기억이 세션 안에 고스란히 직접 확인 가능한 상태로 온전히 살아 숨 쉬고 있었다. 그렇기에 버그 수정 직후 성능이 역전되었다는 새로운 증거를 마주한 바로 그 순간, “그럼 우리가 그때 내렸던 결론의 근거들이 지금 돌이켜보면 도대체 어떻게 보이는가?”라며 즉각적이고 자연스러운 재검토의 흐름으로 매끄럽게 넘어갈 수 있었던 것이다.
만약 우리가 완전히 새로운 세션을 열어 백지상태에서 “음, 예전에 우리가 왜 Sigmoid를 골랐더라?”라며 흐릿해진 인간의 기억을 더듬고 파편화된 문서를 뒤적여가며 상황을 처음부터 재구축해야만 했다면? 아마도 재조사 자체를 시작할 엄두조차 내지 못하고 그 찜찜한 버그를 대수롭지 않게 덮어두고 넘어갔을 가능성이 매우 높다.
이 사투가 남긴 방법론적 교훈
이 에피소드는 우리 팀의 개발 방법론 전체를 송두리째 뒤흔들고 다음과 같은 굵직한 유산들을 남겼다.
1. 모든 아키텍처 결론에는 반드시 “훈련 조건이 100% 안정적이었는가?”라는 엄격한 체크리스트가 따라붙는다.
이제 우리는 특정 결과를 “불변의 결론”이라고 섣불리 문서화하기 전에, 손실 가중치(Loss Weighting), 스케일러의 상태(Scaler State), 레이블 정렬(Label Alignment), 스케줄러 설정(Scheduler Config) 등 훈련 환경의 기저 요소들이 이 결론에 조금이라도 찝찝하게 엮여(Depend) 있지는 않은지 이중 삼중으로 점검한다. 이 깐깐한 점검 체크리스트는 곧바로 우리의 규약집인 CLAUDE.md 문서에 영구적으로 추가되었다.
2. 논문(Paper) 초안의 대대적인 뜯어고침. 작성 중이던 Paper 1의 “Sigmoid 게이트가 이종 전문가 구조에 매우 적합하다”라고 자신 있게 쓰여 있던 섹션은 그야말로 크게 전면 수정되었다. 그리고 그 자리에 **“태스크의 이질성(Heterogeneity)이라는 경계 조건에 따라 게이트의 우위는 언제든 180도 뒤집힐 수 있다”**는 겸손하고도 날카로운 통찰이 명시되었다. 훗날 작성된 Paper 3 (Loss Dynamics)의 핵심 아이디어 역시 바로 이 중요한 실패의 잔해 속에서 싹을 틔웠다.
3. adaTT 모듈의 억울한 실패(Null Result)에 대한 뛰어난 재해석. 에피소드 8에서 더 자세히 다루겠지만, 초기에 “adaTT 모듈을 붙여봤더니 13개 태스크 환경에서 PLE 대비 성능이 -0.019나 깎여나갔다”라고 우울하게 보고되었던 결과 역시, 사실 이 심각한 부호 버그의 악영향 아래서 억울하게 측정된 수치였다. 버그를 걷어내고 다시 측정해 보니 adaTT의 on/off 간 성능 격차는 불과 -0.001로, 사실상 통계적 노이즈 범위 안에 예쁘게 수렴했다. 즉, adaTT라는 구조 자체가 실패한 오류였던 것이 아니라, 당시의 훈련 환경 자체가 심각하게 오염되어 모듈의 진짜 실력을 억눌러 버렸던 것이다.
4. “진정한 전문성이란 실수를 단 한 번도 하지 않는 무결함이 아니라, 스쳐 지나가는 실수를 지속적으로 분석하여 마침내 발견해 내는 능력이다.”
이 묵직한 문장은 프로젝트의 규약집인 CLAUDE.md §1의 가장 첫머리에 훈장처럼 새겨졌다. 로그 계산식의 사소한 부호 버그 하나를 몇 주 동안이나 까맣게 눈치채지 못한 것은 결코 전문성의 부족이 아니다. 하지만 버그가 수정된 직후 나타난 미세한 파장을 놓치지 않고 끝까지 분석하여 마침내 거대한 진실을 뒤집어엎은 그 지속적인 재조사의 흐름이야말로 우리 팀이 가진 진짜 전문성의 증거다.
여전히 남은 질문들
- 그렇다면 과거에 우리가 “이건 일관된 결과다”라며 맹신했던 다른 결론들 중에도, 훈련 환경 버그가 낳은 심각한 부산물이 숨어 있지는 않을까? — 이 엄격한 의심 때문에 우리는 Paper 1에 실려 있던 나머지 모든 결과들을 바닥부터 다시 긁어모아 재검증해야 했고, 실제로 그중 상당 부분이 부끄럽게도 붉은 펜으로 덧칠되며 수정되었다.
- 실제 프로덕션 트래픽 위에서도 같은 성능 역전 현상이 재현될 것인가? — 2026년 4월 30일부터 수집이 시작된 실 트래픽 메트릭이 어느 정도 쌓여야 판정이 가능하다. 재현된다면 이 방법론적 교훈은 실데이터로 뒷받침되고, 재현되지 않는다면 Santander 벤치마크와 프로덕션 분포 간 차이를 설명해야 할 새로운 연구 주제가 된다.
- 그래서, 불확실성 가중치 모듈의 코드는 이제 두 번 다시 무너지지 않을 만큼 견고해졌는가? — 우리는 이 주요한 부호 하나를 감시하는 전용 단위 테스트(Unit Test)를 촘촘하게 짜넣고, 학습 중 값의 요동을 실시간으로 감시하는 모니터링 시스템을 CI(지속적 통합) 파이프라인에 강제로 박아 넣었다. 적어도 이 똑같은 버그가 다시 살아서 우리를 비웃는 일은 영원히 없을 것이다.
다음 편
이어지는 에피소드 7에서는, 지옥 같았던 아키텍처 안정화 작업이 끝난 뒤에 비로소 마주하게 된 진짜 엔지니어링의 세계를 다룬다. 무거운 PLE 교사(Teacher) 모델의 지식을 가벼운 LGBM 학생(Student) 모델로 통합하는 증류(Distillation)의 마법, 그리고 AWS Lambda 위에서 가볍게 춤추는 서빙(Serving) 아키텍처에 대한 이야기다. 도대체 왜 값비싼 딥러닝 모델 대신 낡은 LGBM으로 증류하여 서빙해야만 했는지, 어려운 서버리스(Serverless) 아키텍처의 비용 프로필, 그리고 AWS Bedrock이라는 거대한 무대 위에서 Feature Selector, Reason Generator, Safety Gate, OpsAgent, AuditAgent라는 5명의 에이전트가 각자의 역할을 어떻게 화려하게 분담해 내는지 낱낱이 분석한다.
원문 자료: 개발 스토리 §11 “모든 아키텍처 결정을 압도한 버그”.