본문은 요즘IT와 번역가 David가 함께 데이비드 크로우쇼(David Crawshaw)의 글 <How I program with LLMs>을 번역한 글입니다. 필자는 소프트웨어 엔지니어이자 기업가이며, SQLite의 기여자입니다. 현재 그는 안전하고 편리한 네트워크 솔루션을 제공하는 Tailscale의 공동 창업자로 활동하고 있으며, 자신의 블로그를 통해 프로그래밍과 기술에 대한 통찰력 있는 글들을 공유하고 있습니다. 이번 글에서는 지난 1년간 프로그래밍 작업에서 LLM을 사용하여 코드 자동 완성, 검색, 채팅 기반 프로그래밍 등 세 가지 주요 방식으로 생산성을 향상시켰다고 언급합니다. 필자에게 허락을 받고 번역했으며, 글에 포함된 각주(*표시)는 ‘번역자주’입니다. 글에 포함된 링크는 원문에 따라 표시했습니다. <출처: Unsplash, Jonathan Kemper> 저는 새로운 기술에 대해 항상 호기심이 많은 편입니다. LLM을 조금 실험해 보고 난 뒤, 이것이 실용적 가치가 있을지 궁금해졌습니다. 복잡한 질문에 대해 때때로 정교한 답변을 제시할 수 있는 기술이라는 점이 매력적이었습니다. 더욱 흥미로웠던 것은 컴퓨터가 요청한 대로 프로그램의 일부를 작성하려 시도하고, 실제로 상당한 진전을 이뤄내는 것이었습니다. 제가 경험한 기술적 변화 중 이와 비슷한 감흥을 준 것은 1995년, 우리가 LAN에 사용 가능한 기본 라우터를 설정했을 때뿐입니다. 트럼펫 윈속(Trumpet Winsock)*을 실행하던 다른 방의 공용 컴퓨터를 다이얼업 연결을 라우팅할 수 있는 기계로 교체했고, 순식간에 제 손끝에서 인터넷을 사용할 수 있게 되었습니다. 당시 대학에서 오랫동안 인터넷을 사용해 온 사람들보다 제게 더 큰 충격이었을 것입니다. 웹 브라우저, JPEG, 그리고 수백만 명의 사람들이라는 최첨단 인터넷 기술을 곧바로 접하게 되었기 때문입니다. 강력한 LLM을 사용하는 것도 그때와 비슷한 느낌입니다.*Trumpet Winsock: Windows용 프로그램으로 인터넷 연결을 위해 널리 사용되었던 소프트웨어 그래서 저는 이 호기심을 쫓아, 대부분의 경우 거의 틀리지 않는 결과물을 생성할 수 있는 이 도구가 제 일상 업무에 실질적인 도움이 될 수 있는지 확인해 보기로 했습니다. 결론적으로 말씀드리자면, 생성형 모델은 제가 프로그래밍할 때 유용했습니다. 하지만 이 지점까지 오는 것이 쉽지는 않았습니다. 새로운 기술에 대한 근본적인 매력이 있었기에 이를 이해할 수 있었고, 그래서 다른 엔지니어들이 LLM이 “쓸모없다”리고 주장할 때도 그들의 입장을 이해할 수 있었습니다. 하지만 어떻게 LLM을 효과적으로 사용할 수 있는지에 대해 여러 번 질문을 받았기에, 이 글을 통해 제가 지금까지 발견한 것들을 설명하고자 합니다. 개요제가 일상적인 프로그래밍에서 LLM을 사용하는 방식은 크게 세 가지입니다. 자동 완성: 명백한 타이핑 작업의 대부분을 대신해, 생산성을 향상시켜줍니다. 현재의 기술 수준에서도 개선의 여지가 있지만, 그것은 다른 이야기로 미뤄두겠습니다. 기성 제품이라도 아무것도 없는 것보다는 낫다는 것을 직접 확인했습니다. FIM 모델* 없이 일주일을 버텨보려 했지만, 지루한 타이핑 작업에 좌절감을 느꼈습니다. 이것이 바로 첫 번째로 실험해 볼만한 부분입니다.검색: “CSS에서 버튼을 투명하게 만드는 방법”과 같은 복잡한 환경에 관한 질문이 있을 때, 기존의 웹 검색 엔진을 사용하고 검색 결과 페이지에서 세부 사항을 파악하는 것보다, o1, sonnet 3.5 등 소비자용 LLM에 물어보는 것이 훨씬 더 나은 답변을 얻을 수 있습니다. (때로는 LLM이 틀릴 수도 있습니다. 사람도 마찬가지고요. LLM이 가끔 틀리는 것은 받아들일 수 있습니다.)채팅 기반 프로그래밍: 이것이 세 가지 중 가장 어려운 부분입니다. LLM에서 가장 큰 가치를 얻는 부분이지만, 동시에 가장 걱정되는 부분이기도 합니다. 많은 학습이 필요하고 프로그래밍 방식을 조정해야 하는데, 원칙적으로 저는 이런 것을 좋아하지 않습니다. 계산자를 사용하는 법을 배우는 것만큼이나 많은 시행착오가 필요하며, 더욱 귀찮은 점은 이것이 비결정적인 서비스라 행동과 사용자 인터페이스가 정기적으로 변한다는 것입니다. 실제로 제 작업의 장기적인 목표는 채팅 기반 프로그래밍의 필요성을 대체하여, 이러한 모델의 힘을 개발자에게 거부감 없이 전달하는 것입니다. 하지만 현재로서는 점진적으로 문제에 접근하기로 했습니다. 즉, 현재 가진 것으로 최선을 다하고 이를 개선하는 방법을 찾는 것입니다.*FIM(Fill in the Middle) 모델: 텍스트의 중간 부분을 채우는 것에 특화된 언어 모델 중 하나의 유형 이는 프로그래밍의 ‘실제’ 적용에 관한 것이므로, 정량적 엄밀성을 가지고 쓰기 어려운 근본적으로 정성적인 과정입니다. 제가 제시할 수 있는 가장 근접한 데이터는 이것입니다. 제 기록을 보면 현재 2시간의 프로그래밍 동안 10회 이상의 자동 완성 제안을 수락하고, 한 번의 검색과 같은 LLM 작업을 수행하며, 한 번의 채팅 세션에서 프로그래밍을 합니다. 이제 채팅 기반 프로그래밍에서 가치를 추출하는 방법을 설명하겠습니다. LLM 채팅은 왜 사용하는가?일부 회의적인 분들을 위해 설명하겠습니다. 제가 채팅 기반 프로그래밍에서 얻는 가치의 상당 부분은 하루 중 무엇을 작성해야 하는지 알고 설명할 수 있지만, 새 파일을 만들고 타이핑을 시작한 다음 필요한 라이브러리를 찾아볼 에너지가 없을 때, LLM은 프로그래밍에서 그런 서비스를 제공합니다. (저는 아침형 인간이라 보통 오전 11시 이후가 그렇고, 다른 언어/프레임워크 등으로 문맥 전환할 때도 마찬가지입니다.) 좋은 아이디어와 필요한 종속성 대부분이 포함된 초안을 제공하며, 종종 실수도 있습니다. 대개 그러한 실수를 수정하는 것이 처음부터 시작하는 것보다 훨씬 쉽습니다. 이는 채팅 기반 프로그래밍이 모든 사람에게 적합하지 않을 수 있다는 의미입니다. 저는 특정한 종류의 프로그래밍, 즉 견고한 인터페이스를 통해 사용자에게 프로그램을 전달하려는 제품 개발을 하고 있습니다. 이는 많은 것을 구축하고, 많은 것을 버리며, 여러 환경을 오가는 것을 의미합니다. 어떤 날은 대부분 타입스크립트를, 어떤 날은 대부분 Go를 작성합니다. 지난달에는 한 아이디어를 탐구하기 위해 일주일 동안 C++ 코드베이스에 있었고, HTTP 서버 사이드 이벤트 형식을 배울 기회도 있었습니다. 저는 이곳저곳을 다니며 계속해서 잊어버리고 다시 배우고 있습니다. 만약 코드를 작성하는 것보다 암호화 알고리즘 최적화가 타이밍 공격에 취약하지 않다는 것을 증명하는 데 더 많은 시간을 보낸다면, 제 관찰 내용은 아마 여러분에게 유용하지 않을 겁니다. 채팅 기반 LLM은 시험 유형의 질문에서 가장 뛰어난 성능을 보입니다LLM에게 특정 목표와 필요한 모든 배경 자료를 제공하여 잘 구성된 코드 리뷰 패킷을 만들도록 하고 질문에 따라 조정되기를 기대하십시오. 여기에는 두 가지 주요 요소가 있습니다. LLM이 혼란스러워하고 나쁜 결과를 생성할 만큼 복잡하고 모호한 상황을 만들지 않도록 하십시오. 이것이 제가 IDE 내에서 채팅을 사용하는 데 거의 성공하지 못한 이유입니다. 제 작업 공간은 종종 지저분하고, 작업 중인 저장소는 기본적으로 너무 크며, 주의를 산만하게 하는 것들로 가득 차 있습니다. 2025년 1월 현재 인간이 LLM보다 훨씬 더 잘하는 것 중 하나는 주의가 산만해지지 않는 것입니다. 그래서 저는 여전히 웹 브라우저를 통해 LLM을 사용합니다. 잘 구성된 요청을 만들 수 있는 백지상태가 필요하기 때문입니다.검증하기 쉬운 작업을 요청하십시오. LLM을 사용하는 프로그래머로서 여러분의 일은 그것이 생성하는 코드를 읽고, 생각하고, 작업이 좋은지 결정하는 것입니다. 인간에게는 절대 요청하지 않을 일을 LLM에 요청할 수 있습니다. “테스트를 더 읽기 쉽게 만들기 위해 설계된 <중간 개념>을 도입하여 모든 새로운 테스트를 다시 작성하라”는 것은 인간에게 요청하기에는 끔찍한 일입니다. 작업 비용이 이점에 비해 가치가 있는지에 대한 며칠간의 긴장된 논의가 있을 것입니다. LLM은 60초 만에 이를 수행하고 실행하기 위해 싸울 필요가 없습니다. 작업 재실행이 매우 저렴하다는 사실을 활용하세요. LLM에 이상적인 작업은 많은 공통 라이브러리를 사용해야 하고(인간이 기억할 수 있는 것보다 더 많아서 소규모 연구를 많이 수행합니다), 여러분이 설계한 인터페이스에 맞춰 작업하거나 빠르게 검증할 수 있는 작은 인터페이스를 생성하며, 읽기 쉬운 테스트를 작성할 수 있는 것입니다. 때로는 특이한 것을 원할 경우 라이브러리를 선택해야 할 수도 있습니다(오픈 소스 코드의 경우 LLM이 이를 매우 잘합니다). LLM의 코드를 읽는 데 시간을 쓰기 전에 항상 컴파일러를 통과시키고 테스트를 실행해야 합니다. 모든 LLM이 때때로 컴파일되지 않는 코드를 생성합니다. 더 나은 LLM은 자신의 실수를 복구하는 데 매우 능숙하며, 대개 컴파일러 오류나 테스트 실패를 채팅에 붙여넣기만 하면 코드를 수정합니다. 추가 코드 구조는 훨씬 저렴합니다우리는 매일 코드 작성 비용, 읽기 비용, 리팩토링 비용에 대해 모호한 절충을 합니다. Go 패키지 경계를 예로 들어보겠습니다. 표준 라이브러리에는 와이어 포맷 인코딩, MIME 타입 등을 다루는 기본 타입들이 포함된 “net/http” 패키지가 있습니다. 여기에는 HTTP 클라이언트와 HTTP 서버가 포함되어 있죠. 이것이 하나의 패키지여야 할까요, 아니면 여러 개여야 할까요? 합리적인 사람들도 의견이 다를 수 있습니다. 오늘날에도 정답이 있는지 모를 정도입니다. 현재 작동하는 방식이 있지만, 15년간 사용해 왔음에도 다른 패키지 구성이 더 나을지는 여전히 명확하지 않습니다. 큰 패키지의 장점은 다음과 같습니다. 호출자를 위한 중앙화된 문서, 초기 작성의 용이성, 쉬운 리팩토링, 견고한 인터페이스를 고안하지 않고도 헬퍼 코드를 쉽게 공유할 수 있다는 점입니다(이는 종종 패키지의 기본 타입을 또 다른 타입으로 가득 찬 리프 패키지로 빼내는 것을 포함합니다). 단점으로는 여러 가지 일이 동시에 일어나기 때문에 패키지를 읽기 어렵다는 점(net/http 클라이언트 구현을 읽다가 실수로 서버 코드에서 몇 분을 보내게 되는 경우를 생각해 보세요), 너무 많은 일이 일어나 사용하기 어렵다는 점이 있습니다. 예를 들어, 제가 가진 코드베이스 중 일부 기본 타입에서 C 라이브러리를 사용하지만, 코드베이스의 일부는 C 라이브러리가 기술적으로 필요하지 않은 많은 플랫폼에 널리 배포되어야 하는 바이너리에 있어야 하므로, 멀티플랫폼 바이너리에서 cgo 사용을 피하기 위해 예상보다 더 많은 패키지가 있습니다. 여기에는 정답이 없습니다. 대신 엔지니어가 수행해야 할 다양한 유형의 작업(초기 및 지속적)을 절충하고 있는 것입니다. LLM은 이러한 절충에 영향을 미칩니다. LLM이 시험 스타일 질문에 더 잘 대응하므로, 더 많고 작은 패키지를 사용하면 작업의 완전하면서도 격리된 컨텍스트를 제공하기가 더 쉽습니다. 이는 인간에게도 마찬가지이며, 이것이 우리가 패키지를 사용하는 이유이지만, 더 읽기 쉬운 코드를 만들기 위한 추가 타이핑/배관/파일링과 패키지 크기를 절충합니다. LLM이 그 추가 작업의 큰 부분을 수행하고 혜택을 받으면서, 절충점이 달라집니다. (보너스로 우리는 더 읽기 쉬운 코드를 얻게 됩니다.)더 작고 많은 패키지는 관련 없는 코드와 독립적으로 컴파일되고 테스트 될 수 있습니다. 이는 다른 종속 패키지가 리팩토링 되기 전에 변경 사항을 컴파일하고 테스트할 수 있고, 패키지에 더 단순한 테스트 환경이 있기 때문에 LLM 개발 주기에 도움이 됩니다. 예시 살펴보기논의된 아이디어를 몇 가지 결합한 예시를 살펴보겠습니다 float*의 사분위수를 위한 저장소 샘플러를 작성하세요.*float: 소수점이 있는 숫자를 다루는 데이터 타입 우선 패키지 구조입니다. LLM 이전에 이를 수행했다면, 여러 알고리즘이 포함된 일종의 streamstat 패키지를 선택했을 것이고, 아마도 파일당 하나의 알고리즘을 넣었을 것입니다. 이는 독특한 의견은 아닌 것 같습니다. 여기 그 모델을 따르는 오픈소스 quantile 패키지가 있습니다. 이제는 이 하나의 알고리즘만 자체 패키지에 넣고 싶습니다. 다른 변형이나 관련 알고리즘은 자체 패키지를 가질 수 있습니다. 다음으로 LLM에서 무엇을 얻는지 봅시다. 첫 번째 시도는 나쁘지 않았습니다. Go 언어로 작성하고 싶다는 세부 사항이 포함된 프롬프트로 아래의 샘플러를 얻었습니다. // QuartileSampler는 float64 값 스트림의 사분위수를 추정하기 위한 저장소 샘플을 관리합니다. // 대용량 데이터 스트림을 처리할 때 대표성을 갖는 샘플을 유지하기 위해 저장소 샘플링을 사용합니다. type QuartileSampler struct { reservoir []float64 seen int maxSize int rng *rand.Rand } // NewQuartileSampler는 최대 maxSize 개의 샘플을 유지하는 새로운 샘플러를 생성합니다. // maxSize가 클수록 더 정확한 추정이 가능하지만 그만큼 메모리를 더 사용하게 됩니다. func NewQuartileSampler(maxSize int) *QuartileSampler { ... } 인터페이스도 잘 만들었습니다. // Add는 새로운 값을 샘플러에 추가합니다. func (qs *QuartileSampler) Add(value float64) { ... } // Quartiles는 지금까지 입력된 데이터의 25번째, 50번째(중앙값), 75번째 백분위수를 // 추정하여 반환합니다. 입력된 데이터가 없으면 (0,0,0)을 반환합니다. func (qs *QuartileSampler) Quartiles() (q1, median, q3 float64) { if len(qs.reservoir) == 0 { return 0, 0, 0 } // reservoir를 수정하지 않도록 복사본 생성 sorted := make([]float64, len(qs.reservoir)) copy(sorted, qs.reservoir) slices.Sort(sorted) n := len(sorted) q1 = percentile(sorted, 0.25) median = percentile(sorted, 0.50) q3 = percentile(sorted, 0.75) return q1, median, q3 } 훌륭합니다. 테스트도 있습니다. 때로는 LLM 코드 생성을 특수한 검색 형태로 사용합니다. 예를 들어, 저장소 샘플링에 대해 궁금하지만 시간 윈도우 샘플링과 같은 특이한 제약 조건에서 알고리즘이 어떻게 적용될지 보고 싶을 때가 있습니다. 문헌 검색을 하는 대신 신선도를 추적하는 구현을 위한 프롬프트를 수정할 수 있습니다. (주석에 문헌 참조를 포함하도록 요청할 수도 있으며, 이를 수동으로 확인하여 LLM이 만들어내는 것인지 아니면 견고한 연구가 있는지 확인할 수 있습니다.) 종종 60초 동안 생성된 코드를 읽고, 생각하지 못한 명백한 트릭을 발견한 다음, 버리고 다시 시작합니다. 이제 그 트릭이 가능하다는 것을 알게 되었습니다. 이것이 LLM이 생성한 가치를 귀속시키기 어려운 이유입니다. 때로는 나쁜 코드를 만들고, 루틴에 갇히고, 불가능한 것을 만들어내고 (얼마 전에는 제가 있었으면 하는 모나코 API의 일부를 지어냈습니다.) 시간을 낭비합니다. 하지만 제가 모르는 관련된 것을 지적함으로써 몇 시간을 절약할 수도 있습니다. 코드로 돌아가 보면, 흥미롭게도 초기 코드는 컴파일되지 않았습니다. Quartiles 구현 중간에 다음과 같은 줄이 있었습니다. n := len(sorted) 이는 좋은 줄입니다. sorted는 몇 줄 위에 정의된 슬라이스입니다. 하지만 값이 전혀 사용되지 않아서 gopls*(그리고 go build를 실행하면 Go 컴파일러)가 즉시 말합니다.*gopls: Go 프로그래밍 언어 서버 declared and not used: n 이는 매우 쉽게 고칠 수 있습니다. 오류를 LLM에 다시 붙여 넣으면 수정할 것입니다. 하지만 이 경우, 코드를 읽으면서 그 줄을 직접 삭제할 수 있다는 것이 꽤 명확하므로 제가 삭제합니다. 이제 테스트입니다. 예상했던 대로 quartile_sampler_test.go를 얻었습니다. func TestQuartileSampler(t *testing.T) { tests := []struct { name string data []float64 wantQ1 float64 wantMed float64 wantQ3 float64 epsilon float64 // 부동소수점 비교를 위한 허용 오차 }{ { name: "기본 수열", data: []float64{1, 2, 3, 4, 5, 6, 7}, wantQ1: 2, wantMed: 4, wantQ3: 6, epsilon: 0.1, }, ... } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { qs := NewQuartileSampler(100) // 정확한 테스트를 위해 큰 저장소 사용 for _, v := range tt.data { qs.Add(v) } q1, med, q3 := qs.Quartiles() if math.Abs(q1-tt.wantQ1) > tt.epsilon { t.Errorf("Q1 = %v, want %v (±%v)", q1, tt.wantQ1, tt.epsilon) } if math.Abs(med-tt.wantMed) > tt.epsilon { t.Errorf("Median = %v, want %v (±%v)", med, tt.wantMed, tt.epsilon) } if math.Abs(q3-tt.wantQ3) > tt.epsilon { t.Errorf("Q3 = %v, want %v (±%v)", q3, tt.wantQ3, tt.epsilon) } }) } } 제가 직접 작성했을 법한 테스트 코드입니다. 보통은 다른 구현체를 통해 예상 출력값을 생성한 다음, 그 값들을 이런 식의 테스트에 복사해 넣곤 합니다. 하지만 여기에는 두 가지 문제가 있습니다. 첫 번째 문제는 LLM이 이 숫자들을 다른 구현을 통해 실행하지 않았다는 것입니다. (제가 아는 한에서는 그렇습니다. 정교한 LLM 서비스를 사용할 때는 뒤에서 무슨 일이 일어나는지 확실히 말하기 어렵습니다.) LLM이 이를 만들어냈고, LLM은 산술에 약하다는 평판이 있습니다. 따라서 인간이 다른 도구의 출력을 기반으로 하거나 특별히 구식이라면 직접 산술을 수행하는 이런 종류의 테스트는 LLM에서는 좋지 않습니다. 두 번째 문제는 우리가 더 잘할 수 있다는 것입니다. 프로그래머가 자신의 테스트를 작성하는 시대에 살게 된 것은 기쁘지만, 우리는 테스트에 대해 프로덕션 코드와 같은 기준을 적용하지 않습니다. 이는 합리적인 절충입니다. 하루에는 제한된 시간만 있으니까요. 하지만 LLM은 산술적 능력이 부족한 만큼 열의로 보완합니다. 더 나은 테스트를 작성해 보겠습니다. 테스트에서는 슬라이스에 있는 값들의 사분위 수를 계산하는 가장 단순하고 읽기 쉬운 표준 코드를 구현하고, 이를 통해 테스트 케이스를 표준 코드와 저장소 샘플러에 모두 통과시켜 서로 허용 오차 내에 있는지 확인해 보겠습니다. 또한 퍼즈 테스트*에서도 사용할 수 있도록 비교 코드를 구성해 보겠습니다.*퍼즈 테스트: 프로그램에 이상한 데이터나 예상치 못한 입력값을 마구 넣어보면서 프로그램이 제대로 작동하는지 검사하는 방법 // referenceQuartiles는 float64 타입 슬라이스의 정확한 사분위수 값을 계산하는 함수입니다. // 이는 샘플러에서 기대되는 동작과 일치합니다. func referenceQuartiles(data []float64) (q1, median, q3 float64) { ... } // compareQuartiles는 두 사분위수 세트가 허용 오차 내에 있는지 확인합니다. // 허용 오차 내에 있으면 true, 그렇지 않으면 false를 반환합니다. func compareQuartiles(q1a, meda, q3a, q1b, medb, q3b, epsilon float64) bool { ... } // checkQuartiles는 샘플러 출력을 참조 구현과 비교하고 차이를 보고하는 테스트 도우미입니다. func checkQuartiles(t *testing.T, data []float64, epsilon float64) { t.Helper() // 참조 값 얻기 wantQ1, wantMed, wantQ3 := referenceQuartiles(data) // 정확도를 위해 큰 저장소를 사용하여 샘플러 값 얻기 qs := NewQuartileSampler(1000) for _, v := range data { qs.Add(v) } gotQ1, gotMed, gotQ3 := qs.Quartiles() if !compareQuartiles(gotQ1, gotMed, gotQ3, wantQ1, wantMed, wantQ3, epsilon) { t.Errorf("사분위수 불일치:\n실제값 (q1=%v, med=%v, q3=%v)\n기대값 (q1=%v, med=%v, q3=%v)\n허용오차=%v", gotQ1, gotMed, gotQ3, wantQ1, wantMed, wantQ3, epsilon) } } LLM과의 흥미로운 상호작용이네요. 원래 테스트를 checkQuartiles를 사용하도록 수정했고, 새로운 퍼즈 테스트도 추가했습니다 func FuzzQuartileSampler(f *testing.F) { // 시드 데이터 추가 f.Add([]float64{1, 2, 3, 4, 5}) f.Fuzz(func(t *testing.T, data []float64) { // 극단적인 값이 나올 수 있으므로 퍼징에는 더 큰 허용 오차 사용 checkQuartiles(t, data, 0.2) }) } 재미있게도 이 코드는 잘못되었습니다. gopls 도구가 바로 다음과 같은 오류를 출력했습니다. fuzzing arguments can only have the following types: string, bool, float32, float64, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, []byte 이 오류 메시지를 LLM에 다시 입력하니, func(t *testing.T, data []byte) 함수를 사용하고 math.Float64frombits를 통해 바이트 슬라이스에서 부동소수점을 추출하는 방식으로 퍼즈 테스트를 다시 생성했습니다. 이런 상호작용을 보면 도구의 피드백을 자동화할 필요성이 보입니다. LLM은 단순히 오류 메시지만 있으면 유용한 진전을 이룰 수 있었고, 제 도움은 필요하지 않았습니다. 최근 몇 주간의 LLM 채팅 기록을 살펴보니(앞서 언급했듯이 이는 제대로 된 정량적 분석은 아니지만), 도구 관련 오류가 발생했을 때 80% 이상은 제 통찰 없이도 LLM이 유용한 진전을 이룰 수 있었습니다. 절반 정도는 제가 특별한 말을 하지 않아도 완전히 문제를 해결할 수 있었고, 저는 그저 메시지를 전달하는 역할만 했던 셈이죠. 우리는 어디로 가고 있는가? 더 나은 테스트, 어쩌면 DRY 원칙의 완화약 25년 전, “중복을 피하라”는 원칙을 중심으로 한 프로그래밍 운동이 있었습니다. 학부생들에게 가르치는 간단명료한 원칙들이 그렇듯이, 이 또한 지나치게 과도하게 적용되었습니다. 코드의 재사용을 위한 추상화에는 상당한 비용이 따르며, 새롭게 학습해야 하는 중간 단계의 추상화가 필요하고, 최대한 많은 사용자에게 유용하도록 기능을 추가해야 하기에 불필요하고 산만한 기능들로 가득한 라이브러리에 의존하게 됩니다. 지난 10~15년간 코드 작성에 대한 접근 방식은 훨씬 더 절제되어, 많은 프로그래머들이 코드 공유의 비용이 개별 구현과 유지보수 비용보다 높을 경우, 개념을 재구현하는 것이 더 낫다는 것을 이해하게 되었습니다. 코드 리뷰에서 “이것은 가치가 없으니, 구현을 분리하세요.”라는 말을 하는 경우가 훨씬 줄어들었습니다. 프로그래머들이 트레이드오프를 더 잘 이해하게 된 것입니다. 현재 우리는 트레이드오프가 변화한 세상에 살고 있습니다. 이제 더 포괄적인 테스트를 작성하는 것이 더 쉬워졌습니다. LLM을 통해 원하는 퍼즈 테스트 구현을 몇 시간이나 투자하지 않고도, 적절히 구축할 수 있게 되었습니다. LLM이 “이슈 트래커에서 다른 버그를 픽업하는 것이 회사에 더 도움이 될 것”이라고 끊임없이 생각하지 않기 때문에, 테스트를 더 읽기 쉽게 작성하는 데 더 많은 시간을 투자할 수 있습니다. 따라서 트레이드오프는 더 특화된 구현을 선호하는 방향으로 이동하고 있습니다. 이러한 변화가 가장 두드러지게 나타날 곳은 언어별 REST API* wrapper*입니다. 주요 기업의 API마다 수십 개의 wrapper가 존재하는데, 대부분 품질이 낮고 특정 목표를 위해 실제로 구현을 사용하지 않는 사람들이 작성한 것으로, 대신 거대하고 복잡한 인터페이스에서 API의 모든 구석구석을 포착하려 시도합니다. 잘 done된 경우에도, REST 문서(보통 curl 명령어 세트)를 참조하여 실제로 필요한 API의 1%만을 위한 언어 wrapper를 구현하는 것이 더 쉽다는 것을 발견했습니다. 이는 처음에 학습해야 할 API의 양을 줄이고, 미래의 프로그래머들이 코드를 읽을 때 이해해야 할 내용도 줄여줍니다.*REST API: 웹에서 데이터를 주고받기 위한 규칙이나 약속*wrapper: 복잡한 기능을 사용하기 쉽게 감싸서 단순화해 주는 코드 예를 들어, 최근 sketch.dev 작업의 일환으로 Go 언어에서 Gemini API wrapper를 구현했습니다. 공식 Go wrapper가 언어를 잘 알고 명확히 관심을 가진 사람들에 의해 세심하게 제작되었음에도 불구하고, 이를 이해하기 위해서는 많은 내용을 읽어야 합니다. $ go doc -all genai | wc -l 1155 제가 처음 만든 단순한 wrapper는 총 200줄의 코드로, 하나의 메서드와 세 개의 타입으로 구성되어 있었습니다. 전체 구현을 읽는 것은 공식 패키지의 문서를 읽는 작업의 20%에 불과하며, 구현을 파고들어 가보면 프로토와 gRPC 등이 포함된 또 다른 대규모 코드 생성 구현의 wrapper라는 것을 발견하게 될 것입니다. 제가 원하는 것은 그저 cURL로 JSON 객체를 파싱하는 것뿐입니다. 물론 Gemini가 앱의 기반이 되고, 거의 모든 기능이 사용되며, gRPC 기반 구축이 조직의 다른 곳의 원격 측정 시스템과 잘 맞는 프로젝트에서는 큰 공식 wrapper를 사용해야 할 시점이 옵니다. 하지만 대부분의 경우 우리는 거의 항상 오늘 사용해야 하는 API의 아주 얇은 조각만을 원하기 때문에, GPU에 의해 대부분 작성된 커스텀 클라이언트가 작업을 수행하는 데 훨씬 더 효과적입니다. 따라서 저는 더 전문화된 코드가 많아지고, 일반화된 패키지는 줄어들며, 더 읽기 쉬운 테스트가 있는 세상을 예견합니다. 재사용 가능한 코드는 작고 견고한 인터페이스를 중심으로 계속 번성할 것이며, 그렇지 않은 경우에는 전문화된 코드로 분리될 것입니다. 이것이 얼마나 잘 이루어지느냐에 따라 더 나은 소프트웨어 또는 더 나쁜 소프트웨어로 이어질 것입니다. 저는 둘 다 예상하지만, 중요한 지표들을 기준으로 볼 때 장기적으로는 더 나은 소프트웨어로 향하는 추세가 될 것으로 생각합니다. 이러한 관찰의 자동화: sketch.dev프로그래머로서 반복적인 일은 컴퓨터가 해주면 좋겠다고 생각하죠. LLM을 활용하는 게 꽤 힘든 일인데, 이걸 어떻게 자동화할 수 있을까요? 문제를 해결하는 핵심은 과도하게 일반화하지 않는 것이라고 생각합니다. 특정 문제를 해결한 다음 천천히 확장해 나가는 것이죠. COBOL*부터 Haskell*까지 다 잘하는 범용 채팅 프로그래밍 UI를 만들기보다는, 하나의 특정 환경에 집중하는 게 좋습니다. 제 프로그래밍의 대부분은 Go로 이루어지므로, Go 프로그래머에게 필요한 것을 쉽게 상상할 수 있습니다.*COBOL: 기업의 업무용 프로그램(회계, 급여 관리 등)을 만드는 데 사용되는 프로그래밍 언어*Haskell: 함수형 프로그래밍 언어 Go 플레이그라운드와 비슷하지만, 패키지와 테스트 편집을 중심으로 구축된 것편집 가능한 코드에 대한 채팅 인터페이스go get과 go test를 실행할 수 있는 작은 UNIX 환경goimports 통합gopls 통합자동 모델 피드백: 모델 편집 시 go get, go build, go test를 실행하고, 누락된 패키지, 컴파일러 오류, 테스트 실패를 모델에 피드백하여 자동으로 수정하도록 시도 그렇게 몇 명이 초기 프로토타입을 만든 것이 sketch.dev입니다. 목표는 ‘웹 IDE’가 아니라, 채팅 기반 프로그래밍이 전통적으로 IDE라고 불리는 것에 속해야 한다는 개념에 도전하는 것입니다. IDE는 사람들을 위해 배치된 도구들의 모음입니다. 제가 무슨 일이 일어나고 있는지 아는 섬세한 환경이죠. 저는 현재 작업 중인 브랜치에 LLM이 초안을 마구 뱉어내는 건 원하지 않습니다. LLM이 궁극적으로는 개발자 도구이지만, 효과적으로 작동하는 데 필요한 피드백을 얻기 위해서는 자체적인 IDE가 필요합니다. 다르게 말하면, 우리는 goimports를 스케치에 사람이 사용하도록 임베드한 것이 아니라 자동신호를 사용하여, Go 코드가 컴파일되는 데 더 가깝게 만들어, 컴파일러가 이를 구동하는 LLM에 더 나은 오류 피드백을 제공할 수 있도록 한 것입니다. sketch.dev를 “LLM을 위한 Go IDE”로 생각하는 것이 더 나을 수 있습니다. 이는 모두 최근의 작업이며 아직 할 일이 많습니다. 예를 들어, 기존 패키지를 로드하여 편집하고 결과를 브랜치에 드롭할 수 있는 git 통합이 필요하고, 더 나은 테스트 피드백과 더 많은 콘솔 제어도 필요합니다. 우리는 여전히 탐색 중이지만, 특정 종류의 프로그래밍을 위한 환경에 집중하는 것이 일반화된 도구보다 더 나은 결과를 가져다줄 것이라고 확신하고 있습니다.<원문>How I program with LLMs ©위 번역글의 원 저작권은 David Crawshaw에게 있으며, 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다