
결정론적 생성을 토대로 비결정론적 생성으로 비즈니스 로직을 작성하기
요약
결정론적 코드 생성 도구와 비결정론적 LLM 생성을 결합하여 안정적인 AI 에이전트 개발 환경을 구축하는 설계 방식을 제안합니다. 스키마 기반의 확정적 구조 위에 LLM의 유연한 비즈니스 로직 생성을 얹는 것이 핵심입니다.
핵심 포인트
- 결정론적 생성은 OpenAPI, Prisma처럼 입력에 따라 동일한 출력을 보장함
- 비결정론적 생성은 LLM을 통해 정의되지 않은 의도를 코드로 구현함
- AI 에이전트의 신뢰성을 위해 결정론적 구조를 토대로 비즈니스 로직을 설계해야 함
서론
"소스 코드를 생성한다"라는 말에는 현재 두 가지 의미가 섞여 있습니다.
하나는 OpenAPI 정의나 DB 스키마로부터 타입 정의나 클라이언트를 출력하는,
OpenAPI Generator
나
Prisma
와 같은 기존 방식의 코드 생성 도구에 의한 생성입니다.
또 하나는 LLM에 자연어로 요구사항을 던져서,
그 응답으로서 소스 코드를 받는 생성입니다.
이 두 가지는 "코드 생성"이라는 같은 말로 묶이기 쉽지만, 성질은 정반대입니다.
| 코드 생성 도구 | LLM |
|---|---|
| 생성의 성질 | 결정론적 (Deterministic) |
이 기사에서는 결정론적인 생성물을 토대로 깔고,
그 위에 비결정론적인 생성으로 비즈니스 로직을 얹는 설계가,
AI Agent가 착실하게 개발을 수행하게 하기 위해 바람직한 것이 아닌가 하는 이야기를 쓰고자 합니다.
2종류의 코드 생성
결정론적 생성: 동일한 입력에서 동일한 출력이 나온다
결정론적 생성(Deterministic Generation)이란, 기계 판독 가능한 정의를 입력으로 하여,
규칙에 따라 출력을 조립하는 방식입니다.
동일한 입력을 주면, 몇 번을 실행해도 동일한 출력이 반환됩니다.
대표적인 것으로는 다음과 같은 것들을 들 수 있습니다.
| 도구 이름 | 생성에 필요한 파일 | 생성 대상 | 출력의 재편집 |
|---|---|---|---|
OpenAPI Generator, oapi-codegen | openapi.yaml | API 클라이언트·서버 스텁 | 하지 않음 |
| Protocol Buffers | .proto | 메시지 타입·RPC 스텁 | 하지 않음 |
| Prisma, sqlc, SeaORM | schema.prisma 등 | 모델 계층·쿼리 계층 | 하지 않음 |
| GraphQL Code Generator | GraphQL 스키마 | 스키마 유래 타입 | 하지 않음 |
| ts-rs, specta | Rust의 타입 정의 | TypeScript의 타입 정의 | 하지 않음 |
derive 매크로, go:generate | 소스 중의 어노테이션 | 보일러플레이트 (Boilerplate) | 하지 않음 |
Plop, Hygen, rails generate | 템플릿 파일 | 견본 코드 | 함 |
마지막의 Plop나 Hygen 같은 견본 생성 도구만은 성질이 다릅니다.
출력된 견본은 그 후에 인간이나 LLM이 다시 작성한다는 전제로 만들어져 있습니다.
재생성해도 같은 결과가 반환된다는 점은 같지만,
재생성하면 손을 댄 내용이 사라지기 때문에,
이 기사에서 말하는 토대로서는 다룰 수 없습니다.
견본 생성 도구의 출력은 후술할 비즈니스 로직의 출발점으로 위치시킵니다.
derive 매크로나 go:generate에도 주의점이 있습니다.
이것들은 입력이 되는 어노테이션이 직접 작성한 소스 코드와 같은 파일에 놓이기 때문에,
정의와 비즈니스 로직이 파일 단위로는 분리되지 않습니다.
분리되는 것은 생성된 출력뿐입니다.
이들에게 공통되는 성질은, 출력이 입력 정의의 사상(Mapping)일 뿐이라는 점입니다.
출력을 보고 놀랄 일은 없습니다. 정의가 바뀌면 출력도 바뀌고,
정의가 바뀌지 않으면 출력도 바뀌지 않습니다.
비결정론적 생성: 동일한 입력에서 다른 출력이 나온다
반면 비결정론적 생성(Non-deterministic Generation)은 LLM에 자연어로 요구를 전달하여,
소스 코드라는 응답을 얻는 방식입니다.
동일한 프롬프트를 두 번 던져도, 동일한 소스 코드는 반환되지 않습니다.
모델의 effort를 바꾸더라도, 이 성질은 본질적으로 사라지지 않습니다.
이것은 결함이 아니라 특성입니다.
비결정론적 생성은 결정론적 생성으로는 원리적으로 불가능한 일을 해냅니다.
정의에 적혀 있지 않은 의도를 채울 수 있기 때문입니다.
"탈퇴한 유저는 30일 이내라면 복구할 수 있다"라는 요구를,
DB 스키마로부터 자동으로 도출할 방법은 없습니다.
이것은 판단이며, 판단은 정의의 외부에 있습니다.
두 가지 성질을 나열하기
| 관점 | 결정론적 생성 | 비결정론적 생성 |
|---|---|---|
| 입력 | 스키마나 정의 파일 | 자연어 프롬프트 |
...
두 가지는 경합하는 것이 아니라, 보완 관계에 있습니다.
어느 쪽이 담당할지를 다루는 대상에 따라 정리하면 다음과 같습니다.
| 다루는 대상 | 결정론적 생성 | 비결정론적 생성 | 이 설계에서의 담당 |
|---|---|---|---|
| 정의로부터 기계적으로 도출할 수 있는 부분 | 우수 | 미흡 | 결정론적 생성 |
| ... | |||
| 3행이 이 글에서 말하고자 하는 부분입니다. |
타입 정의(Type Definition)는 결정론적 생성의 출력이면서, 동시에 비결정론적 생성의 입력이기도 합니다.
두 가지 생성 방식은 타입 정의라는 동일한 대상을 매개로 연결됩니다.
왜 「결정론을 토대로 하는가」
이유 1: LLM이 가장 어려워하는 것은 일관성 유지
LLM은 하나의 함수를 작성하는 작업에는 매우 뛰어납니다.
하지만 20곳에 흩어져 있는 타입 정의를 모순 없이 유지하는 작업은 서툽니다.
테이블에 컬럼을 하나 추가했을 때,
타입 정의, API 스키마(API Schema), 유효성 검사(Validation), 시리얼라이저(Serializer), 테스트 픽스처(Fixture) 등
수정해야 할 곳은 연쇄적으로 확장됩니다.
이 연쇄는 기계적으로 도출할 수 있기 때문에, LLM보다 결정론적인 생성을 다룰 수 있는 도구가 더 적합합니다.
LLM에게 맡기면 대부분을 고친 뒤 일부를 고치지 않고 넘어가는 사고가 발생합니다.
결정론적 생성에 맡기면, 생성기가 대상으로 하는 범위에 한해서는 고치지 않고 넘어가는 일이 발생하지 않습니다.
이유 2: 결정론적 생성물이 LLM에 대한 제약이 됨
타입을 토대로 깔아두면, 비결정론적인 출력에 대한 자동 검증 장치를 얻을 수 있습니다.
그 위에 작성되는 소스 코드가 타입 검사(Type Check)를 통과하기 때문입니다.
LLM이 존재하지 않는 필드를 참조하면,
tsc
나 cargo check
가 즉시 에러를 반환합니다.
이 실패는 그대로 에이전트(Agent)에 대한 피드백으로 되돌릴 수 있습니다.
토대가 없는 상태에서 LLM에게 소스 코드를 쓰게 하면,
출력의 올바름을 인간이 읽고 판단하는 것 외에는 수단이 없습니다.
토대가 있다면 올바름의 일부를 기계 측에서 판정할 수 있습니다.
출력의 올바름은 tsc나 cargo check와 같은 타입 검사뿐만 아니라,
별도로 준비한 테스트 코드를 통과시킴으로써도 기계적으로 확인할 수 있습니다.
이는 테스트 주도 개발(TDD)에서의 Red to Green 공정에 해당합니다.
이유 3: 컨텍스트(Context) 절약
"사용자 테이블에는 이런 컬럼이 있고, API 응답은 이런 형식으로"와 같은 설명을 자연어로 작성하면,
내용이 길어질 뿐만 아니라 모호함이 남습니다.
생성된 타입 정의를 파일 하나로 읽히면, 동일한 정보를 더 정확하고
또한 짧게 전달할 수 있습니다.
기계 판독 가능한(Machine-readable) 정의는 인간을 위한 사양서보다
LLM을 위한 사양서로서 더 잘 기능합니다.
이유 4: 리뷰 대상을 좁힐 수 있음
결정론적 생성물은 생성기와 입력 정의만 확인하면,
출력을 하나씩 눈으로 쫓을 필요가 없습니다.
인간이 읽어야 할 소스 코드는 비결정론적 생성에 맡긴 부분으로만 좁혀집니다.
이 효과에 대해서는 후술할 「리뷰량이 인간의 처리 능력을 초과하는 문제에 대한 대응」에서 자세히 다루겠습니다.
책임의 경계선을 어떻게 그을 것인가
판정 기준
경계선의 판정은 다음 한 질문이면 충분합니다.
그 구현은 입력 정의로부터 기계적으로 도출할 수 있는가?
- 도출할 수 있는 경우: 결정론적 생성 쪽으로 분류합니다.
- 도출할 수 없는 경우: 비결정론적 생성에 맡깁니다.
도출할 수 없다는 것은, 즉 판단이 필요하다는 뜻입니다.
그리고 판단이 필요한 것이야말로 비즈니스 로직의 본체입니다.
구체적인 예시
| 하고 싶은 것 | 담당 |
|---|---|
| 사용자 테이블에 대응하는 타입 정의 | 결정론 |
| ... |
결정론 열에 나열되는 것들은 모두 사양서에 적으면 자동으로 결정되는 것들입니다.
비결정론 열에 나열되는 것들은 요구사항을 요건으로 일의적으로 정함에 있어,
이해관계자(Stakeholder) 간의 합의가 필요할 정도로,
사람의 의사 판단이 개입되는 복잡한 요건이라고 할 수 있습니다.
실제 레이어 구성
이 사고방식을 레이어(Layer)로 정리하면 다음과 같습니다.
각 레이어가 무엇에 대응하는지 정리하면 다음 표와 같습니다.
| Layer | 내용 | 생성·편집의 주체 | 편집 가능 여부 | 실패 시 되돌릴 곳 |
|---|---|---|---|---|
| Layer 0 | 단일 진실 공급원 (Single Source of Truth) | 인간 | 편집함 | — |
| ... |
Layer 0은 schema.prisma, openapi.yaml, .proto와 같은 단일 진실 공급원입니다.
Layer 0에 오류가 있으면, 거기서 생성되는 Layer 1 이후의 모든 것에 오류가 전달됩니다.
따라서 Layer 0만큼은 AI 에이전트에게 쓰게 하는 경우라도,
인간이 변경 사항을 반드시 리뷰합니다.
Layer 1은 타입 정의 (Type Definition), API 클라이언트 (API Client), 마이그레이션 (Migration), 기본적인 유효성 검사기 (Validator)와 같은 결정론적 생성물 (Deterministic Artifacts)입니다.
인간과 AI 모두 이를 직접 편집하지 않습니다.
편집하고 싶다면 Layer 0로 돌아가 정의를 수정하고 다시 생성합니다.
Layer 2의 구현이 타입과 일치하지 않는 경우에도, 되돌아갈 곳은 Layer 1이 아니라 Layer 0입니다.
Layer 2는 유스케이스 (Use Case), 도메인 서비스 (Domain Service), 화면 고유의 처리와 같은 비결정론적 생성물 (Non-deterministic Artifacts)입니다.
여기서 LLM이 참조하는 것은 Layer 1의 타입입니다.
타입이 존재하기 때문에, LLM은 "어떤 필드를 사용할 수 있는지"를 추측할 필요가 없습니다.
Layer 3은 타입 검사 (Type Checking), 테스트 (Test), Lint, 계약 테스트 (Contract Test)와 같은 검증층 (Verification Layer)입니다.
이 실패는 AI 에이전트에게 코드를 작성하게 하는 경우, 그대로 다음 턴의 입력으로 전달할 수 있습니다.
타입 정의의 부족이 원인이라고 판명된 경우에만 Layer 0까지 되돌립니다.
Layer 3 중 테스트만은 취급이 다릅니다.
테스트 또한 LLM이 작성하기 때문에, 그 자체로는 비결정론적인 생성물입니다.
인간이 기대값을 확인하고 승인한 시점에 비로소 판정 기준으로 기능합니다.
이 점에 대해서는 후술할 "예상되는 반론"에서 자세히 다루겠습니다.
운용 규칙
생성물을 편집하게 하지 않는다
Layer 1을 편집하는 순간, 이 설계는 성립할 수 없게 됩니다.
재생성 시 사라질 변경 사항을 사람이 추가하면, 재생성할 수 없는 디렉터리가 만들어지기 때문입니다.
먼저, 생성물임을 리포지토리 측에서 명시합니다.
src/generated/** linguist-generated=true
이 설정을 넣으면 GitHub의 PR 화면에서 해당 파일의 차이점(diff)이 기본적으로 접힙니다.
언어 통계에서도 제외됩니다.
편집 자체를 금지하는 효과는 없지만, 리뷰 시에 생성물이 시야에 들어오지 않게 된다는 점에서 후술할 리뷰 범위 축소에 직접적인 영향을 줍니다.
다음으로, AI 에이전트에게 금지 사항으로 전달합니다.
## 생성 코드의 취급
- `src/generated/` 하위는 자동 생성물입니다. 직접 편집하지 마세요.
- 출력을 바꾸고 싶다면 `prisma/schema.prisma`를 변경하고,
...
Claude Code를 사용하고 있다면 설정으로도 방지할 수 있습니다.
{
"permissions": {
"deny": [
...
더욱 확실하게 방지하고 싶다면, PreToolUse 이벤트에서 발생하는 Hook을 사용하여, Write와 Edit 도구가 src/generated/**를 대상으로 하는 호출 자체를 거부하는 방법도 있습니다.
설정 파일의 deny 규칙은 승인 화면에서 덮어씌워질 가능성이 있지만, Hook을 통한 거부는 커맨드 실행 자체를 중단시킬 수 있습니다.
CI에서 재생성 차이를 검출한다
로컬에서의 재생성 누락은 CI에서 기계적으로 검출합니다.
- name: 재생성하여 차이를 검출한다
run: |
pnpm generate
...
생성물을 커밋하지 않는 운용 방식도 괜찮지만, 그 경우에는 LLM이 타입을 읽을 수 있도록 생성 타이밍을 에이전트의 작업 전에 배치해야 합니다.
워크플로우 예시
실제 1 사이클은 다음과 같은 흐름이 됩니다.
1. 정의를 변경한다
model User {
id String @id @default(cuid())
email String @unique
...
2. 재생성한다
pnpm prisma generate
이 시점에서 deletedAt을 포함한 타입이 Layer 1에 생성됩니다.
export type User = {
id: string;
email: string;
...
3. 비즈니스 로직을 LLM에게 작성하게 한다
여기서 처음으로 프롬프트를 던집니다.
src/generated/user.ts의 User 타입을 사용하여,
탈퇴한 사용자의 복구 처리를 src/usecase/restoreUser.ts에 구현해 주세요.
사양:
...
분리된 가부 판정은 대체로 다음과 같은 형태가 됩니다.
Result 타입은 src/lib/result.ts에 이미 선언되어 있다는 전제입니다.
import type { User } from "../generated/user";
import type { Result } from "../lib/result";
const RESTORABLE_DAYS = 30;
...
프롬프트에는 타입(Type)에 관한 이야기가 단 한 글자도 나오지 않습니다.
deletedAt가 Date | null이라는 사실은 Layer 1의 타입이 이미 전달하고 있습니다.
프롬프트에 적혀 있는 것은 판단의 내용뿐입니다.
4. 검증을 돌리기
pnpm tsc --noEmit && pnpm test
여기서 발생한 오류 내용을 그대로 에이전트(Agent)에게 돌려줍니다.
30일의 경계 조건(Boundary condition)을 틀렸다면, 테스트가 이를 알려줄 것입니다.
왜 이것이 표준이 될 수 있는가
비용의 무게중심은 '쓰는 것'에서 '고치는 것'으로 옮겨갔다
LLM(Large Language Model)에 의해 소스 코드를 쓰는 비용은 극적으로 낮아졌지만,
오류를 찾아내고 그것을 수정하게 만드는 코드 리뷰(Code review)는 여전히 존재하며,
그것이 개발의 핵심적인 작업이 되어가고 있습니다.
그렇게 되면 합리적인 전략은 명확합니다.
기계적으로 도출할 수 있는 부분은 결정론(Deterministic)에 가깝게 만들고,
수정 비용이 발생할 수 있는 부분을 최소화하는 것입니다.
결정론적으로 처리할 수 있는 것을 LLM에게 쓰게 하면,
생성 비용은 낮게 유지할 수 있어도, 오류를 찾아내고 고치는 비용이 더 높게 발생합니다.
리뷰량이 인간의 처리 능력을 초과하는 문제에 대한 대응
에이전트에게 개발을 맡기면,
리뷰해야 할 차분(Diff)의 양이 인간의 처리 능력을 초과합니다.
이 문제에 대해 리뷰를 빠르게 하는 것만으로는 한계가 있습니다.
리뷰해야 할 차분 그 자체를 줄이는 방향이 필요합니다.
결정론적인 생성물을 리뷰 대상에서 제외할 수 있다면,
인간이 읽어야 할 차분은 판단이 포함된 부분뿐이게 됩니다.
차분의 양이 아니라, 차분의 밀도가 높아지는 형태입니다.
PR(Pull Request)에서 주목해야 할 차분은, 정의가 어떻게 변했는지를 보여주는 Layer 0의 차분과,
그 정의를 사용하여 작성된 기능 코드를 보여주는 Layer 2의 차분, 이 두 곳으로 압축됩니다.
Layer 1의 차분은 정의로부터 기계적으로 도출된 것에 불과하므로,
생성기(Generator)의 버전만 바뀌지 않았다면 읽지 않고 넘어가도 무방합니다.
차분의 행수가 아니라, 읽어야 할 파일의 수가 압축됩니다.
인간이 리뷰해야 할 관점은 이 시점에서 다음 4가지로 수렴합니다.
- 로직이 요구사항에 따라 작성되었는가
- 로직이 기대한 대로 동작하는가
- 퍼포먼스(Performance) 측면에서 문제가 없는가
- 보안상의 취약점이 존재하지 않는가
애초에 타입 정의에 부족함이 있는지는 볼 필요가 없게 되므로,
인간과 AI 모두 확인해야 할 파일 수가 줄어들어 컨텍스트(Context) 부하가 낮아진다는
부차적인 이점도 생깁니다.
백엔드 재작성에 강하다
장래에 프레임워크나 언어를 바꾸어 백엔드를 재작성해야 하는 상황을 고려하면,
이 구조에는 또 하나의 장점이 있습니다.
Layer 0의 스키마(Schema)는 생성기를 교체하는 것만으로
다른 언어용 Layer 1을 출력할 수 있습니다.
.prisma나 .proto와 같은 정의 파일 자체는 다시 쓸 필요가 없습니다.
문제는 Layer 2입니다. 이곳은 보통 언어마다 인간이 직접 이식(Porting)하게 됩니다.
하지만 Layer 2가 자연어 프롬프트나 이슈(Issue) 내용을 읽고 생성한 것이라면 이야기가 달라집니다.
그것들이 어느 정도 기록으로 남아 있다면, 출력 내용에 어느 정도 차이는 있더라도
기록에는 '무엇을 구현해야 하는가'가 그대로 남아 있기 때문에,
새로운 언어의 Layer 1 타입에 대해 동일한 프롬프트를 다시 던지는 것만으로도
원래의 코드베이스와 유사한 내용의 구현을 얻을 가능성이 높다고 생각됩니다.
Layer 2의 자산은 소스 코드 그 자체가 아니라,
그것을 만들어낸 프롬프트 쪽에 있다고 볼 수 있습니다.
이식(Porting)이란 코드를 한 줄씩 바꿔 쓰는 작업이 아니라,
축적된 프롬프트를 새로운 토대 위에 재실행하는 작업이 됩니다.
이 때문에 프롬프트도 일회용으로 쓰지 않고,
specs/restoreUser.md와 같은 형태로 Layer 0와 나란히 자산화해 두면,
재작성 시 그대로 재사용할 수 있습니다.
하네스(Harness) 논의와의 관계
AI 에이전트의 제어 구조에 대해서는
이미 몇 가지 논의가 쌓여 있습니다.
특히 후자의 논문에서는 결정론적인 조작을 코드베이스의 도구(Tool)로 보유하고,
유연한 제어 로직을 자연어로 분리하는 아키텍처가 제시되어 있습니다.
이 글에서 논하는 분계점은,
그러한 사고방식을 애플리케이션 코드 측면으로 가져온 것이라 할 수 있습니다.
제어 로직(Control Logic) 계층에서 성립하는 분리가,
구현 계층에서도 동일하게 성립한다는 이야기입니다.
사양 주도 개발(Specification-Driven Development)과 테스트 주도 개발(Test-Driven Development)과의 관계
이 구조는 사양 주도 개발과 테스트 주도 개발, 양측의 발상과도 연결됩니다.
Kiro나 spec-kit과 같은 사양 주도 개발은,
코드를 작성하기 전에 사양을 단일 진실 공급원(Single Source of Truth)으로 둔다는 점에서,
이 글의 Layer 0와 동일한 발상에 서 있습니다.
다른 점은, 그 근원을 자연어로 작성하느냐, 기계 판독 가능한 형식으로 작성하느냐입니다.
두 방식은 배타적이지 않습니다.
형식으로 옮길 수 있는 부분을 스키마(Schema)로 작성하고,
옮길 수 없는 부분을 자연어 사양으로 남겨둔다면, 그대로 조합할 수 있습니다.
테스트 주도 개발(TDD)은 조금 다른 각도에서 이 구조에 연결됩니다.
비결정론적 생성(Non-deterministic Generation)의 약점은 출력할 때마다 결과가 달라진다는 것입니다.
따라서 정답 여부를 판정하는 기준 측이 불안정하다면,
LLM의 출력을 신뢰할 근거를 잃게 됩니다.
테스트는 바로 이 판정 기준을 고정하는 역할을 수행합니다.
앞서 언급한 canRestore 예로 들자면,
30일이라는 경계 조건(Boundary Condition)을 확인하는 테스트만 있다면,
구현이 어떤 모델을 사용했든, 몇 번을 다시 생성했든 관계없이,
정답 여부를 기계적으로 판정할 수 있습니다.
테스트는 사양서인 동시에,
비결정론적 생성물에 대한 결정론적인 수락 기준(Acceptance Criteria)이기도 합니다.
프롬프트에 사양을 쓰고, 테스트에 합격/불합격 기준을 쓰는 분업은,
Layer 2와 Layer 3의 관계 그 자체라고 할 수 있습니다.
예상되는 반론
LLM의 성능이 올라가면 불필요해지지 않을까
정합성(Consistency) 유지가 능숙해지더라도, 이 설계의 전제는 무너지지 않을 것이라고 생각합니다.
결정론적 생성의 가치는 출력의 정밀도가 아니라 재현성(Reproducibility)에 있기 때문입니다.
동일한 입력으로부터 항상 동일한 출력이 반환된다는 성질은,
모델의 성능을 아무리 높여도 얻을 수 없습니다.
재현성이 없는 한, 출력이 올바른지 매번 확인할 필요가 남습니다.
다만, 결정론에 가깝게 가져가야 할 범위는 좁아질 가능성이 있습니다.
현시점에서 LLM에게 맡기기 위험하다고 여겨지는 작업 중 일부는,
장래에 LLM으로 옮길 수 있게 될 것이라 생각합니다.
타입이 없는 언어에서는 성립하지 않을까
정적 타입 언어(Statically Typed Language)를 전제로 한 논의라는 점은 맞습니다.
즉각적으로 결과를 반환하는 타입 검사(Type Check)라는 검증 수단을 통해, 이 설계의 효과는 뒷받침됩니다.
동적 타입 언어(Dynamically Typed Language)에서는 그 역할을 Pydantic이나 Zod와 같은
런타임 스키마 검증(Runtime Schema Validation)과 테스트가 담당하게 됩니다.
이것들 또한 스키마 정의로부터 생성할 수 있으므로, Layer 0와 Layer 1의 관계 자체는 성립합니다.
다만, 오류를 검출하는 타이밍이 컴파일 시점에서 런타임 시점으로 늦춰집니다.
그만큼 피드백 속도는 떨어지기 때문에,
정적 타입 언어보다 효과는 약해질 것이라 생각합니다.
LLM이 작성한 테스트로 검증하는 것은 순환 논리(Circularity)가 아닌가
이것은 맞는 지적이며, 이 설계의 약점입니다.
LLM이 사양을 오해할 경우, 구현과 테스트가 동일한 오해를 공유하게 됩니다.
그 결과, 테스트는 통과하지만 사양에는 맞지 않는 상태가 발생합니다.
테스트가 자동으로 정답을 보장해 주는 것은 아닙니다.
따라서 테스트는 인간이 기대값을 확인하고 승인한 시점에,
비로소 판정 기준으로서 기능합니다.
실무적으로는 다음과 같은 방법을 통해 순환을 어느 정도 피할 수 있습니다.
- 경계값과 기대값만은 인간이 먼저 작성하고, 구현을 LLM에게 맡긴다
- 사양서의 수락 조건(Acceptance Condition)을 그대로 테스트 케이스 명으로 작성하게 한다
- 구현과 테스트를 서로 다른 세션에서 생성하게 하여, 전제 조건의 공유를 줄인다
한계와 주의점
생성기(Generator) 설계 비용은 인간에게 남는다
어떤 생성기를 선택하고 어디까지를 스키마로 표현할 것인가 하는 설계는,
결정론과 비결정론 어느 쪽에도 해당하지 않습니다.
이 부분은 인간의 업무로 남습니다.
이 설계를 그르치면, 스키마의 편의에 구현이 휘둘리는 상황을 초래하게 됩니다.
스키마의 표현력을 넘어서는 제약은 남는다
"시작일은 종료일보다 앞서야 한다"와 같은 제약은,
많은 스키마 언어에서 표현할 수 없습니다.
이러한 제약은 Layer 2나 Layer 3가 떠맡습니다.
결정론 측에 모든 것을 밀어 넣으려 하지 않는 판단이 필요합니다.
결정론적 레이어를 너무 늘리지 말 것
생성기는 늘릴수록 변경의 유연성을 빼앗습니다.
사양이 빈번하게 흔들리는 영역을 무리하게 스키마화하면,
사양 변경이 있을 때마다 생성기의 설정을 몇 번이고 다시 조정해야 하는 상황이 됩니다.
안정적인 부분만 결정론적 (Deterministic)으로 가져가고,
흔들리는 부분은 비결정론적 (Non-deterministic)인 상태로 남겨두는 판단이 유효합니다.
최종적인 책임은 인간에게 있다
타입 검사 (Type checking)를 통과하는 것과 사양으로서 올바른 것은 별개의 문제입니다.
Layer 3의 모든 검증을 통과하더라도, 판단 그 자체의 타당성은 보장되지 않습니다.
판단이 포함된 차이(diff)만큼은 인간의 눈을 거칠 필요가 있습니다.
이 기사의 설계는 읽어야 할 양을 줄이기 위한 것이지,
읽지 않고 넘어가기 위한 것이 아닙니다.
마치며
결정론적 생성과 비결정론적 생성은,
어느 쪽이 더 우월한지를 겨루는 관계가 아닙니다.
- 기계적으로 도출할 수 있는 것은 결정론적 생성으로 토대를 구축한다
- 판단이 필요한 것은 비결정론적 생성으로 그 위에 얹는다
- 양자의 정합성은 검증층이 기계적으로 판정한다
이 세 가지 점을 지키면, AI 에이전트와의 개발은 안정되기 쉬워질 것이라고 생각합니다.
코드 생성 도구는 이전부터 존재해 왔지만,
LLM이라는 비결정론적인 생성기가 더해짐으로써,
그 역할은 수고의 절감에서 AI에 대한 제약 제공으로 변하고 있다고 생각합니다.
토대를 정성스럽게 닦아 놓는 것이 곧 생성물의 품질로 이어집니다.
생성기 설정 파일을 재검토하는 것부터 시작해 보시는 건 어떨까요?
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기