에이전트 기반 엔지니어링이 부상하는 시대에, 새로운 프로그래밍 언어가 왜 가능하고 또 필요할 수 있는지, 그리고 에이전트가 선호/기피하는 언어적·도구적 특성은 무엇인지에 대한 고찰.
Armin Ronacher의 생각과 글
2026년 2월 09일에 작성
작년에 나는 에이전트 기반 엔지니어링(agentic engineering)이 성장하는 지금, 프로그래밍 언어의 미래가 어떤 모습일지 처음으로 진지하게 생각하기 시작했다. 처음에는 방대한 기존 코드 코퍼스가 기존 언어들을 제자리에 고정시킬 거라고 느꼈지만, 이제는 오히려 그 반대가 사실일 수 있다고 생각하기 시작했다. 여기서는 왜 우리가 더 많은 새로운 프로그래밍 언어를 보게 될지, 그리고 흥미로운 혁신을 위한 공간이 꽤 넓은지에 대한 내 생각을 정리해 보려고 한다. 그리고 혹시 누군가 실제로 언어를 만들고 싶어 한다면, 우리가 무엇을 목표로 삼아야 하는지에 대한 생각도 덧붙인다!
에이전트가 자기 가중치(weights)에 들어 있는 언어에서 극적으로 더 잘 수행할까? 당연히 그렇다. 하지만 에이전트가 어떤 언어로 프로그래밍을 얼마나 잘하는지에 영향을 미치는 덜 자명한 요인들도 있다. 그 언어 주변의 도구(tooling)가 얼마나 좋은지, 그리고 변화(churn)가 얼마나 많은지 같은 것들이다.
Zig는 (적어도 내가 써본 모델들에서는) 가중치 안에서의 비중이 작아 보이고, 또 변화가 빠르다. 이 조합은 최적이 아니다. 그래도 완전히 못 쓸 정도는 아니다. 올바른 문서를 에이전트에게 가리켜 주기만 하면, 다가오는 Zig 버전으로도 프로그래밍할 수는 있다. 하지만 훌륭하진 않다.
반대로, 어떤 언어들은 가중치 안에서 잘 대표되어 있는데도 도구 선택 때문에 에이전트가 덜 성공하기도 한다. Swift가 좋은 예다. 내 경험상 Mac이나 iOS 앱을 빌드하는 주변 도구들이 너무 고통스러울 정도여서, 에이전트가 그걸 헤쳐 나가기가 어렵다. 이것도 별로다.
따라서 ‘이미 존재한다’는 이유만으로 에이전트가 잘 해내는 것도 아니고, ‘새롭다’는 이유만으로 에이전트가 고생하는 것도 아니다. 나는 한 번에 모든 것을 다 갈아엎지만 않는다면, 새 언어에도 충분히 적응해 갈 수 있다고 확신한다.
새 언어가 통할 수 있는 가장 큰 이유는 코딩 비용이 급격히 내려가고 있기 때문이다. 그 결과, 생태계의 폭(breadth)이 덜 중요해진다. 요즘 나는 예전 같으면 Python을 썼을 법한 곳에서도 JavaScript를 자주 집어 든다. JavaScript를 사랑해서도, 생태계가 더 좋아서도 아니다. 에이전트가 TypeScript에서 훨씬 더 잘하기 때문이다.
이렇게 생각하면 된다. 내가 선택한 언어에 중요한 기능이 없다면, 다른 언어의 라이브러리를 에이전트에게 보여 주고 포팅을 만들어 달라고 하면 된다. 구체적인 예로, 나는 최근 샌드박스용 호스트 컨트롤러를 구현하기 위해 JavaScript로 Ethernet 드라이버를 만들었다. Rust, C, Go에는 구현이 있었지만, 나는 JavaScript에서 플러그 가능하고 커스터마이즈 가능한 것을 원했다. 네이티브 바인딩에 맞춰 빌드 시스템과 배포를 억지로 맞추는 것보다, 에이전트에게 재구현하게 하는 편이 더 쉬웠다.
새 언어는 가치 제안(value proposition)이 충분히 강하고, LLM이 어떻게 학습하는지를 염두에 두고 진화한다면 통할 수 있다. 가중치에서의 비중이 낮아도 사람들은 채택할 것이다. 그리고 에이전트와 잘 맞게 설계된다면, 이미 잘 작동하는 것으로 알려진 친숙한 문법을 중심으로 설계될 수도 있다.
그렇다면 애초에 왜 새 언어가 필요할까? 이것을 생각해 보는 것이 흥미로운 이유는, 오늘날 많은 언어들이 ‘키를 두드리는 일’이 고된 노동이라는 가정 아래 설계되었기 때문이다. 그래서 우리는 간결함을 위해 어떤 것들을 희생했다. 예를 들어 많은 언어(특히 현대 언어들)는 타입을 일일이 쓰지 않도록 타입 추론에 크게 의존한다. 단점은 이제 표현식의 타입이 무엇인지 알아내기 위해 LSP나 컴파일러 에러 메시지에 의존해야 한다는 점이다. 에이전트도 이것 때문에 고생하고, 복잡한 연산이 들어간 PR 리뷰에서도 실제 타입이 뭔지 파악하기가 매우 어려워서 짜증이 난다. 완전 동적 언어는 이 점에서 더 나쁘다.
코드를 쓰는 비용은 내려가지만, 우리가 더 많은 코드를 생산하고 있기 때문에, 코드가 무엇을 하는지 이해하는 일이 더 중요해지고 있다. 리뷰할 때 모호함이 줄어든다면, 오히려 더 많은 코드를 쓰는 편이 좋을 수도 있다.
또 한 가지: 우리는 인간이 절대 보지 않고 기계만 소비하는 코드가 생기는 세계로 향하고 있다. 그런 경우에도, (잠재적으로 비프로그래머인) 사용자에게 지금 무슨 일이 벌어지는지에 대한 신호를 제공하고 싶다. 어떻게 하는지의 세부 사항으로 들어가지 않더라도, 코드가 무엇을 할지를 사용자에게 설명할 수 있어야 한다.
따라서 새 언어의 필요성은 이렇게 정리된다. 프로그래밍을 누가 하는지, 그리고 코드의 비용이 얼마인지에 대한 근본적 변화가 일어났으니, 적어도 새 언어를 고려해 볼 이유가 충분하다.
에이전트가 무엇을 원하는지 말하기는 어렵다. 에이전트는 거짓말을 하기도 하고, 자신이 본 코드에 영향을 받기도 한다. 하지만 에이전트가 얼마나 잘하고 있는지 추정하는 한 가지 방법은, 파일에 대해 얼마나 많은 변경을 수행하는지, 그리고 흔한 작업에 얼마나 많은 반복(iteration)이 필요한지를 보는 것이다.
내가 발견한 것들 중, 당분간은 사실로 남을 것 같은 것들이 있다.
언어 서버 프로토콜(Language Server Protocol)은 IDE가 코드베이스에 대한 의미론적 지식을 바탕으로 커서 아래의 것이 무엇인지, 무엇을 자동완성해야 할지를 추론할 수 있게 해준다. 훌륭한 시스템이지만, 에이전트에게 까다로운 특정 비용이 하나 있다: LSP가 실행 중이어야 한다.
에이전트가 LSP를 아예 실행하지 않는 상황이 있다. 기술적 한계 때문이 아니라, 그냥 게으르고 꼭 필요하지 않으면 그 단계를 건너뛰기 때문이다. 문서에서 예제를 주면, 그건 완전한 코드가 아닐 수도 있는 스니펫이라 LSP를 돌리기 쉽지 않다. GitHub 저장소를 가리켜서 개별 파일을 내려받게 하면, 에이전트는 그냥 코드를 훑어볼 뿐이다. 타입 정보를 위해 LSP를 세팅하지 않는다.
LSP가 있을 때와 없을 때의 경험이 둘로 갈라지지 않는 언어는, 더 많은 상황에서 하나의 통일된 방식으로 작업할 수 있게 해주기 때문에 에이전트에게 유리하다.
파이썬 개발자로서 이런 말을 하려니 괴롭지만, 공백 기반 들여쓰기는 문제다. 공백을 정확히 맞추는 데 필요한 토큰 효율(token efficiency)이 까다롭고, 의미 있는 공백(significant whitespace)을 가진 언어는 LLM이 다루기 더 어렵다. 특히 보조 도구 없이 LLM에게 외과 수술 같은 국소 변경(surgical changes)을 시키면 이 문제가 두드러진다. 종종 LLM은 의도적으로 공백을 무시하고, 코드를 켜고 끄기 위한 마커를 추가한 다음, 나중에 코드 포매터가 들여쓰기를 정리해 주길 기대한다.
반대로, 공백으로 분리되지 않은 중괄호도 문제를 일으킬 수 있다. 토크나이저에 따라 닫는 괄호가 연달아 나오는 부분이 놀랍게 토큰으로 쪼개지기도 한다(“strawberry” 개수 세기 문제 같은), 그래서 Lisp이나 Scheme 같은 언어는 LLM이 닫는 괄호를 몇 개 이미 내보냈는지/보고 있는지 추적을 잃어버리기 쉽다. 미래 LLM으로 해결될까? 물론이다. 하지만 이것은 도구 없이 인간도 맞추기 어려웠던 것이기도 하다.
이 블로그를 읽어온 사람들은 내가 async local과 실행 흐름 컨텍스트(flow execution context)를 강하게 믿는다는 걸 알지도 모르겠다. 즉, 콜 체인의 아래쪽 여러 층에서만 필요할 수 있는 데이터를 모든 호출을 통해 운반할 수 있는 능력이다. 관측가능성(observability) 회사에서 일하면서, 이것의 중요성이 더욱 뼈저리게 와 닿았다.
문제는 암묵적으로 흐르는 모든 것이 설정되어 있지는 않을 수 있다는 점이다. 예를 들어 현재 시간을 보자. 모든 함수에 타이머를 암묵적으로 전달하고 싶을 수 있다. 그런데 타이머가 설정되어 있지 않은데 갑자기 새 의존성이 등장하면 어떡하나? 모든 것을 명시적으로 전달하는 것은 인간과 에이전트 모두에게 번거롭고, 나쁜 지름길이 생기기 쉽다.
내가 실험해 본 한 가지는, 코드 포매팅 단계에서 함수에 effect 마커를 추가하는 것이다. 함수는 현재 시간이나 DB가 필요하다고 선언할 수 있지만, 이를 명시적으로 표시하지 않으면 사실상 린팅 경고가 되고, 자동 포매팅이 그걸 고쳐 준다. LLM이 함수 안에서 현재 시간 같은 것을 사용하기 시작하면, 기존 호출자(callers)는 경고를 받는다. 그리고 포매팅이 애노테이션을 전파한다.
이 방식이 좋은 점은, LLM이 테스트를 만들 때 이러한 부작용을 정확히 목킹(mocking)할 수 있다는 것이다. 에러 메시지를 통해 무엇을 공급해야 하는지 이해할 수 있다.
예를 들어:
textfn issue(sub: UserId, scopes: []Scope) -> Token needs { time, rng } { return Token{ sub, exp: time.now().add(24h), scopes, } } test "issue creates exp in the future" { using time = time.fixed("2026-02-06T23:00:00Z"); using rng = rng.deterministic(seed: 1); let t = issue(user("u1"), ["read"]); assert(t.exp > time.now()); }
에이전트는 예외를 힘들어한다. 예외를 무서워한다. 이게 RL(강화학습)로 얼마나 해결될 수 있는지는 모르겠지만, 지금의 에이전트는 가능한 모든 것을 catch하려 들고, 로그를 남기고, 꽤 형편없는 복구를 한다. 에러 경로에 대해 실제로 उपलब्ध한 정보가 적다는 점을 생각하면 이해가 간다.
체크드 예외(checked exceptions)는 한 가지 접근이지만, 콜 체인 끝까지 전파되고 극적으로 개선되지는 않는다. 설령 린터가 어떤 에러가 전파될 수 있는지 추적해서 힌트로 제공한다 해도, 여전히 많은 호출 지점을 조정해야 한다. 그리고 컨텍스트 데이터에 대해 제안했던 자동 전파처럼, 이 또한 올바른 해법이 아닐 수도 있다.
아마 정답은 타입이 있는 result(typed results)를 더 강하게 밀어붙이는 것일지 모른다. 하지만 이를 합성 가능하게 만들려면, 그것을 지원하는 타입 및 객체 시스템이 있어야 해서 여전히 까다롭다.
오늘날 에이전트가 파일을 메모리로 읽는 일반적 접근은 라인 기반이다. 그래서 여러 줄 문자열을 가로지르는 청크를 자주 선택한다. 이게 쉽게 무너지는 사례: 긴 임베디드 코드 문자열(본질적으로 코드 생성기)을 포함한 2000라인짜리 파일에서 에이전트를 일하게 해보라. 에이전트는 종종 그 멀티라인 문자열 내부를 실제 코드라고 착각하고 편집한다. 멀티라인 문자열에 대해 내가 아는 한 좋은 해법을 가진 언어는 Zig뿐인데, Zig의 프리픽스 기반 문법은 대다수에게 꽤 낯설다.
리포매팅(reformatting)도 종종 구문을 다른 라인으로 이동시킨다. 많은 언어에서 리스트의 trailing comma는 지원되지 않거나(JSON), 관례적이지 않다. diff 안정성을 원한다면, 리포매팅이 덜 필요하고 멀티라인 구문을 대체로 피하는 문법을 목표로 하게 될 것이다.
Go에서 정말 좋은 점은, 다른 패키지의 심볼을 스코프 안으로 가져올 때 대부분 모든 사용이 패키지 이름으로 접두(prefix)되어야 한다는 것이다. 예: Context가 아니라 context.Context. 탈출구(import alias나 dot-import)가 있긴 하지만 비교적 드물고 대체로 권장되지 않는다.
이건 에이전트가 자신이 보고 있는 것이 무엇인지 이해하는 데 큰 도움이 된다. 일반적으로 가장 기본적인 도구로도 코드를 찾을 수 있게 만드는 것은 훌륭하다. 인덱싱되지 않은 외부 파일에서도 작동하고, 즉석에서 생성된 코드로 구동되는 대규모 자동화(예: sed, perl 호출)에서 거짓 양성(false positives)도 줄여 준다.
내가 말한 많은 것들은 결국 이렇게 요약된다: 에이전트는 로컬 추론(local reasoning)을 정말 좋아한다. 에이전트는 종종 문맥에 몇 개 파일만 로드한 채로 작업하고, 코드베이스에 대한 공간적 인식(spatial awareness)이 크지 않다. grep 같은 외부 도구에 의존해 찾고, grep하기 어렵거나 정보를 다른 곳에 숨기는 것은 까다롭다.
여러 언어에서 에이전트의 성공/실패를 가르는 것은 빌드 도구가 얼마나 좋은가이다. 많은 언어는 교차 참조가 너무 많아서 실제로 무엇을 다시 빌드하거나 재테스트해야 하는지 결정하기가 어렵다. Go는 여기서 매우 좋다. 패키지 간 순환 의존성(import cycle)을 금지하고, 패키지는 명확한 레이아웃을 가지며, 테스트 결과는 캐시된다.
에이전트는 매크로를 자주 힘들어한다. 인간도 매크로를 어려워한다는 점은 이미 꽤 분명했지만, 매크로를 옹호하던 논리는 대개 ‘코드 생성을 통해 작성해야 할 코드가 줄어든다’는 것이었다. 이제는 그 걱정이 덜하니, 매크로 의존이 적은 언어를 지향해야 한다.
제네릭과 comptime에 대해서는 별도의 질문이 있다. 나는 이것들은 다소 더 잘 버틴다고 생각한다. 대체로 같은 구조를 다른 플레이스홀더로 생성하는 것이고, 에이전트가 이해하기가 훨씬 쉽기 때문이다.
grep 가능성과 관련해서: 에이전트는 배럴 파일(barrel files)을 이해하기 어려워하고, 싫어한다. 클래스나 함수가 어디에서 오는지 빠르게 알아낼 수 없으면, 잘못된 곳에서 import하거나, 아예 빠뜨리기도 하고, 너무 많은 파일을 읽느라 컨텍스트를 낭비한다. 어떤 것이 선언된 위치와 import되는 위치 사이에 1:1 매핑이 있는 것은 훌륭하다.
그렇다고 지나치게 엄격할 필요도 없다. Go는 어느 정도 그 방향인데, 극단적이지는 않다. 디렉터리 안의 어떤 파일이든 함수를 정의할 수 있는데, 최적은 아니지만 충분히 빨리 찾을 수 있고 멀리까지 검색할 필요가 없다. 패키지가 충분히 작도록 강제되기 때문에 grep으로 전부 찾을 수 있다.
최악의 경우는, 구현을 디스크 상의 어떤 자명하게 재구성 가능한 위치로부터 완전히 분리해 버릴 정도로 여기저기 자유롭게 재-익스포트하는 것이다. 혹은 더 나쁜 것: aliasing.
에이전트는 alias가 끼어들면 종종 싫어한다. 사실, alias가 많은 코드를 리팩터링하게 하면, 생각 블록(thinking blocks)에서조차 불평하게 만들 수도 있다. 이상적으로는 언어가 좋은 네이밍을 장려하고, 결과적으로 import 시 aliasing을 억제해야 한다.
flaky 테스트를 좋아하는 사람은 없지만, 에이전트는 더더욱 싫어한다. 아이러니하게도 에이전트는 애초에 flaky 테스트를 만드는 데 특히 능하긴 하다. 이는 현재 에이전트가 목킹을 좋아하고, 대부분의 언어가 목킹을 잘 지원하지 않기 때문이다. 그래서 많은 테스트가 우연히 동시성 안전(concurrency safe)하지 않거나, 개발 환경 상태에 의존해서 CI나 프로덕션에서 상태가 달라지곤 한다.
대부분의 프로그래밍 언어와 프레임워크는 flaky하지 않은 테스트보다 flaky 테스트를 더 쉽게 쓰게 만든다. 이는 곳곳에서 비결정성(indeterminism)을 장려하기 때문이다.
이상적인 세계라면 에이전트에게는 명령 하나가 있다. 린트와 컴파일을 수행하고, 모두 잘됐는지 알려 준다. 그리고 필요한 모든 테스트를 돌리는 또 다른 명령 하나가 있을 수도 있다. 하지만 실제 환경은 대부분 그렇지 않다. 예를 들어 TypeScript는 종종 타입 체크에 실패해도 코드를 실행할 수 있다. 이것은 에이전트를 가스라이팅할 수 있다. 마찬가지로 번들러 설정이 여러 가지면, 어떤 설정에서는 성공하는데 CI에서 약간 다른 설정으로 나중에 실패할 수 있다. 도구가 균일할수록 좋다.
이상적으로는 ‘돌아가거나 안 돌아가거나’ 둘 중 하나여야 하고, 가능한 많은 린팅 실패는 기계적으로 고칠 수 있어야 해서 에이전트가 수작업으로 고치지 않도록 해야 한다.
그럴 거라고 생각한다. 우리는 그 어느 때보다 더 많은 소프트웨어를 쓰고 있다. 더 많은 웹사이트, 더 많은 오픈소스 프로젝트, 더 많은 모든 것. 새 언어의 비율이 같게 유지된다 해도 절대 수는 늘어난다. 하지만 나는 더 많은 사람들이 소프트웨어 엔지니어링의 기초와 우리가 쓰는 언어들을 기꺼이 다시 생각해 보려 할 거라고 진심으로 믿는다. 예전에는 언어가 뜨려면 많은 인프라를 구축해야 한다는 느낌이 있었지만, 이제는 꽤 좁은 사용 사례를 타깃으로 할 수 있다. 즉, 에이전트가 행복하게 만들고, 그 다음 인간을 향해 확장하는 것이다.
나는 두 가지를 보게 되길 바란다. 첫째, 아웃사이더 아트(outsider art): 언어를 만들어 본 적 없는 사람들이 도전해서 새로운 것을 보여 주는 것. 둘째, 무엇이 작동하고 무엇이 작동하지 않는지를 제1원리부터 문서화하려는 훨씬 더 의도적인 노력. 우리는 사실 좋은 언어가 무엇인지, 대규모 팀에서 소프트웨어 엔지니어링을 어떻게 확장하는지에 대해 많은 것을 배웠다. 하지만 그것이 ‘소비 가능한 개요’ 형태로 글로 정리된 것을 찾기는 매우 어렵다. 너무 많은 부분이 하드 팩트가 아니라 다소 무의미한 것들에 대한 취향과 의견에 의해 형성되어 왔다.
하지만 이제 우리는 천천히 사실이 더 중요해지는 지점으로 가고 있다. 에이전트가 그것으로 얼마나 잘 수행하는지를 보면 무엇이 작동하는지 실제로 측정할 수 있기 때문이다. 어떤 인간도 설문조사의 대상이 되고 싶어하지 않지만, 에이전트는 신경 쓰지 않는다. 우리는 그들이 얼마나 성공하는지, 어디에서 고전하는지를 볼 수 있다.
이 글은 ai 태그가 달려 있다.
다른 형식으로 복사 / 보기 마크다운
© Copyright 2026 Armin Ronacher.
콘텐츠는 Creative Commons attribution-noncommercial-sharealike License로 라이선스됩니다.
추가 정보: imprint&AI 투명성. 구독: atom / RSS.
색상 스킴: auto , light , dark .