The Infinite Software Crisis – Jake Nations, Netflix

서론: 우리가 이해하지 못하는 코드를 배포하는 시대

Netflix에서 AI 도구 도입을 주도해온 Jake Nations는 강연을 충격적인 고백으로 시작한다. 그는 자신이 완전히 이해하지 못한 코드를 생성하고, 테스트하고, 배포했으며, 어떻게 작동하는지 설명할 수 없었다고 말한다. 그리고 이것은 그만의 문제가 아니라 현대 소프트웨어 개발자 모두가 직면한 현실이라고 지적한다.

AI를 활용한 코드 생성의 가속화는 명백하다. 예전에는 며칠 걸리던 백로그 작업이 이제는 몇 시간 만에 완료되고, 수년간 미뤄졌던 대규모 리팩토링이 마침내 실행되고 있다. 하지만 문제는 대규모 프로덕션 시스템이 항상 예상치 못한 방식으로 실패한다는 점이다. 최근 Cloudflare의 장애 사례처럼, 시스템이 실패할 때 디버깅하려면 자신이 작성한 코드를 이해해야 한다. 그런데 지금 우리는 너무나 빠른 속도와 엄청난 양의 코드를 생성하고 있어, 우리의 이해력이 따라잡기 어려운 상황이다.

역사는 반복된다: 소프트웨어 위기의 계보

이러한 상황은 사실 새로운 것이 아니다. 모든 세대의 소프트웨어 엔지니어들은 결국 소프트웨어 복잡성이 그들의 관리 능력을 초과하는 벽에 부딪혀왔다. 우리는 소프트웨어 위기를 맞이한 최초의 세대가 아니며, 다만 무한한 규모의 생성 속도로 이를 맞이한 최초의 세대일 뿐이다.

1960-70년대: 최초의 소프트웨어 위기

1960년대 후반과 1970년대 초반, 당대의 뛰어난 컴퓨터 과학자들이 모여 소프트웨어 위기를 선언했다. 소프트웨어에 대한 수요는 엄청났지만, 개발 속도가 따라가지 못하고 프로젝트는 지나치게 오래 걸렸다.

Dijkstra(다익스트라)는 이를 명쾌하게 표현했다: “약한 컴퓨터 몇 대가 있을 때 프로그래밍은 가벼운 문제였다. 이제 거대한 컴퓨터들이 있으니 프로그래밍은 거대한 문제가 되었다.” 그는 하드웨어 성능이 1,000배 성장하면서 사회의 소프트웨어 요구도 비례해서 증가했고, 프로그래머들이 이 간극을 메우는 방법을 찾아야 한다고 설명했다.

소프트웨어 위기의 사이클

이후 이러한 패턴은 계속 반복되었다:

  • 1970년대: C 프로그래밍 언어 등장으로 더 큰 시스템 작성 가능
  • 1980년대: 개인용 컴퓨터 보급으로 모든 사람이 소프트웨어 작성 가능
  • 1990년대: 객체지향 프로그래밍(OOP) 도입, Java와 함께 온 지옥같은 상속 계층구조
  • 2000년대: Agile 방법론, 스프린트와 스크럼 마스터, 폭포수 모델의 종말
  • 2010년대: 클라우드, 모바일, DevOps로 소프트웨어가 진정으로 세상을 먹어치움
  • 2020년대: AI, Copilot, Cursor, Claude, Codex, Gemini 등으로 설명할 수 있는 만큼 빠르게 코드 생성 가능

패턴은 계속되지만, 규모가 근본적으로 변했다. 이제는 무한하다(Infinite).

Fred Brooks와 “No Silver Bullet”

1986년 Fred Brooks는 유명한 논문 “No Silver Bullet”에서 소프트웨어 생산성을 획기적으로(order of magnitude) 향상시킬 단일 혁신은 없을 것이라고 주장했다.

왜 그럴까? 그는 어려운 부분이 코딩의 기계적 측면(mechanics)—문법, 타이핑, 보일러플레이트—이 아니라 실제 문제를 이해하고 솔루션을 설계하는 것이며, 어떤 도구도 이 근본적인 어려움을 제거할 수 없다고 설명했다.

지금까지 우리가 만든 모든 도구와 기법은 기계적 측면을 더 쉽게 만들었다. 하지만 핵심 과제—무엇을 만들어야 하는지, 어떻게 작동해야 하는지 이해하는 것—은 여전히 똑같이 어렵다.

그렇다면 문제가 기계적 측면에 있지 않다면, 왜 우리는 계속 그것을 최적화하는가? 왜 경험 많은 엔지니어들이 이제 자신이 이해하지 못하는 코드를 갖게 되는가?

Simple vs Easy: 우리가 혼동하는 두 개념

답은 우리가 혼동하는 두 단어에 있다: **Simple(단순함)**과 Easy(쉬움).

우리는 이 둘을 같은 의미로 사용하는 경향이 있지만, 실제로는 완전히 다른 의미다. Clojure 프로그래밍 언어의 창시자 Rich Hickey는 2011년 “Simple Made Easy”라는 강연에서 이를 설명했다.

Simple의 정의

Simple은 “one fold, one braid”를 의미한다. 즉, 얽힘이 없다(no entanglement). 각 요소가 하나의 일을 하고 다른 것들과 뒤엉키지 않는다. Simple은 구조(structure)에 관한 것이다.

Easy의 정의

Easy는 “adjacent”, 즉 손이 닿는 곳에 있다는 것을 의미한다. 노력 없이 접근할 수 있는 것. 복사-붙여넣기-배포. Easy는 근접성(proximity)에 관한 것이다.

핵심적인 차이

우리는 바라는 것만으로 무언가를 Simple하게 만들 수 없다. 단순함은 사고, 설계, 그리고 얽힘을 푸는 작업(untangling)을 요구한다. 하지만 무언가를 Easy하게 만드는 것은 언제나 가능하다—그냥 더 가까이 두기만 하면 된다. 패키지를 설치하고, AI로 생성하고, Stack Overflow에서 솔루션을 복사하면 된다.

쉬운 길을 택하는 것은 인간의 본성이다:

  • Stack Overflow에서 복사하기—바로 거기 있다
  • 마법처럼 모든 것을 처리해주는 프레임워크—설치하고 실행하면 된다

하지만 Easy는 Simple을 의미하지 않는다. Easy는 시스템에 빠르게 추가할 수 있다는 뜻이고, Simple은 당신이 한 작업을 이해할 수 있다는 뜻이다.

Easy를 선택할 때마다, 우리는 “지금 속도, 나중 복잡성”을 선택하는 것이다. 솔직히 말하면, 이 트레이드오프는 예전에는 효과가 있었다. 코드베이스에 축적되는 복잡성이 충분히 느려서, 필요할 때 리팩토링하고, 재고하고, 재구축할 수 있었다.

AI는 이 균형을 파괴했다

AI는 궁극의 Easy 버튼이다. AI는 Easy한 경로를 너무나 마찰 없게 만들어서, 우리는 더 이상 Simple한 경로를 고려하지 않게 되었다. 코드가 즉시 나타나는데 왜 아키텍처에 대해 생각하는가?

복잡성의 나선: 대화형 인터페이스의 함정

인증(authentication) 기능을 추가하는 간단한 작업을 예로 들어보자:

  1. “Add auth” → 깔끔한 auth.js 파일 생성
  2. 몇 번 반복 → message.js 파일도 생성됨
  3. 계속 반복 → 세션이 깨지고, 충돌 발생
  4. Turn 20에 이르면: 더 이상 대화가 아니다. 너무 복잡해진 컨텍스트를 관리하고 있으며, 자신이 추가한 모든 제약조건조차 기억하지 못한다

결과물:

  • 포기한 접근법에서 나온 죽은 코드(dead code)
  • 그냥 작동하게만 만들어서 수정한 테스트
  • 세 가지 다른 솔루션의 파편들

각 새로운 지시사항이 아키텍처 패턴을 덮어쓴다. “여기서 auth가 작동하게 해”라고 하면 그렇게 한다. “이 에러 고쳐”라고 하면 고친다. 잘못된 아키텍처 결정에 대한 저항이 없다. 코드는 단지 당신의 최신 요청을 만족시키기 위해 변형될 뿐이다.

각 상호작용은 Easy를 Simple보다 선택하는 것이고, Easy는 항상 더 많은 복잡성을 의미한다. 우리는 더 잘 알고 있지만, Easy한 경로가 바로 거기 있을 때 우리는 그것을 택한다. 그리고 복잡성은 너무 늦기 전까지 계속 복리로 쌓인다.

복잡성의 두 가지 유형: Essential vs Accidental

Fred Brooks의 “No Silver Bullet” 논문으로 돌아가보면, 그는 모든 시스템에 두 가지 주요 유형의 복잡성이 있다고 식별했다:

1. Essential Complexity (본질적 복잡성)

해결하려는 문제 자체의 근본적인 어려움이다. 사용자는 결제를 해야 하고, 주문은 이행되어야 한다. 이것은 소프트웨어 시스템이 존재하는 이유 자체의 복잡성이다.

2. Accidental Complexity (우발적 복잡성)

그 외 우리가 과정에서 추가한 모든 것들이다:

  • 회피책(workarounds)
  • 방어적 코드(defensive code)
  • 프레임워크
  • 한때는 의미 있었던 추상화

코드 자체가 작동하게 만들기 위해 우리가 조합한 모든 것들이다.

AI의 맹점

실제 코드베이스에서 이 두 유형의 복잡성은 도처에 있으며, 너무 얽혀 있어서 이들을 분리하려면 컨텍스트, 히스토리, 경험이 필요하다. 생성된 출력은 이러한 구분을 하지 않는다. 따라서 모든 패턴이 계속 보존된다.

실제 사례: Netflix의 인증 시스템 리팩토링

Jake Nations가 Netflix에서 겪은 실제 사례를 보자. 5년 전에 작성한 오래된 인증 코드와 새로운 중앙화된 인증 시스템 사이에 추상화 레이어(abstraction layer)가 있는 시스템이 있었다. 전체 앱을 재구축할 시간이 없어서 그 사이에 shim을 넣었다.

이제 AI가 있으니 새 시스템을 직접 사용하도록 코드를 리팩토링할 좋은 기회처럼 보였다. 간단한 요청처럼 들리지만 아니었다:

  • 오래된 코드가 인증 패턴에 너무 강하게 결합되어 있었다
  • 권한 검사가 비즈니스 로직 전체에 엮여 있었다
  • 역할 가정(role assumptions)이 데이터 모델에 베이킹되어 있었다
  • 인증 호출이 수백 개의 파일에 흩어져 있었다

AI 에이전트는 리팩토링을 시작하고 몇 개 파일을 처리한 후 풀 수 없는 의존성에 부딪혀 통제 불능으로 빠지거나, 포기하거나, 더 나쁘게는 오래된 시스템의 기존 로직을 새 시스템을 사용해 재구현하려고 시도했다.

AI는 장면 뒤를 볼 수 없었다. 비즈니스 로직이 끝나고 인증 로직이 시작되는 곳을 식별할 수 없었다. 모든 것이 너무 얽혀 있어서, 완벽한 정보를 가지고도 AI는 깨끗한 경로를 찾을 수 없었다.

우발적 복잡성이 이렇게 얽혔을 때, AI는 이를 개선하는 데 별로 도움이 되지 않는다. 오히려 그 위에 더 많은 레이어를 추가할 뿐이다.

인간만이 할 수 있는 구분

우리는 그 차이를 구별할 수 있다. 적어도 생각할 시간을 충분히 가질 때는 말이다. 우리는 어떤 패턴이 본질적이고 어떤 것이 단지 몇 년 전 누군가가 해결한 방식인지 안다. 우리는 AI가 추론할 수 없는 컨텍스트를 갖고 있다. 하지만 시작하기 전에 이러한 구분을 만들 시간을 가질 때만 가능하다.

Jake가 작업하는 Netflix 코드베이스는 약 100만 줄의 Java 코드를 포함하고, 주요 서비스는 약 500만 토큰이다. 그가 접근할 수 있는 어떤 컨텍스트 윈도우도 이를 담을 수 없다.

처음에는 코드베이스의 큰 부분을 컨텍스트에 복사해서 패턴이 병합되는지, AI가 무슨 일이 일어나는지 파악할 수 있는지 보려고 했다. 하지만 이전의 인증 리팩토링처럼, 출력은 자체 복잡성에 갇혀버렸다.

해결책: Context Compression (컨텍스트 압축)

이로 인해 다른 접근법을 취할 수밖에 없었다:

  1. 무엇을 포함할지 선택하기: 설계 문서, 아키텍처, 다이어그램, 핵심 인터페이스
  2. 요구사항 작성하기: 컴포넌트가 어떻게 상호작용해야 하고 어떤 패턴을 따라야 하는지
  3. 명세서(spec) 작성하기: 500만 토큰이 2,000 단어의 명세서가 되었다
  4. 정확한 실행 단계 생성하기: 모호한 지시가 아닌, 정확한 작업 시퀀스

이는 훨씬 깨끗하고 집중된 코드를 생성했고, Jake는 이를 이해할 수 있었다. 그가 먼저 정의하고 실행을 계획했기 때문이다.

이 접근법을 그는 **“Context Compression”**이라고 불렀지만, “Context Engineering” 또는 “Spec-Driven Development”라고 불러도 된다. 이름은 중요하지 않다. 중요한 것은 사고와 계획이 작업의 대부분이 된다는 것이다.

3단계 접근법: Research → Plan → Implementation

Phase 1: Research (연구)

  1. 모든 것을 앞서 제공하기: 아키텍처, 다이어그램, 문서, Slack 스레드 등 변경과 관련된 모든 컨텍스트
  2. 에이전트를 사용해 분석하기: 코드베이스를 분석하고 컴포넌트와 의존성을 매핑
  3. 반복적으로 탐색하기:
    • “캐싱은 어떻게 되나?”
    • “실패를 어떻게 처리하나?”
    • 분석이 틀렸으면 수정하고, 컨텍스트가 빠졌으면 제공
  4. 단일 연구 문서 출력:
    • 무엇이 존재하는가
    • 무엇이 무엇에 연결되는가
    • 변경이 무엇에 영향을 미칠 것인가

몇 시간의 탐색이 몇 분의 읽기로 압축된다.

인간 체크포인트: 이것은 전체 프로세스에서 가장 레버리지가 높은 순간이다. 여기서 분석을 현실과 대조해 검증한다. 여기서 에러를 잡으면, 나중에 재앙을 방지한다.

Phase 2: Plan (계획)

검증된 연구를 손에 쥐었으니, 이제 상세한 구현 계획을 생성한다:

  • 실제 코드 구조
  • 함수 시그니처
  • 타입 정의
  • 데이터 흐름

“Paint by Numbers” 수준의 상세함: 가장 주니어 엔지니어에게 건네주고 “이거 해”라고 말할 수 있어야 한다. 그들이 한 줄씩 복사해도 작동해야 한다.

이 단계에서 중요한 아키텍처 결정을 내린다:

  • 복잡한 로직이 올바른지 확인
  • 비즈니스 요구사항이 좋은 관행을 따르는지 확인
  • 좋은 서비스 경계, 깨끗한 분리 확인
  • 불필요한 결합 방지

AI가 할 수 없는 것: 우리는 문제를 미리 발견할 수 있다. 우리는 이미 그것들을 경험했기 때문이다. AI에게는 그런 선택지가 없다. AI는 모든 패턴을 요구사항으로 취급한다.

진짜 마법: 리뷰 속도다. 우리는 이 계획을 몇 분 안에 검증하고 정확히 무엇이 만들어질지 알 수 있다. 코드 생성 속도를 따라잡으려면, 우리가 무엇을 하고 있는지를 똑같이 빠르게 이해할 수 있어야 한다.

Phase 3: Implementation (구현)

명확한 계획과 명확한 연구가 뒷받침되면, 이 단계는 매우 간단해야 한다. 그리고 그것이 포인트다.

AI가 따라야 할 명확한 명세를 가지면:

  • 컨텍스트가 깨끗하고 집중적으로 유지된다
  • 긴 대화의 복잡성 나선을 방지한다
  • 진화하는 코드의 50개 메시지 대신, 검증된 3개의 집중된 출력을 갖는다
  • 포기된 접근법, 충돌하는 패턴, 죽은 코드를 남기는 “wait-actually” 순간이 없다

진짜 보상: 백그라운드 에이전트를 사용할 수 있다는 것이다. 모든 사고와 어려운 작업을 미리 했기 때문에, 구현을 시작하게 하고 다른 일을 하러 가서 나중에 리뷰할 수 있다.

리뷰는 빠르다. 계획을 따르는지만 확인하면 되기 때문이다. 새로운 것이 발명되었는지 이해하려고 노력할 필요가 없다.

핵심: 우리는 AI를 사용해 우리를 위해 생각하게 하는 것이 아니다. 우리의 이해 능력을 유지하면서 기계적 부분을 가속화하는 데 사용한다. 연구는 더 빠르고, 계획은 더 철저하고, 구현은 더 깨끗하다. 하지만 사고, 종합, 판단은 우리에게 남아 있다.

실제 적용: 수동 마이그레이션의 중요성

앞서 언급한 AI가 처리할 수 없었던 인증 리팩토링을 다시 보자. 지금은 실제로 작업 중이고 좋은 진전을 보이고 있다. 하지만 더 나은 프롬프트를 찾아서가 아니다.

연구, 계획, 구현으로 바로 뛰어들 수조차 없었다. 먼저 손수 직접 이 변경을 해야 했다. AI 없이, 그냥 코드를 읽고, 의존성을 이해하고, 변경해서 무엇이 깨지는지 보는 것이다.

솔직히 그 수동 마이그레이션은 고통스러웠다. 하지만 결정적이었다(crucial):

  • 숨겨진 제약조건을 드러냈다
  • 어떤 불변성(invariants)이 유지되어야 하는지 보여줬다
  • 인증이 변경되면 어떤 서비스가 깨질지 드러냈다
  • 그 어떤 양의 코드 분석도 드러내지 못했을 것들이다

그리고 나서 그 수동 마이그레이션의 Pull Request를 연구 프로세스에 넣고, 앞으로의 모든 연구에 대한 시드(seed)로 사용하게 했다. 그제야 AI가 깨끗한 마이그레이션이 어떻게 보이는지 볼 수 있었다.

각 엔티티는 약간씩 다르기 때문에 계속 질문해야 했다:

  • “이것에 대해서는 어떻게 하나요?”
  • 어떤 것은 암호화되어 있고, 어떤 것은 그렇지 않다
  • 매번 추가 컨텍스트를 제공해야 했다

그리고 나서야, 한 번에 작동할 수 있는 계획을 생성할 수 있었다. “might”가 핵심 단어다—여전히 검증하고, 여전히 조정하고, 여전히 엣지 케이스를 발견하고 있다.

3단계 접근법은 마법이 아니다. 우리가 한 번 손으로 마이그레이션을 했기 때문에만 작동한다. 프로세스에 코딩하기 전에 이해를 얻어야(earn) 했다.

Silver Bullet은 없다

여전히 은탄환(Silver Bullet)은 없다:

  • 마법 프롬프트가 아니다
  • 더 나은 모델이 아니다
  • 더 나은 명세서를 작성하는 것도 아니다

단지 시스템을 충분히 깊이 이해하여 안전하게 변경할 수 있도록 하는 작업일 뿐이다.

왜 이 모든 노력을 하는가?

왜 이 모든 과정을 거쳐야 하는가? AI와 반복하다가 결국 작동하게 만들면 안 되는가? 결국 모델이 충분히 강력해지면 그냥 작동하지 않을까?

“It works”는 충분하지 않다. 테스트를 통과하는 코드와 프로덕션에서 살아남는 코드 사이에는 차이가 있다. 오늘 기능하는 시스템과 미래에 다른 누군가가 변경할 수 있는 시스템 사이에는 차이가 있다.

진짜 문제는 지식 격차(Knowledge Gap)

AI가 수천 줄의 코드를 몇 초 만에 생성할 수 있을 때, 그것을 이해하는 데는 몇 시간, 복잡하다면 며칠, 정말 얽혀 있다면 영원히 걸릴 수도 있다.

우리가 놓치고 있는 것: 패턴 인식의 위축

생성 속도를 따라잡기 위해 사고를 건너뛸 때마다, 우리는 단지 이해하지 못하는 코드를 추가하는 것이 아니다. 우리는 문제를 인식하는 능력 자체를 잃고 있다.

“이거 복잡해지고 있어”라고 말하는 본능은, 자신의 시스템을 이해하지 못할 때 위축된다(atrophies).

패턴 인식은 경험에서 온다:

  • 위험한 아키텍처를 발견하는 것은, 새벽 3시에 그것을 다루며 깨어 있었던 사람이기 때문이다
  • 더 간단한 솔루션을 밀어붙이는 것은, 대안을 유지보수해본 경험이 있기 때문이다

AI는 당신이 요청한 것을 생성한다. 과거 실패로부터의 교훈을 인코딩하지 않는다.

3단계 접근법이 이 격차를 메운다

이해를 생성 속도로 리뷰할 수 있는 아티팩트로 압축한다. 이것 없이는, 우리가 이해할 수 있는 것보다 빠르게 복잡성을 축적하고 있을 뿐이다.

결론: 소프트웨어는 인간의 노력이다

AI는 우리가 코드를 작성하는 방식의 모든 것을 바꾼다. 하지만 소프트웨어가 실패하는 이유에 대해서는 아무것도 바꾸지 않는다.

모든 세대는 자신의 소프트웨어 위기를 마주했다:

  • Dijkstra의 세대는 소프트웨어 엔지니어링이라는 학문을 만들어 이를 해결했다
  • 우리는 무한한 코드 생성과 함께 우리의 위기를 마주한다

해결책은 또 다른 도구나 방법론이 아니다. 우리가 항상 알고 있던 것을 기억하는 것이다:

소프트웨어는 인간의 노력이다(Software is a human endeavor).

어려운 부분은 결코 코드를 타이핑하는 것이 아니었다. 애초에 무엇을 타이핑해야 하는지 아는 것이었다.

번성할 개발자들

앞으로 번성할 개발자는 단지 가장 많은 코드를 생성하는 사람이 아니다:

  • 자신이 만드는 것을 이해하는 사람
  • 여전히 이음새(seams)를 볼 수 있는 사람
  • 잘못된 문제를 해결하고 있다는 것을 인식할 수 있는 사람

그것은 여전히 우리이고, 오직 우리일 것이다.

마지막 질문

Jake Nations는 질문 하나로 강연을 마무리한다:

질문은 AI를 사용할 것인가가 아니다. 그 배는 이미 떠났다. 그것은 기정사실이다.

진짜 질문은 이것이다:

“AI가 우리 코드의 대부분을 작성할 때, 우리는 여전히 우리 자신의 시스템을 이해할 것인가?”


핵심 메시지:

AI 코드 생성 시대에 성공하려면, 우리는 생성 속도를 이해 속도로 따라잡아야 한다. 이를 위해서는:

  1. Simple과 Easy를 구분하라 - 쉬운 것이 항상 단순한 것은 아니다
  2. 본질적 복잡성과 우발적 복잡성을 분리하라 - AI는 이를 구분하지 못한다
  3. Context Compression을 실천하라 - Research → Plan → Implementation
  4. 사고를 아웃소싱하지 마라 - AI는 기계적 작업을 가속화하지만, 판단은 우리의 몫이다
  5. 수동으로 먼저 이해하라 - 복잡한 리팩토링은 한 번은 손으로 해봐야 한다

소프트웨어 위기는 새로운 것이 아니다. 우리가 마주한 것은 무한한 규모의 소프트웨어 위기다. 하지만 해결책은 예전과 같다: 깊은 이해, 신중한 설계, 그리고 인간의 판단.