대부분의 개발자는 LLM을 거꾸로 사용한다. 다섯 단어로 원하는 것을 설명하고, 200줄의 코드를 받은 뒤, 다음 한 시간은 모델이 내린 가정을 디버깅하는 데 쓴다.
나는 반대 workflow를 3개월간 실행했다. 한 줄의 코드도 요청하기 전에 완전한 설계 설명을 작성하는 것이다. 결과적으로 코드의 버그는 줄었지만, 진정한 개선은 나 자신의 이해에 있었다.
LLM과의 Literate Programming이란?
Donald Knuth는 1984년에 “literate programming”이라는 용어를 만들었다. 아이디어는 단순했다. 프로그램을 설명하는 산문을 쓰고, 그 산문에 코드를 짜 넣는 것이다. 대부분의 개발자는 두 가지 산출물을 유지하는 것이 번거로워 외면했다. LLM은 그 계산을 바꾼다. 산문은 더 이상 문서가 아니다. 그것은 prompt다.
코드를 요청하기 전에 LLM에게 자신의 접근 방식을 설명할 때, 그것은 표준 prompt engineering과 다른 무언가를 하는 것이다. 모델을 위한 token을 최적화하는 것이 아니다. 모델이 대신 아키텍처를 환각하기 전에, 스스로 constraint, edge case, 의존성을 명확히 하도록 강제하는 것이다. LLM은 대답하는 rubber duck이 된다. 단, 자신이 실제로 사고를 마친 뒤에만 그렇다.
실험: 30일간 먼저 설명하기
백로그에서 12개의 실제 업무를 골랐다. CSV 파서부터 WebSocket 재연결 전략까지 다양했다. 각각에 대해 두 가지 접근법을 사용했다.
- 지름길: 티켓 설명을 LLM에 붙여넣고 코드를 요청한다.
- 설명: 문제, constraint, 오류 처리 전략, test case를 설명하는 300~500단어를 작성한다. 그리고 LLM에게 그 설계를 구현하도록 요청한다.
출력을 세 가지 기준으로 평가했다. code review에서 발견된 버그 수, merge까지 걸린 시간, 문제 수정에 필요한 follow-up prompt의 수였다.
수치는 압도적이었다. 설명 우선 코드는 평균 1.2개의 follow-up prompt가 필요했고, 지름길 접근법은 4.7개였다. code review에서 설명 우선 제출물당 0.3개의 버그가 발견되었고, 지름길 코드는 2.1개였다.
문제는 시간이었다. 설명을 작성하는 것이 모든 업무의 시작에 15~20분을 추가했다. 그것이 payoff가 될지는 무엇을 만들고 있느냐에 달려 있다.
실제로 작동하는 설명 구조화법
좋은 설명은 단순히 더 긴 prompt가 아니다. 특정 구조를 가진 design document다. 다음은 첫 주 이후에 다듬은 템플릿이다.
## Problem
[What are we solving and why?]
## Constraints
[What can't we do? What formats, protocols, or dependencies must we respect?]
## Edge Cases
[What happens when input is null, empty, malformed, or hostile?]
## Error Handling Strategy
[Fail fast or fail graceful? What do we log? What do we return?]
## Test Cases
[The specific inputs and expected outputs that prove this works.]
실제 예시를 들어보자. HMAC-SHA256을 사용하여 webhook 서명을 검증하는 함수가 필요했는데, 변칙이 있었다. 서명 헤더는 요청을 보낸 서비스에 따라 두 가지 형식을 사용할 수 있었다.
Problem: 들어오는 webhook 서명을 검증한다. 일부 발신자는 X-Signature: sha256=<hex>를 사용하고, 다른 일부는 접두사 없는 X-Signature: <hex>를 사용한다. 기존 통합을 깨뜨리지 않고 둘 다 지원해야 한다.
Constraints: Node.js crypto 모듈을 사용해야 한다. timing attack을 방지하기 위해 constant-time 비교를 사용해야 한다. 명확한 boolean을 반환해야 하며, malformed input에서 절대 throw하지 않아야 한다.
Edge Cases: 헤더가 없다. 헤더가 빈 문자열이다. 헤더에 sha512= 같은 알 수 없는 접두사가 있다. hex 문자열의 길이가 홀수이거나 비hex 문자를 포함한다.
Error Handling Strategy: 디버그 레벨에서 구체적인 실패 이유를 기록하지만, 모든 무효한 경우에 false를 반환한다. 오류 메시지에 서명 세부 정보를 절대 누출하지 않는다.
Test Cases:
- Valid
sha256=abc123...matches computed signature. - Valid bare
abc123...matches computed signature. - Missing header returns
false. - Wrong signature returns
false. - Non-hex characters in signature return
false.
그리고 나서 “Implement this function in TypeScript.”라고 덧붙였다.
모델은 다음을 반환했다.
import { createHmac, timingSafeEqual } from 'crypto';
export function validateWebhookSignature(
payload: string,
header: string | undefined,
secret: string
): boolean {
if (!header || header.length === 0) {
return false;
}
const hexPattern = /^[0-9a-fA-F]+$/;
let signatureHex: string;
const prefixMatch = header.match(/^sha256=(.+)$/);
if (prefixMatch) {
signatureHex = prefixMatch[1];
} else {
signatureHex = header;
}
if (!hexPattern.test(signatureHex) || signatureHex.length % 2 !== 0) {
return false;
}
const expected = createHmac('sha256', secret).update(payload).digest();
const actual = Buffer.from(signatureHex, 'hex');
if (actual.length !== expected.length) {
return false;
}
return timingSafeEqual(actual, expected);
}
이것은 첫 시도에 컴파일되었다. 더 중요한 것은, 이미 사고를 마쳤기 때문에 내가 지정한 모든 edge case를 처리했다는 점이다. 모델은 “validate”가 무엇을 의미하는지 추측할 필요가 없었다.
도움이 되는 곳과 시간을 낭비하는 곳
이 접근법은 분기 로직, 외부 의존성, 보안 영향이 있는 모든 것에서 빛을 발한다. 위의 webhook 검증기가 완벽한 예시다. 모델은 당신이 알려주지 않으면 threat model을 알 수 없다.
하지만 보일러플레이트에는 과하다. React 폼 컴포넌트나 표준 데이터베이스 마이그레이션을 생성하기 위해 500단어를 쓰지는 않는다. 이들에 대해서는 패턴이 training data에 충분히 잘 표현되어 있고 stakes도 낮기 때문에 지름길 접근법이 잘 작동한다.
진정한 함정은 중간 지대에 있다. 단순해 보이지만 실제로는 그렇지 않은 업무다. CSV 파서는 쉼표를 포함한 인용 필드나 BOM 마커, 혼합된 line ending을 만나기 전까지는 간단해 보인다. 설명을 건너뛰어 가장 많은 시간을 잃게 되는 것이 바로 이런 업무다.
오늘부터 따라 할 수 있는 워크플로
전체 프로세스를 바꿀 필요는 없다. 이번 주 한 업무에 대해 다음을 시도해 보자.
- LLM 채팅을 열고 먼저 설명을 작성한다. 아직 코드는 언급하지 않는다.
- 자신의 설명을 다시 읽는다. edge case를 명확히 표현할 수 없다면 멈춘다. 아직 코드를 쓸 준비가 되지 않았으며, LLM에게 쓰게 할 준비도 확실히 되지 않았다.
- 같은 메시지에 구현 요청을 덧붙인다.
- 출력을 자신의 직관이 아닌, 자신의 test case에 대해 검토한다.
대부분이 건너뛰는 단계는 두 번째이다. 설명이 모호하면 LLM은 그 모호함을 미묘한 버그의 형태로 되돌려 줄 것이다. 설명은 자신의 이해에 대한 smoke test이다.
진정한 이점은 코드가 아니다
30일 후, 예상 밖의 사실을 깨달았다. 코드는 더 나아졌지만, 나의 설계 능력이 더 빠르게 향상되었다. 설명을 작성하는 것은 자신의 가정을 명시적으로 이름 짓도록 강요한다. 그렇게 하면 종종 한 줄의 코드도 쓰기 전에 자신의 접근법이 틀렸음을 깨닫게 된다.
LLM은 여전히 타이핑을 하고 있다. 하지만 사고하는 것은 당신이며, 그것이 언제나 bottleneck이었다.
직접 시도해 보고 싶다면 다음 버그 수정부터 시작하라. LLM에 도움을 요청하기 전에, 무슨 일이 일어나고 있다고 생각하는지, 왜 그렇게 생각하는지 설명하는 두 문단을 작성해 보자. 다 쓰기 전에 해결했을지도 모른다.