AI는 개발 언어를 어떻게 바꾸고 있는가 — 70년 전 어셈블리에 일어난 일

1950년대 어셈블리 코드와 천공 카드에서 2020년대 AI 코드 생성으로 이어지는 타임라인

Kotlin을 만든 안드레이 브레슬라프가 올해 CodeSpeak을 공개했다. 구상은 이렇다. 사람이 마크다운으로 스펙을 쓰고, LLM이 그 스펙을 읽어 파이썬이나 타입스크립트로 컴파일한다. 반응은 엇갈렸는데, 문법도 파서도 타입 시스템도 없는 것을 언어라고 부를 수 있느냐는 지적이 제일 많았다.

논쟁을 따라가다 보니 나는 다른 것이 궁금해졌다. 만약 CodeSpeak이 정말 잘 동작해서 사람이 파이썬 코드를 더 이상 읽지 않게 된다면, 그때 우리는 굳이 파이썬을 써야 할 이유가 있을까.

물론 이 질문이 처음 나온 것은 아니다. 70년 전에도 사람들은 어셈블리를 두고 똑같은 질문을 했고, 그때 이미 답을 얻었다. 사람이 읽지 않게 된 언어는 직접 쓰는 코드가 아니라 기계가 뽑아내는 결과물이 된다. 어셈블리가 딱 그렇게 됐고, 지금은 어셈블리를 손으로 쓰거나 커밋하는 사람이 없다.

여기서 스펙(Spec)은 무엇을 만들지 사람 말로 적어 커밋하는 파일을 뜻한다. 그 스펙을 읽어 코드를 만드는 LLM을 이 글에서는 생성기라고 부른다.


▍포트란의 등장

1954년, IBM의 존 백커스 팀은 포트란을 만들기 시작했다. 그때 프로그래머들은 컴파일러가 뱉은 코드를 믿지 않았는데, 손으로 짠 어셈블리가 실제로 더 빨랐으니 그럴 만도 했다.

그래서 포트란 팀이 스스로 정한 목표는 문법의 편리함이 아니라 성능이었다. 사람이 짠 것만큼 빠른 코드가 나오지 않으면 아무도 포트란을 쓰지 않는다는 사실을 그들도 알고 있었다. 1957년 4월에 나온 첫 컴파일러는 그 목표를 대체로 달성했고, 어셈블리는 그때부터 20년쯤에 걸쳐 주류에서 밀려났다.

물론 지금도 우리는 어셈블리를 읽는다. 다만 프로파일러가 이상한 값을 뱉거나 컴파일러가 의심스러울 때 어셈블리를 열어보는 정도이고, 평소에는 아무도 그것을 커밋하지 않는다.

여기서 짚고 넘어갈 게 있는데, 컴파일러가 신뢰를 얻은 건 빨라서만이 아니라 출력이 결정적(deterministic)이었기 때문이다. 같은 입력에 같은 출력이 나오고, 결과가 이상하면 어셈블리를 열어 확인할 수 있고, 버그가 생기면 재현해서 리포트할 수 있었다. 사람들이 어셈블리를 읽지 않기로 마음먹은 것이 아니라, 컴파일러를 믿게 되면서 읽을 필요가 없어진 것이다.


▍고수준 언어의 역할

Kotlin에서 data class를 쓰면 equals, hashCode, toString, copy가 붙은 클래스가 나온다. 하지만 이 문법이 실행 파일에 없던 기능을 만들어주지는 않는다. 사람이 손으로 쓰던 코드를 대신 써줄 뿐이다. 파이썬 데코레이터도, 타입스크립트 유니온 타입도 마찬가지다.

다만 타입은 예외다. 타입은 옵티마이저에게 진짜 정보를 주는데, 컴파일러가 이 값이 절대 null이 아니라는 사실을 알면 검사 코드를 지울 수 있고, 정수 범위를 알면 레지스터 할당이 달라진다. 타입만 사람과 기계 양쪽에 쓸모가 있다.

타입을 뺀 나머지는 전부 사람을 위해 개발된 것이다. 덕분에 우리는 코드를 쉽게 읽고 고치며, 여럿이 나눠 맡아 작업할 수 있다. 이름을 붙이고 모듈을 나누고 들여쓰기를 맞추는 일이 모두 그래서 생겼다.

그렇기에 사람이 코드를 읽지 않는 순간 그 존재 이유는 통째로 사라진다. 기계가 필요로 하는 것은 타입 하나뿐인데, 그마저도 이미 IR이 더 정확하게 표현한다.


▍사람이 읽지 않게 된 언어들

이러한 운명을 맞이한 언어는 어셈블리 뿐만이 아니다.

대상지금 위치사람이 읽는 빈도
어셈블리컴파일러 출력프로파일링과 컴파일러를 의심할 때
JavaScript 번들TypeScript · wasm의 타깃소스맵으로 원본을 대신 본다
JVM 바이트코드처음부터 중간 표현거의 읽지 않는다
LLVM IR처음부터 중간 표현컴파일러를 만드는 쪽
SQL 실행 계획옵티마이저가 정한다느릴 때 EXPLAIN을 본다

JavaScript에는 현재 많은 변화가 있다. 번들러가 뱉은 main.a3f9.js를 열어보는 사람은 없고, 우리는 소스맵을 켜서 원본을 본다. 브라우저에게 JS는 이미 실행 포맷이지만 사람에게는 아직 소스이니, JS는 지금 두 자리를 겸하고 있는 셈이다.

SQL은 좀 다른데, 애초에 방법을 사람이 정하지 않는다. 우리는 WHEREJOIN으로 무엇을 원하는지만 적고, 인덱스를 탈지 해시 조인을 할지는 옵티마이저가 정한다. 통계가 바뀌면 같은 쿼리인데도 실행 계획이 달라지지만, 아무도 그것을 문제 삼지 않는다. 사람들은 결과만 같으면 과정에는 그다지 큰 관심을 두지 않는다는 뜻이다. 1970년대도 그랬다.

지금까지 이야기한 것을 그림으로 정리하면 이렇다.

현재요구사항문서 · 이슈 · 머릿속버전 관리 밖고수준 언어Kotlin · Python · TypeScriptIR · 바이트코드LLVM IR · JVM bytecode어셈블리x86-64 · ARM64기계어CPU instruction여기부터 산출물미래스펙구조화된 인간 언어고수준 언어Kotlin · Python · TypeScriptIR · 바이트코드LLVM IR · JVM bytecode어셈블리x86-64 · ARM64기계어CPU instruction여기부터 산출물경계가 한 칸 올라가면 고수준 언어는 어셈블리와 같은 자리에 놓인다

▍LLM이 파이썬을 뱉는 이유

우리가 LLM에게 기능을 하나 만들어달라고 하면 대개 파이썬이나 타입스크립트가 나온다. 그런데 왜 하필 그 둘일까. 성능 때문도 아니고 생성 타깃으로 알맞아서도 아니다. 깃허브에 그 두 언어가 제일 많이 쌓여 있어서다. LLM은 많이 본 것을 뱉는다.

이것은 컴파일러가 타깃을 고르는 것과 성격이 다르다. LLVM이 x86-64를 뱉는 이유는 CPU가 그것만 실행하기 때문이다. 반면 LLM이 파이썬을 뱉는 이유는 사람이 파이썬을 많이 썼기 때문이다. CPU는 우리가 바꿀 수 없지만, 학습 데이터는 언제든 바뀐다.

만약 사람이 생성 결과를 읽지 않게 된다면, 파이썬이라는 도구는 장점이 사라지고 오로지 단점만 남는다. 인터프리터를 거쳐야 하고, GIL을 안고 가야 하고, 패키지 의존성을 계속 관리해야 한다. 아무도 읽지 않는 코드를 우리가 읽기 좋게 유지할 이유는 없다.

그 비용이 드러나는 시점도 짐작할 수 있다. 지금은 사람이 손으로 쓰니 코드가 늘어나는 속도에 한계가 있어서, 비효율이 섞여도 그 양이 얼마 되지 않는다. 하지만 생성기가 만드는 코드가 사람이 읽을 수 있는 양을 넘어서면, 아무도 읽지 않은 수십만 줄의 불필요한 할당과 중복 호출이 매달 부채로 남는다.

그러면 우리는 포트란 팀이 받았던 질문을 다시 받게 될 확률이 높다. 이는 사람이 손으로 짜는 것만큼 효율적인가. 그 질문에 답해야 한다면 스펙에서 파이썬을 생략하고 곧장 IR로 가는 편이 효율적이라고 할 수 있겠다. IR에서 기계어를 뽑는 구간에는 반세기 넘게 쌓아 온 최적화가 이미 들어 있기 때문이다.


▍AI 코딩에 가장 필요한 조건

포트란이 성공한 이유에는 성능뿐만 아니라 결정성도 있었는데, LLM은 아직 이 조건을 갖추지 못했다. 같은 스펙을 두 번 컴파일하면 다른 코드가 나오고, 컴파일러 버그는 재현되는데 LLM의 오생성은 재현되지 않는다. CodeSpeak을 두고 나온 지적 중에 제일 날카로웠던 것도 이 부분이다.

그렇기에 최근 도구들은 저마다 다른 검증 단계를 둔다.

도구검증
Dafny + LLM형식 검증기 통과
Marsha생성된 테스트 통과
Tessl스펙 레지스트리로 API 사용법 고정
CodeSpeak요구사항과 코드의 매핑 유지
Spec Kit · Kiro단계마다 사람이 검토

위 방식들이 서로 달라 보여도 하는 일은 같다. 반드시 지켜야 할 조건을 하나 정해두고, 그것만 통과하면 코드가 어떻게 되어있는지는 체크하지 않는다.

그런데 생각해보면 이건 컴파일러가 이미 하던 일이다. 예를 들어 gcc -O2로 만든 바이너리는 -O0과 완전히 다르게 생겼는데 아무도 신경 쓰지 않는다. 겉으로 보이는 동작이 같다는 보장이 있어서다. 중요한 것은 결정성이 아니라 동작이 같다는 보장이고, 결정성은 그 보장을 얻는 제일 쉬운 방법 중 하나이다.


▍src를 gitignore하는 저장소

그렇다면 프로젝트 구조는 어떻게 바뀔까. dist/가 이미 .gitignore에 있듯이 아마 src/도 그 안에 들어가고, 커밋에 올라오는 건 스펙과 테스트뿐일 것이다. 이에 따라 생겨날 변화를 적어보면 이렇다.

  • 코드 리뷰가 스펙 리뷰가 된다. 지금 PR에서 오가는 변수명 논쟁이나 널 체크 지적은 생성기 몫이 되고, 사람은 “이 조건에서 무엇을 반환해야 하나”만 본다
  • 버그 리포트가 가리키는 대상이 바뀐다. 스택 트레이스는 아무도 읽지 않는 코드를 가리키니 쓸모가 줄고, 대신 어떤 요구사항이 비어 있었는지가 리포트의 본문이 된다
  • git blame이 찾는 사람이 바뀐다. 그 줄을 쓴 사람이 아니라 그 요구사항을 정한 사람을 찾는다
  • 배포마다 코드가 달라져도 문제가 아니다. 컴파일러를 올렸을 때 바이너리가 달라지는 것과 같은 일이 된다

여기까지는 어셈블리에 이미 벌어졌던 일이고, 대상만 어셈블리에서 고수준 언어로 바뀌었을 뿐이다. 하지만 한 가지가 다르다. 어셈블리는 읽으면 알 수 있었다. 컴파일러가 의심스러울 때 우리는 어셈블리를 열어보고 컴파일러가 맞았는지 틀렸는지 판단할 수 있었고, 그래서 평소에는 읽지 않아도 안심할 수 있었다.

물론 LLM이 생성한 코드도 우리가 열어볼 수는 있다. 다만 컴파일러에는 명세가 있어서 출력이 왜 그렇게 나왔는지 따라갈 수 있지만, LLM에는 그런 명세가 없다. 코드를 열어봐도 이것이 맞게 나온 것인지 판단할 기준이 없다는 뜻이다. 어셈블리는 읽을 일이 없어졌을 뿐이지만, 생성된 코드는 읽어도 답이 나오지 않는다. 이 차이를 채울 방법은 아직 등장하지 않았다.


▍스펙이 새로운 언어가 되는 이유

카파시가 2023년에 이런 말을 했다. “가장 인기 있는 새 프로그래밍 언어는 영어다.” 그때는 농담에 가까웠는데 지금은 거진 사실이 됐다. 하지만 사람의 언어는 여전히 모호한 면이 많다.

“사용자가 로그인하면 대시보드를 보여준다”만 놓고 보면 정해지지 않는 것이 너무 많다. 로그인이 실패하면? 세션이 만료됐으면? 권한이 없는 사용자면? 대시보드 데이터를 못 가져오면? 사람끼리는 나머지를 알아서 채우고 넘어가는데, 생성기는 매번 다르게 채운다.

그러니 우리는 모호함을 줄여야 하고, 줄이려면 구조가 필요하다. CodeSpeak이 자기 문법을 structured human language라고 부르고 Kiro가 EARS 표기를 쓰는 이유가 여기에 있다. 여기에 재사용을 붙이면 모듈이 생기고, 스펙끼리 참조하면 의존성이 생기고, 같은 개념을 여러 곳에서 쓰면 이름과 범위가 필요해진다.

그 뒤로는 문법이 있고 모듈이 있고 의존성이 있고 이름이 있다. 이쯤 되면 이것은 그냥 컴퓨터 언어와 다름없다.

프로그래밍이라는 도구가 영영 사라지는 것이 아니다. 구현 도구가 코드에서 스펙으로 옮겨가고, 그 스펙이 새로운 언어가 될 뿐이다. 포트란이 어셈블리의 반복 구문에 이름을 붙이며 시작했듯이, 스펙 언어도 프롬프트에서 반복되는 패턴에 이름을 붙이며 시작한다. 어쩌면 브레슬라프가 Kotlin 다음으로 이 자리를 고른 것도 우연이 아닐 것이다.


▍마무리

프레드 브룩스는 1986년에 소프트웨어의 어려움을 본질우연으로 나눴다. 무엇을 만들지 정하고 그 구조를 짜는 일이 본질이고, 그걸 특정 언어와 도구로 옮기는 일이 우연이다.

40년에 걸쳐 점점 사라진 건 전부 우연에 해당하는 부분이었다. 수동 메모리 관리, 빌드 스크립트, 의존성 충돌, 널 체크를 이제 아무도 손으로 하지 않는다. 문법과 프레임워크를 익히는 일도 마찬가지다.

이제 남은 건 본질이다. 무엇을 만들지 정하고, 모순 없이 적고, 빠짐없이 적는 일이 여기에 해당한다. 스펙을 쓰든 코드를 쓰든 이 일 자체는 그대로 남고, 오히려 개발자가 여기에 쓰는 시간의 비중은 커진다. 문법과 프레임워크에 쓰던 시간이 이제 그 일로 옮겨가기 때문이다.

그렇다면 우리가 지금 할 일은 분명하다. 새로 나오는 스펙 도구를 하나씩 따라가는 것보다, 요구사항을 모순 없이 적는 연습을 하자. 도구는 또 바뀌겠지만, 구조화 능력은 사람이 코드를 쓰든 스펙을 쓰든 그대로 활용될 가능성이 높다.


▍참고 자료