마지막으로 LLM 에이전트의 도움 없이 코드를 작성한게 언제였는지 기억이 잘 나지 않습니다. 사실 이제 혼자서 코드를 짜는게 조금 어색하기까지 합니다. 기본적인 디버깅과 코드 초안 작성을 에이전트에게 맡기고 있으며, 그렇게 생성한 코드 대부분은 약간의 검토를 거쳐 코드베이스에 편입되고 있습니다. 코드 작성 뿐만 아니라 코드를 리뷰하고 잠재적인 취약점이나 개선할 부분들을 잡아내는 역할도 맡기고 있습니다.
한편, 잠도 안 자고 쉬지도 않는 에이전트의 “기계성” 덕분에 기존과 완전히 다른 호흡의 프로그래밍이 가능해졌습니다. 이제 책상 앞에 앉아서 코드를 지켜볼 필요도 없습니다. 자리에 누운 채로 휴대전화로 채팅을 치듯이 원하는 것을 보내기만 하면 곧 결과가 나옵니다. 코드가 푸시되고 PR이 게시되며 빌드와 테스트까지 수행됩니다.
여기서 우리는 종종 이런 물음을 가지게 됩니다. “이제 사람은 무슨 일을 해야 하는가?”
자연어의 한계와 형식 언어의 필요성
에츠허르 다익스트라 선생님은 자연어로는 프로그래밍을 할 수 없다고 말합니다. 자연어는 본질적으로 모호하기 때문입니다. 자연어는 맥락에 따라 다르게 해석될 수 있고, 생략 등 정보의 손실이 있어도 “작동”하도록 진화해왔습니다.
프로그램은 결국 CPU가 읽을 수 있는 바이너리 수준의 산출물을 결정론적으로 유도할 수 있어야 합니다. 해석 주체(컴파일러, 인터프리터)가 코드를 보았을 때, 이것으로밖에 번역할 수밖에 없는, 다른 해석의 여지가 없는 명료한 서술이어야 합니다.
당연히, 자연어는 이 용도로 부적절합니다. 기계에게는 아무 맥락 없이 던져주어도 실행할 수 있는 가장 확실하고 엄밀한 형식 언어가 필요합니다.
오늘날의 프로그래밍 언어는 이 역할을 아주 잘 수행합니다. 인간에게 그나마 가까우면서도 기계가 받아들일 수 있는 결정론적 엄밀함을 보장합니다.
프로그래밍의 본질적인 어려움은 모호성
프로그래밍의 어려움은 본질적으로 모호성에서 출발합니다. 인간의 생각은 추상적인데 프로그래밍의 결과물은 엄밀해야 하기 때문입니다.
개발자는 자신에게 주어진 문제를 풀어가는 사고의 과정을 엄밀한 형식 언어로 기술해야 합니다. 사람이 보기에는 간단한 일이더라도, 실제로 컴퓨터에게 시키려면 먼저 추상적 사고를 치밀하게 타고 내려가 숨겨져 있던 모든 복잡성과 의존관계를 파악하고 구체화해야 합니다. 그런 다음에야 실제로 작동하는 코드가 작성될 수 있습니다.
그래서 전통적인 프로그래밍 과정에서 코드를 타이핑하는 과정은 사실은 이미 정리된 심상 모형을 꺼내 나열하는 것에 불과하고, 진짜 프로그램이 만들어지는 순간은 이 모호성을 하나씩 해체하며 구체적인 설계가 형성되는 순간이라고 생각합니다.
오늘날의 형식 언어는 엄밀함을 유지하면서도 추상화 수준을 천천히 끌어올리는 인간 친화적 방향으로 진행되어 왔습니다. 말처럼 술술 읽히는 코드를 작성할 수 있게 된 것도 이러한 흐름 덕분입니다.
그러나 LLM 기반의 에이전트는 이 문제를 아주 다르게 풀어버립니다. 사람이 엄밀한 언어를 편하게 다룰 수 있게 해주는 대신, 애초에 자연어 프롬프트에서 시작하여 형식 언어를 생산해주는 것입니다. 기존에 프로그래머가 감당해야 했던 모호성을 대신 풀어주는 것입니다.
그럼에도 해결의 주체가 달라졌을 뿐, 프로그래밍의 모호함을 누군가는 풀어야 한다는 사실은 변하지 않습니다. 프로그래밍의 어려움은 사라지지 않습니다. 다만 이 모호성을 논리로 변환시키는 과정을 LLM에게 외주 주는 것입니다.
LLM과 함께라면 자연어 프롬프트는 프로그램이 될 수 있는가?
이 질문은 자연어로 프로그램을 작성할 수 있는가? 를 묻는 것이 아닙니다. LLM이라는 중간 단계를 놓는다면 자연어 자체로 프로그램이 될 수 있는가? 를 묻는 것입니다.
당연히 안 됩니다.
소스 코드가 결정론적으로 작성되었다면 바이너리도 결정론적으로 작동합니다. 컴파일은 의미 보존 변환이기 때문입니다. 그러나 LLM이 자연어 프롬프트로부터 소스 코드를 생성하는 과정은 의미 보존 과정이 아닙니다. 그저 이 자연어 명세라면 이걸 만족하는 가장 그럴듯한 형식 언어는 이것이다를 반복하는 확률적 근사입니다.
자연어로 아무리 구체적인 지침을 제시하더라도, 그렇게 생성된 소스 코드는 정확(의도에 부합)하지도 않고 심지어 생성할 때마다 달라지기까지 합니다. 이는 프로그램이 기본적으로 갖추어야 할 결정론적 특징을 상실한 모습입니다. 따라서 자연어 자체는 그 모호하고 생략적인 특성 탓에 근본적으로 프로그램이 될 수 없습니다. LLM과 함께일지라도요.
다만 자연어 자체를 프로그램으로 보지 않고 그저 프로그램 소스 코드를 생성해줄 지침 정도로만 다룬다면
굉장히 높은 확률로 작동하고 실제로 쓸만하기도 한 코드를 생성해줄 뿐입니다.
바이브 코딩의 위치
물론 “설계 의도”의 통제 범위를 프로그램 내부의 구성과 작동 방식보다 느슨하게 잡아 겉으로 출력하는 결과로만 본다면, 구현체가 다르더라도 똑같은 결과를 내보이는 프로그램은 모두 설계 의도를 만족하는 프로그램으로 볼 수 있습니다.
이렇게 본다면 프로그래밍의 모호성을 에이전트가 대신 책임지며 개발자는 자연어로 지시만을 내리는 프롬프트-결과물 루프에서 만족할만한 결과가 나왔을 때, 이 또한 설계 의도를 만족하는 프로그램이라고 불러도 되겠습니다. 다만 작동 결과 수준에서 의도를 만족할 뿐이지 내부 형식 언어의 설계에서는 지시자의 의도를 온전히 반영하였을 가능성이 없으니(심지어 지시자의 형식 언어 구현 아이디어가 존재하지 않을 수도 있으니) 그것은 “형식 언어로 구현된 프로그램”의 관점에서 보면 LLM이 모호성 해체와 구체화를 담당한, LLM의 프로그램이라고 부르는 것이 합당하겠습니다.
그래서 사람은 무엇을 해야 하는가
소스 코드는 “프로그램”이라고 부를 수 있는 가장 추상적이고 인간적인 언어입니다. 따라서 프로그램의 작동을 엄밀하게 제어하고 싶다면 누군가는 이 형식 언어가 설계 의도와 벗어나지 않도록 유지해야 합니다.
LLM은 절대로 프롬프트에 담긴 의도를 100% 반영하는 소스 코드를 생산할 수 없습니다. 우리가 사용하는 자연어 프롬프트에는 필연적으로 정보의 누락이 발생하는데, 이 정보들을 LLM이 알 방법이 없기 때문입니다. 따라서 LLM이 제한된 정보만을 가지고 확률적으로 생성하는 결과물이 우리의 의도에 들어맞는지 계속 지켜보고 검증해야 합니다.
그리고… LLM 에이전트에게 맡기기에 적절한 일
LLM은 자신의 해석이 반영된 형식 언어를 생산합니다. 따라서 이 해석이 프로그래머의 의도와 벗어날(작동 결과가 동일할지라도) 수 있다는 것을 염두에 두어야 합니다.
LLM은 정보의 한계(인간의 모든 상태와 의도를 파악할 수 없음)로 인간의 결정과 창작을 온전히 대신할 수는 없습니다. 하지만 정답을 맞추는 문제 대신, 특정 유형의 문제에 강합니다.
“웹 사이트를 만들어줘” 같은 요청은 어렵습니다. 주어진 정보의 절대적인 양이 적기 때문입니다. 인간이 이러한 모호함을 힘들어하듯이, LLM도 이것을 어려워합니다.
대신 이미 정보가 존재하는 상황에서 이들을 다른 형태로 바꾸는 것에 특화되어 있습니다. 가령 복잡한 코드 변경에 대한 이유를 요약해달라는 요청이나, 로그를 보고 무슨 일이 일어나고 있는지 알려달라는 요청에는 아주 잘 답변합니다.
또한 같은 관념을 다른 표현 방식으로 번역하는 것에 능합니다. 코드에 녹아 있는 정보를 자연어로 표현할 수도 있고, 다이어그램으로 보여줄 수도 있고, 적절한 비유를 가지고 와서 설명해줄 수도 있습니다.
인간의 사고는 직관과 기억과 체력에 크게 의존하지만, 에이전트는 언제나 최상의 상태로 같은 작업을 몇 번이고 수행할 수 있습니다. 사람이 지치거나 바빠서 미처 들여다보지 못한 부분을 파악하고 잡아내는 보조적 역할을 굉장히 잘 수행합니다.
댓글