나는 고의로 Bad Decision System을 구축했다 (당신이 직접 만들지 않게)

발행: (2025년 12월 19일 오후 08:00 GMT+9)
12 min read
원문: Dev.to

I’m happy to translate the article for you, but I need the full text of the post (excluding the source line you already provided) in order to do so. Could you please paste the content you’d like translated? Once I have the text, I’ll keep the source link at the top and translate the rest into Korean while preserving all formatting, markdown, and technical terms.

작업: 동일한 문제, 두 구현

두 시스템 모두 정확히 동일한 문제를 해결합니다:

입력 텍스트 → 키워드 추출 → 점수 계산 → 행동 추천

행동 공간은 의도적으로 작게 설정되었습니다:

  • WAIT_AND_SEE
  • BUY_MORE_STOCK
  • PANIC_REORDER

작업을 간단하게 유지함으로써 모델 품질이 아니라 시스템 동작에 전적으로 집중할 수 있습니다.

벤치마크 아이디어

벤치마크는 의도적으로 최소화되었습니다:

  1. 단일, 고정된 입력 텍스트를 사용한다.
  2. 시스템을 여러 번 실행한다.
  3. 출력이 안정적인지 관찰한다.

왜 중요한가: 한 번만 작동하는 시스템은 시스템이 아니다 — 우연일 뿐이다. 같은 입력이 다른 출력을 만든다면, 시스템 수준에서 근본적인 문제가 있다.

벤치마크 결과: BAD vs GOOD

다음 결과는 동일한 입력을 두 시스템 모두에서 다섯 번 실행하여 얻은 것입니다.

BAD 시스템 출력 (발췌)

BAD 시스템은 점진적으로 결정이 강화됩니다:

실행점수동작
114WAIT_AND_SEE
342BUY_MORE_STOCK
574PANIC_REORDER

동일한 입력. 동일한 키워드. 완전히 다른 결정.

집계된 벤치마크 요약

BAD system

  • 실행 횟수: 5
  • 고유 점수: 5 → [14, 28, 42, 58, 74]
  • 고유 동작: 3

GOOD system

  • 실행 횟수: 5
  • 고유 점수: 1 → [14, 14, 14, 14, 14]
  • 고유 동작: 1

GOOD 시스템은 순수 함수처럼 동작합니다. BAD 시스템은 메모리 누수처럼 동작합니다.

Source:

Failure Taxonomy: How the BAD System Breaks

BAD 시스템은 한 가지 명확한 방식으로만 실패하지 않습니다. 대신 실제 AI 및 데이터 시스템에서 흔히 나타나는 여러 상호작용하는 실패 모드를 보여줍니다. 이러한 실패 모드에 이름을 붙이면 감지하기 쉬워지고, 실수로 배포되는 것을 방지할 수 있습니다.

1️⃣ Drift

  • 정의: 입력이 정확히 동일한 상태에서도 시스템의 출력이 시간이 지남에 따라 변합니다.
  • 근본 원인: 실행마다 누적되는 전역 점수; 리셋 없이 단조롭게 증가하는 상태.
  • 위험성:
    • 명시적인 변경 없이 비즈니스 로직이 변형됩니다.
    • 과거 실행 순서가 현재 결정에 영향을 미칩니다.
    • 값이 “합리적”으로 보이기 때문에 모니터링 대시보드가 문제를 놓치는 경우가 많습니다.

Drift는 특히 학습처럼 보이지만 실제로는 그렇지 않기 때문에 위험합니다.

2️⃣ Non‑determinism

  • 정의: 동일한 입력이 서로 다른 출력을 생성합니다.
  • 근본 원인: 점수 산정에 삽입된 무작위 노이즈; 실행 이력에 대한 암묵적 의존.
  • 위험성:
    • 버그를 신뢰성 있게 재현할 수 없습니다.
    • 테스트 실패가 불안정해지고 신뢰를 잃습니다.
    • A/B 실험이 통계적 의미를 상실합니다.

결정을 재현할 수 없으면 디버깅도 할 수 없습니다.

3️⃣ Hidden State

  • 정의: 함수가 인터페이스나 입력에 보이지 않는 데이터를 사용합니다.
  • 근본 원인: CURRENT_SCORE, LAST_TEXT, RUN_COUNT와 같은 전역 변수.
  • 위험성:
    • 코드를 로컬에서 이해할 수 없습니다.
    • 리팩터링 시 비정상적인 방식으로 동작이 바뀝니다.
    • 새로운 기여자가 무의식적으로 회귀를 도입합니다.

숨겨진 상태는 모든 함수 호출을 추측 게임으로 만듭니다.

4️⃣ Silent Corruption

  • 정의: 시스템은 오류 없이 계속 실행되지만, 결정이 점점 더 잘못됩니다.
  • 근본 원인: 명시적인 실패 신호가 없으며, 불변식이나 정상성 검사가 없습니다.
  • 위험성:
    • 잘못된 출력이 하위 시스템으로 전파됩니다.
    • 문제는 비즈니스 영향으로만 드러납니다.
    • 롤백이 어렵거나 불가능해집니다.

시끄러운 실패는 고쳐지지만, 조용한 실패는 배포됩니다.

왜 이 분류 체계가 중요한가

이러한 실패 모드는 거의 단독으로 나타나지 않는다. BAD 시스템에서는 서로를 강화한다:

  • 숨겨진 상태가 드리프트를 가능하게 한다.
  • 드리프트가 비결정성을 증폭시킨다.
  • 비결정성이 무음 손상을 숨긴다.

이러한 패턴을 이해하는 것이 단일 버그를 수정하는 것보다 더 가치 있다—왜냐하면 동일한 분류 체계가 훨씬 크고 복잡한 AI 시스템에도 적용되기 때문이다.

단일 지표: 안정성 점수

시스템 동작을 요약하기 위해 단일 지표를 사용했습니다:

stability_score = 1 - (unique_scores / runs)
  • 1.0 → 완벽히 안정적
  • 0.0 → 완전히 불안정

안정성 결과

시스템안정성 점수
BAD0.0
GOOD0.8

이 한 숫자만으로도 어느 시스템을 신뢰할 수 있는지 알 수 있습니다.

최소 수정: 모든 것을 바꾸는 네 개의 작은 패치

이것은 재작성이 아닙니다. 수술적 변경입니다. 각 패치는 새로운 추상화나 프레임워크를 도입하지 않고 전체 클래스의 실패 모드를 제거합니다.

패치 1 — 전역 상태 제거

Before (BAD):

# global mutation + history dependence
GS.CURRENT_SCORE += base
return GS.CURRENT_SCORE

After (GOOD):

def score_keywords(keywords, text):
    return sum(len(w) % 7 for w in keywords) + len(text) % 13

이것이 해결하는 문제

  • 점수 드리프트를 없앱니다.
  • 숨겨진 히스토리 의존성을 제거합니다.
  • 함수가 결정적이고 테스트 가능해집니다.

전역 상태에 의존하는 함수는 함수가 아니라 메모리 누수입니다.

패치 2 — 부작용을 경계로 이동

Before (BAD):

def extract_keywords(text):
    print("Extracting keywords...")
    open("log.txt", "a").write(text)
    return tokens[:k]

After (GOOD):

def extract_keywords(text):
    # Pure computation – no I/O, no printing
    return tokenize(text)[:k]

이것이 해결하는 문제

  • 실행을 비결정적으로 만드는 숨겨진 I/O 부작용을 제거합니다.
  • 로깅을 핵심 로직과 분리합니다(예: 데코레이터나 래퍼를 통해).

패치 3 — 불변 조건 강제

Before (BAD):

def compute_score(keywords):
    # No sanity checks
    return sum(len(k) for k in keywords) * random.random()

After (GOOD):

def compute_score(keywords):
    assert all(isinstance(k, str) for k in keywords), "Keywords must be strings"
    base = sum(len(k) for k in keywords)
    return base  # deterministic, no random factor

이것이 해결하는 문제

  • 손상된 입력을 조기에 감지합니다.
  • 점수가 예상 범위 내에 머물도록 보장합니다.

패치 4 — 실행당 상태 초기화

Before (BAD):

RUN_COUNT += 1          # global counter never reset
CURRENT_SCORE += 5     # accumulates across runs

After (GOOD):

def run_pipeline(text):
    # Local state only
    keywords = extract_keywords(text)
    score = compute_score(keywords)
    action = decide_action(score)
    return {"score": score, "action": action}

이것이 해결하는 문제

  • 각 호출이 독립적임을 보장합니다.
  • 실행 간 드리프트와 숨겨진 상태를 제거합니다.

Additional Patch Details

Patch 3 — Make Dependencies Explicit

Before (BAD):

if GS.LAST_TEXT is not None:
    base += len(GS.LAST_TEXT) % 13

After (GOOD):

def score_keywords(keywords, text):
    base = sum(len(w) % 7 for w in keywords)
    return base + (len(text) % 13)

What this fixes

  • 숨겨진 입력이 없음.
  • 데이터 흐름이 명확함.
  • 안전한 리팩터링.

Patch 4 — Name the Magic Numbers

Before (BAD):

if score > 42:
    action = "PANIC_REORDER"

After (GOOD):

@dataclass(frozen=True)
class Config:
    panic_threshold: int = 42

if score > cfg.panic_threshold:
    action = "PANIC_REORDER"

What this fixes

  • 결정이 설명 가능해짐.
  • 매개변수가 검토 가능해짐.
  • 동작 변경이 의도적으로 이루어짐.

요약

  • 숨겨진 상태 제거
  • 비결정성 제거
  • 행동을 설명 가능하게 만들기
  • 시스템에 대한 신뢰 회복

최종 요약

The BAD system works. That’s the problem.
It fails in the most dangerous way possible: plausibly and quietly.

The GOOD system is boring, predictable, and easy to reason about — which is exactly what you want in production.

Working code is not the same as a working system.

코드 및 재현성

이 글에서 사용된 모든 코드 — 의도적으로 깨진 시스템, 깔끔한 구현, 그리고 벤치마크를 포함 — 는 GitHub에서 확인할 수 있습니다:

👉 https://github.com/Ertugrulmutlu/I-Intentionally-Built-a-Bad-Decision-System-So-You-Don-t-Have-To

결과를 재현하고 싶다면 다음을 실행하세요:

python compare.py

벤치마크는 동일한 입력을 두 시스템에 여러 번 실행하고, 몇 줄의 출력으로 예측 가능성이 화려한 추상화보다 왜 더 중요한지를 보여줍니다.

Back to Blog

관련 글

더 보기 »