Claude Code나 GPT-5 Codex를 매일 쓰는 개발자라면 한 번쯤 마주치는 역설이 있다.
같은 프롬프트를 두 번 넣으면 다른 코드가 나온다. 어떤 때는 Python 함수가 한 줄짜리 list comprehension으로 나오고, 다음엔 for 루프 세 줄로 나온다. 변수명이 다르고, 주석 표현이 다르고, 예외 처리 순서까지 바뀐다. 결정적(deterministic)이지 않다. 학부 시절 우리가 "좋은 소프트웨어"의 필수 조건으로 배운 바로 그 속성이 깨진다.
그런데 막상 빌드는 성공한다. 테스트는 통과한다. 종종 사람보다 낫다. 2026년 현재 "시니어 엔지니어 대부분이 실제 코드는 거의 쓰지 않는다"는 보고가 나온다. 이 사람들이 순진한 것도, 품질을 포기한 것도 아니다. 오히려 그 반대다 — 프로덕션 배포되는 코드의 양은 늘었는데 사람이 직접 타이핑한 비율은 줄었다.
이 역설이 이 글의 출발점이다. 주사위를 굴려 나온 코드가 왜 빌드에 성공하는가. 그리고 우리는 그 사실을 어떻게 받아들여야 하는가.


1. 먼저 — LLM 비결정성의 구조

이 논의를 하려면 비결정성이 어디서 오는지부터 정확히 써야 한다.
LLM은 다음 토큰을 확률 분포로 예측한다. 어휘에 있는 모든 토큰마다 "이 자리에 이 토큰이 올 확률"이 매겨진다. 생성 단계에서 모델은 이 분포에서 하나를 샘플링한다. 이 샘플링 과정에 세 종류의 무작위성이 개입한다.

  • Temperature: 분포를 평평하게 할지 뾰족하게 할지 결정하는 스칼라. T=0이면 가장 확률 높은 토큰만 뽑고, T가 높아질수록 덜 확률 높은 토큰에도 기회가 간다.
  • Top-k / Top-p 샘플링: 후보를 상위 k개 또는 누적 확률 p까지로 제한.
  • Random seed: 같은 분포에서도 난수 시드가 다르면 다른 토큰이 뽑힌다.

"온도 0으로 하면 결정적이지 않나?"라는 질문이 자주 나온다. 답은 '아니오'다. 2024년 ACM Transactions on Software Engineering and Methodology에 실린 실증 연구는 ChatGPT의 코드 생성 비결정성을 829개 과제에 대해 측정했는데, 기본 설정에서는 같은 프롬프트에 대해 "모든 요청이 동일한 테스트 출력을 내는 과제 비율"이 CodeContests에서 24.24%, APPS에서 49%, HumanEval에서 52.44%에 불과했다. 온도를 0으로 내려도 비결정성은 줄어들지만 완전히 사라지지 않았다.
그 이유는 여러 층에 있다. GPU 병렬 연산의 부동소수점 누적 순서, batching에서의 다른 요청과의 상호작용, 모델 서빙 인프라의 라우팅 비결정성 등. 결국 LLM은 구조적으로 비결정적이고, 이는 "설정으로 끌 수 있는 버그"가 아니라 시스템의 본질적 속성이다.


2. 그런데도 빌드는 성공한다 — 두 번째 역설

여기가 흥미로워지는 지점이다. LLM이 비결정적이라는 사실은 10년 전이라면 "소프트웨어 엔지니어링 도구로는 실격"이라는 결론으로 가야 했다. 그런데 2026년 현실은 반대다.

  • Rakuten은 Opus 4.1로 오픈소스 프로젝트를 7시간 자율 리팩터링, 코드 품질 30% 개선.
  • Anthropic 내부 팀은 Claude Code로 PR 처리 속도 3배 개선.
  • 시니어 엔지니어들이 "코드의 100%는 AI가 쓰고, 내가 쓴 건 0%다"라고 말하고 있다.

"비결정적 생성"과 "프로덕션 품질"이 어떻게 공존 가능한가. 이게 이 글이 파고들 질문이다.


3. 해답의 첫 번째 층 — 분포가 충분히 뾰족하면 된다

가장 먼저 짚어야 할 지점은 "비결정적"과 "무작위"가 다르다는 것이다.
온도 0.7의 LLM이 def factorial(n): 다음에 생성할 토큰을 상상해보자. 확률 분포를 보면 \n if n 같은 토큰 시퀀스의 확률이 압도적으로 높고, 그 외의 선택지들은 사실상 0에 가깝다. 분포가 뾰족하다(peaked). 이런 경우 샘플링을 10번 해도 거의 매번 같은 토큰이 뽑힌다. 겉보기에 "결정적으로 보이는 비결정성"이다.
이 관찰을 실증적으로 확인한 것이 2024년의 AdapT Sampling 연구다. 연구자들은 코드 토큰을 "도전적 토큰(challenging tokens)"과 "자신 있는 토큰(confident tokens)" 으로 분류했다. 자신 있는 토큰은 분포가 뾰족해서 어떤 온도를 써도 같은 것이 뽑힌다. 도전적 토큰은 분포가 상대적으로 평평해서 실제로 다른 선택이 나온다.
흥미로운 발견은 도전적 토큰은 코드 블록의 첫 번째 위치에 집중적으로 나타난다는 것. 파이썬에서 이는 79.8%(HumanEval), 83.4%(MBPP), 82.5%(APPS)의 예측 난도를 보였다. 즉 LLM이 고민하는 지점은 "어떤 제어 구조를 쓸 것인가"(if/for/while/try)와 같은 구조 결정 순간이고, 그 이후의 토큰들은 상당히 결정론적으로 이어진다.
코드의 많은 부분이 "주사위"가 아니라 "자석"이다. 분포가 충분히 뾰족해서 샘플링해도 같은 결과로 끌려간다. 이게 첫 번째 해답이다.


4. 해답의 두 번째 층 — 원래 결정적이었던 적 없다

Paul Bernard의 2026년 2월 Medium 에세이 "Your Code Was Never Deterministic"이 찌르는 지점이 이것이다.
그는 이렇게 쓴다. 우리가 "결정적 코드"라고 부르던 것의 실체를 보자. 컴파일러는 결정적이다. 같은 소스 코드를 두 번 컴파일하면 같은 바이너리가 나온다. 맞다. 그런데 "같은 소스 코드"가 과연 결정적으로 쓰여졌는가는 다른 문제다.
10명의 시니어 개발자에게 "이 요구사항을 코드로 옮겨 주세요"라고 똑같이 말해보자. 10개의 다른 구현이 나온다. 변수명, 함수 분리 방식, 에러 처리 전략, 성능과 가독성의 트레이드오프 선택. 이 모든 것이 사람의 비결정성이다. 그런데 우리는 이걸 "비결정성"이라고 부르지 않았다. "스타일"이라고 불렀다.
더 중요한 지점. 우리가 실제 프로덕션에서 겪는 대부분의 장애는 컴파일러가 주사위를 굴려서 생긴 것이 아니라, 사람이 불완전한 의도를 코드로 옮기면서 생긴 것이다. 요구사항을 오해했거나, 엣지 케이스를 놓쳤거나, 동시성 가정을 틀리게 했거나.
Bernard의 핵심 통찰은 이것이다. "LLM은 비결정적이니까 믿을 수 없다"고 말할 때, 우리는 이전에 도대체 무엇을 믿고 있었는가? 테스트 커버리지를 믿고 있었나? 대부분 시스템에서 실질적 커버리지는 30~50% 수준이다. 코드 리뷰를 믿고 있었나? 진지한 리뷰와 LGTM 스탬프를 구분해야 한다. 우리가 믿었던 건 "코드가 동일 입력에 동일 출력을 낸다"는 컴파일러의 충실함이지, "코드가 올바르다" 는 보장이 아니었다.
그런데 컴파일러의 충실함은 LLM 코드에도 적용된다. LLM이 생성한 코드도 컴파일러가 결정적으로 처리한다. 같은 테스트를 돌리면 같은 결과가 나온다. 생성 단계는 비결정적이지만, 생성된 결과물은 여전히 결정적으로 검증 가능하다.
이것이 두 번째 해답이다. 비결정성의 위치가 "실행 시점"에서 "생성 시점"으로 이동했을 뿐이고, 우리는 둘 중 후자를 훨씬 잘 다룰 수 있다.


5. 해답의 세 번째 층 — 검증이 생성을 잡는다

가장 근본적인 해답은 여기 있다. 현대 AI 코딩 워크플로우는 "생성-검증 루프"로 설계되어 있다.
Claude Code, GPT-5 Codex, Cursor가 작동하는 방식을 생각해보자.

  1. 모델이 코드를 생성한다 (비결정적).
  2. 그 코드가 컴파일된다 (결정적).
  3. 타입 체커가 돌아간다 (결정적).
  4. 린터가 돈다 (결정적).
  5. 테스트 스위트가 실행된다 (결정적).
  6. 실패가 있으면 그 정보가 다시 모델에 피드백된다.
  7. 모델이 수정한 코드를 재생성한다 (비결정적).
  8. 루프가 수렴할 때까지 반복.

이 루프에서 비결정성의 역할은 "탐색(exploration)"이다. 모델이 여러 후보 구현을 시도해볼 수 있게 해주는 도구. 결정적(T=0) 모델은 한 번 막히면 계속 같은 곳에서 막힌다. 비결정적 모델은 같은 프롬프트에서 다른 경로를 시도해볼 수 있고, 그중 하나가 검증을 통과하면 그게 답이다.
이 구조가 진화적 알고리즘과 비슷하다는 점에 주목할 필요가 있다. 유전 알고리즘은 무작위 변이와 결정적 선택을 조합해서 해를 찾는다. 변이 단계는 비결정적이어야 한다 — 그래야 새로운 해공간을 탐색할 수 있다. 선택 단계는 결정적이어야 한다 — 그래야 나쁜 해가 걸러진다.
Claude Code 같은 에이전틱 코딩 도구는 정확히 이 구조다. 모델은 변이를 만들고, 테스트와 타입 시스템이 선택을 한다. 최종 산출물의 품질은 모델의 비결정성이 아니라 검증 시스템의 엄격함에 의해 결정된다.
이 관점으로 보면 우리가 AI 시대에 개발 인프라에 투자해야 할 것이 바뀐다. "모델을 더 정확하게 만드는 것"보다 "검증을 더 촘촘하게 만드는 것" 이 레버가 된다.


6. 한 층 더 — 명세는 변하지 않는다

Bernard의 에세이가 말하는 더 깊은 층이 있다. "명세"의 역할.
AI 코딩 파이프라인에서 개발자가 하는 일은 무엇인가. 명세를 쓴다. 자연어로, 구조화된 acceptance criteria로, property-based test로, 실행 가능한 스펙으로. 이걸 LLM이 구현으로 옮긴다. 구현은 run마다 다를 수 있다. 하지만 명세는 다르지 않다.
검증 인프라가 명세 층에 anchoring되어 있으면, 구현의 변동성은 더 이상 무서운 것이 아니다. 내부 구현 디테일일 뿐이다. 레지스터 할당이나 분기 예측처럼, 한 추상화 층 위로 올라갔을 때 보이지 않게 되는 종류의 디테일.
이 비유는 정확하다. 1970년대 개발자는 어셈블리로 레지스터 할당을 직접 했다. C 컴파일러가 등장하면서 레지스터 할당은 컴파일러가 상황에 따라 다르게 하는 내부 디테일이 됐다. 개발자는 더 이상 레지스터를 보지 않는다. 레지스터 할당의 "비결정성"(컴파일러 버전에 따라, 최적화 플래그에 따라 다른 결과)은 문제가 되지 않는다. 왜냐하면 C 명세가 그대로 유지되기 때문이다.
AI 코딩이 만드는 추상화의 도약도 같은 구조다. 개발자가 명세를 쓰고, 구현은 AI가 매번 다르게 할 수 있다. 명세 층에서 검증만 제대로 되면 구현의 변동은 무관한 내부 디테일이 된다.
이 틀에서 보면 "AI가 쓴 코드를 한 줄 한 줄 리뷰해야 한다"는 주장은 1970년대 C 개발자에게 "레지스터 할당을 한 줄 한 줄 리뷰해야 한다"고 말하는 것과 같다. 아래 층위에서의 결정을 위 층위에서 감시하려는 시도. 추상화의 이점을 스스로 포기하는 것.


7. 반론과 그 반론에 대한 반박

여기까지 읽은 독자의 머릿속에 떠오르는 반론들이 있을 것이다. 하나씩 다룬다.

반론 1: "테스트 코드도 코드다. 그걸 누가 검증하나?"

맞다. 그런데 비대칭이 존재한다. 명세는 프로덕션 코드보다 훨씬 짧고, 훨씬 선언적이며, 훨씬 변경이 드물다. 10,000줄의 구현을 생성하기 위한 명세는 200줄일 수 있다. 200줄은 사람이 리뷰할 수 있다. Property-based test로 쓰면 더 짧아진다. 리뷰해야 할 표면적 자체가 50배 줄어든다.

반론 2: "LLM이 테스트도 같이 생성한다. 같이 틀릴 수 있다."

타당한 지적이다. 하지만 해결책은 있다. 테스트와 구현을 다른 세션에서, 다른 모델로, 다른 프롬프트로 생성하는 것. 한 Claude가 테스트를 쓰고, 다른 Claude가 그 테스트를 통과하는 구현을 쓴다. 두 인스턴스가 공통으로 틀리려면 훨씬 정교한 체계적 오류가 필요하다. 이는 다수 인스턴스 합의(N-of-M consensus)와 구조적으로 같다.

반론 3: "미세한 엣지 케이스는 테스트로 못 잡는다."

사실이다. 하지만 이건 AI 시대 이전에도 사실이었다. 사람이 쓴 코드도 엣지 케이스에서 터진다. 오히려 AI는 알려진 엣지 케이스 패턴에 대한 기억이 사람보다 넓다. HackerNews의 C++/Qt 프로젝트 사례에서 Claude Code가 20분에 해결한 UI 버그는 숙련된 사람이 2시간 걸린 것이었다. 엣지 케이스 커버리지에서 AI가 사람보다 낫다는 증거가 이미 쌓이고 있다.

반론 4: "생성 루프가 무한히 돌 수 있다."

구조적 문제가 있을 때 그렇다. 해결은 타임아웃과 예산 설정, 그리고 "실패 시 사람에게 에스컬레이션"이다. 이는 마이크로서비스에서 circuit breaker가 하는 역할과 같다. 무한 루프는 아키텍처 결함이지 AI 코딩의 본질이 아니다.

반론 5: "그래도 나는 한 줄씩 읽어야 마음이 놓인다."

그건 정서의 문제지 엔지니어링의 문제가 아니다. 공감은 한다. 나도 처음에 Claude Code가 쓴 PR을 마우스로 한 줄씩 긁으며 읽었다. 그런데 그 시간이 실제로 버그를 잡았는가? 대부분은 아니었다. 마음이 편해지는 효과는 있었지만, 품질 보장 효과는 테스트와 타입 시스템이 이미 다 하고 있었다. 불안은 대체 가능한 의식이지, 대체 불가능한 검증이 아니다.


8. 실무자의 관점 — 이 통찰을 어떻게 써야 하나

이론을 썼으니 실무로 내려간다. 이 통찰이 2026년 개발자의 하루하루에 어떻게 적용되는가.

8.1 투자 우선순위를 다시 짜라

"좋은 AI 코딩 환경"의 조건이 바뀌었다.

  • 빠른 타입 체커 (rustc, tsc, mypy)
  • 빠른 테스트 러너 (pytest-xdist, jest parallel, cargo nextest)
  • 엄격한 린터 (ruff, eslint, clippy)
  • Property-based testing 프레임워크 (hypothesis, fast-check, proptest)
  • 계약 기반 테스트 (contract testing, consumer-driven contracts)

이 인프라가 허약한 프로젝트에서는 AI 코딩의 ROI가 낮다. 강한 프로젝트에서는 ROI가 폭발적이다. "검증 인프라에 투자한 프로젝트가 AI 시대의 수혜를 독점한다."

8.2 명세 쓰기가 핵심 역량이 된다

개발자의 시간 배분이 바뀐다.

  • 과거: 70% 구현, 20% 설계, 10% 테스트
  • AI 시대: 20% 명세 작성, 30% 검증 설계, 30% 리뷰·디버깅, 20% 통합

이 변화를 빨리 받아들이는 개발자일수록 경쟁력이 크다. "타이핑 속도가 더 이상 개발 속도와 상관없다." 대신 "명확한 명세를 언어로 옮기는 능력"이 중요해진다.

8.3 Writer/Reviewer 패턴을 기본 워크플로우로

지금 가장 효과적인 AI 코딩 방법론 중 하나는 두 개의 모델 인스턴스를 병렬 운영하는 것이다.

  • Writer: 구현을 생성하는 역할. 빠르고 공격적으로.
  • Reviewer: 별도 세션에서 구현을 평가하는 역할. 의심 많고 보수적으로.
  • Human: 두 결과를 보고 최종 판단.

Claude Code와 GPT-5 Codex를 병렬로 돌리며 서로를 교차 검증하게 하는 기법은 2026년 현재 시니어 엔지니어들 사이에서 표준이 되어가는 중이다. 한 모델의 비결정성을 다른 모델의 비결정성으로 상쇄시키는 설계.

8.4 명세 자체를 재사용 자산으로

한 번 잘 쓴 명세는 모델이 바뀌어도, 언어가 바뀌어도, 프레임워크가 바뀌어도 재사용 가능하다. Ruby on Rails로 구현한 명세를 Spring Boot로 다시 구현하는 것이, 명세가 있을 때는 AI에게 일이고 없을 때는 사람에게 대공사다.


9. 한 걸음 물러서서 — 추상화의 역사에 놓기

이 모든 변화가 어디에 놓이는지 역사적 관점으로 보면 선명해진다.
소프트웨어 역사에서 추상화는 한 방향으로만 올라왔다.

  • 기계어 → 어셈블리: 니모닉과 레이블의 도입.
  • 어셈블리 → C: 레지스터, 스택, 메모리 할당을 컴파일러에 맡김.
  • C → Java/Python: 수동 메모리 관리를 GC에 맡김.
  • Java/Python → 프레임워크: HTTP 파싱, ORM, 의존성 주입을 프레임워크에 맡김.
  • 프레임워크 → 서비스: 인증, 결제, 검색을 외부 서비스에 맡김.

각 단계에서 아래 층의 "결정"들이 비가시적이 됐다. 그 비가시성은 처음엔 두려운 것이었다. "GC가 언제 돌지 모르는데 어떻게 실시간 시스템을 짜냐" "ORM이 어떤 SQL을 쏠지 모르는데 어떻게 성능을 보장하냐." 이런 질문들이 그 시대의 엔지니어들을 실제로 밤잠 못 이루게 했다.
답은 매번 같았다. 한 층 위에서 추상화를 붙들면 아래 층의 비결정성은 관리 가능한 내부 디테일이 된다. GC의 동작은 JVM 튜닝 파라미터로 관리되고, ORM의 쿼리는 모니터링으로 잡히고, 서비스의 장애는 circuit breaker로 격리된다.
2026년 AI 코딩도 같은 위치에 있다. 개발자가 명세를 쓰고, 구현의 비결정성은 검증 층에서 흡수된다. 처음에는 두렵지만, 한 세대 지나면 "이 레벨에서 왜 걱정했지?"라고 돌이켜볼 추상화 층이 된다.


10. 마무리 — 주사위는 구르고, 빌드는 선다

글의 처음으로 돌아간다. 주사위를 굴려 나온 코드가 왜 빌드에 성공하는가.
세 층의 답을 정리하면.
첫 번째 층: 분포가 뾰족해서 대부분의 토큰은 사실상 결정적이다. "비결정적"이라는 꼬리표가 주는 인상만큼 혼란스럽지 않다.
두 번째 층: 우리가 "결정적 코드"라고 불렀던 것들도 인간의 비결정성 위에 서 있었다. 위치만 바뀐 것이지 전에 없던 성질이 생긴 것이 아니다.
세 번째 층: 현대 AI 코딩은 비결정적 생성과 결정적 검증의 루프로 설계된다. 생성의 변동성은 탐색의 도구이고, 품질은 검증의 엄격함에서 온다.
이 셋을 합쳐 보면 역설은 역설이 아니다. 비결정성은 문제가 아니라 구조다. 그리고 그 구조 위에서 잘 작동하는 개발 인프라를 가진 팀이 2026년의 승자가 될 것이다.
Paul Bernard가 에세이 끝에 던진 질문을 조금 바꿔 다시 쓴다.

LLM이 비결정적이라서 쓸 수 없다고 말하기 전에, 우리는 이전에 무엇을 믿고 있었는지 먼저 물어야 한다. 우리의 테스트? 우리의 리뷰? 우리의 직감? 그중 무엇도 "결정적"이지 않았다. 우리가 진짜로 믿었던 건 단 하나, 검증 가능한 명세를 세우는 우리의 능력이었다. 그 능력은 AI 시대에도 그대로 우리 것이고, 오히려 레버가 훨씬 길어진다.

주사위는 계속 구른다. 그런데 빌드는 선다. 그 사실이 우리에게 말하는 것은 이것이다. 우리는 이미 답을 알고 있었다. 답의 위치가 바뀌었을 뿐이다.


참고 자료

LIST

+ Recent posts