AI Agent Engineering / 2
울타리는 타입이다
AI 펜스 엔지니어링에 대한 단상
목차
개발자라면 누구나 any의 유혹을 안다. 처음엔 빠르다. 타입을 신경 쓸 필요 없으니 코드가 술술 나온다. 하지만 프로젝트가 커지면 any는 부채가 된다. 어디서 무엇이 깨졌는지 런타임이 되어서야 알게 된다. 결국 우리는 타입을 선언한다. 매 라인마다 "이건 string이야"라고 지시하는 게 아니라, 경계를 선언해두고 그 안에서 코드가 자유롭게 흐르게 하는 것. 위반하면 컴파일 타임에 잡힌다.
나는 에이전트 시스템도 정확히 같은 전환점에 서 있다고 생각한다.
하네스가 만든 것
이전 글에서 나는 에이전트를 함수에 비유했다. 관심사를 분리하고, 서브에이전트를 조합하고, 오케스트레이터가 흐름을 설계하는 것. 하네스 엔지니어링.
하네스의 본질은 제어가 아니라 구조화다. 복잡한 문제를 분해 가능한 단위로 나누고, 각 단위를 조합하여 전체를 구성하는 것. 어떤 에이전트를 쓸지, 어떤 조건에서 분기할지, 어떤 결과를 다음으로 넘길지 — 이 오케스트레이션이 없으면 에이전트 시스템은 존재할 수 없다.
하네스는 에이전트 시스템의 골격이다. 골격 없이 움직이는 생명체는 없다.
하지만 골격이 있다고 해서 그 생명체가 어디로 갈지 알 수 있는 것은 아니다.
구조 위의 경계
하네스가 "이 에이전트들을 어떻게 조합할 것인가"의 문제라면, 그 다음 질문은 자연스럽게 이것이 된다. "이 조합된 시스템이 어디까지 움직일 수 있는가."
모델이 충분히 똑똑해지면, 구조를 잘 짰다는 것만으로는 부족해진다. 서브에이전트 하나하나가 자기 역할 안에서 정확히 동작해도, 시스템 전체가 의도한 방향으로 가고 있는지는 별개의 문제다. 부품이 다 좋아도 완성품이 엉뚱한 곳으로 걸어갈 수 있다.
필요한 것은 구조 위의 경계다.
이 경계를 설계하는 것. 나는 이것을 펜스 엔지니어링이라고 부른다.
하네스는 JavaScript, 펜스는 TypeScript
비유를 하나 세워보자.
JavaScript에서 함수를 호출하면, 그 함수가 무엇을 리턴할지는 실행해봐야 안다. 인풋에 뭐가 들어올지도 보장이 없다. 그래서 우리는 방어 코드를 곳곳에 심는다. if (typeof x !== 'string') throw Error. 이것은 런타임 제어다. 문제가 터진 후에야 잡는다.
TypeScript는 접근이 다르다. 함수의 인풋과 아웃풋에 타입을 선언한다. 내부 구현은 자유롭지만, 경계에서의 계약은 명확하다. 위반은 코드가 실행되기 전에 잡힌다.
하네스 엔지니어링은 에이전트들을 조합하고 연결하는 구조를 설계한다. 어떤 에이전트에게 어떤 인풋을 넘기고, 어떤 순서로 파이프라인을 구성할지. 이것은 함수 합성이다.
펜스 엔지니어링은 그 조합된 시스템이 움직일 수 있는 영역의 타입을 선언한다. 목표, 권한, 품질 기준, 자원 한도. 내부에서 어떤 순서로, 어떤 판단을 내리든 에이전트의 자율에 맡기되, 경계를 넘으면 시스템 레벨에서 차단된다. 이것은 타입 선언이다.
핵심 차이는 관점이다. 하네스는 "어떻게 나눌까"를 묻는다. 펜스는 "어디까지 허용할까"를 묻는다.
그러나 비유에는 균열이 있다
여기서도 한 발 물러서야 한다. 이전 글에서 "에이전트는 함수가 아니다"라고 꺾었듯이, 이 비유에도 한계가 있다.
TypeScript의 타입 체크는 컴파일 타임에 일어난다. 코드가 실행되기 전에 위반을 잡는다. 하지만 에이전트 시스템은 본질적으로 런타임 시스템이다. 에이전트의 행동은 실행 중에만 관찰할 수 있고, 경계 위반도 실행 중에 감지된다.
그래서 에이전트의 펜스는 TypeScript의 타입 체크보다는 런타임 contract enforcement에 가깝다. 타입처럼 선언하되, 실행 중에 검증하는 것. 사전에 경계를 정의한다는 점에서 타입적이고, 실행 중에 강제한다는 점에서 런타임적이다.
이 이중성을 인식하는 것이 중요하다. 펜스를 타입"처럼" 설계하되, 타입"이라고" 착각하지 않는 것. 이전 글에서 에이전트를 함수"처럼" 다루되 함수"라고" 착각하지 않는 것과 같은 긴장이다.
any의 유혹, 다시
TypeScript를 쓰더라도 any를 남발하면 JavaScript와 다를 바 없다. 마찬가지로, 펜스를 세우더라도 경계가 너무 느슨하면 에이전트는 의미 없는 방향으로 드리프트한다.
"좋은 결과를 내라"는 것은 울타리가 아니다. 그것은 any다.
반대도 마찬가지다. 모든 필드에 리터럴 타입을 걸면 타입은 정확하지만 코드는 경직된다. 울타리를 너무 촘촘하게 치면, 에이전트의 자율성은 사라지고 결국 매 스텝을 지시하는 것과 다를 바 없어진다.
이것은 TypeScript에서 우리가 매일 하는 트레이드오프와 동일하다. strict: true로 시작하되, 필요한 곳에서는 제네릭으로 유연성을 확보한다. 경계의 단위(granularity)를 조절하는 감각. 이것이 펜스 엔지니어링의 난이도가 숨어 있는 곳이다.
인터페이스로서의 울타리
TypeScript에서 좋은 설계는 구현이 아닌 인터페이스에 의존한다. 내부가 어떻게 돌아가든 상관없다. 계약만 지키면 된다.
펜스도 마찬가지다. 좋은 울타리는 에이전트에게 "어떻게 하라"를 말하지 않는다. "무엇을 지켜라"를 말한다.
// 하네스적 접근 — 실행 흐름을 조합
1. 데이터 수집 에이전트를 호출한다
2. 수집 결과를 분석 에이전트로 넘긴다
3. 분석 결과를 요약 에이전트로 넘긴다
4. 요약을 검증 에이전트로 넘긴다
// 펜스적 접근 — 경계를 선언
Goal: 주어진 데이터에 대한 검증된 인사이트를 도출하라
Constraints:
- 모든 주장은 데이터 소스가 명시되어야 한다
- 결론의 신뢰도를 high/medium/low로 표기하라
- 2개 이상의 관점에서 교차 검증하라
Quality Gate: 검증되지 않은 주장이 포함되면 반려
하네스적 접근은 절차적이다. 에이전트를 조합하고 데이터의 흐름을 설계한다. 이것은 강력하다. 복잡한 문제를 분해 가능하게 만들고, 디버깅이 가능하고, 결과가 예측 가능하다.
펜스적 접근은 선언적이다. 에이전트가 어떤 순서로 도달하든, 계약을 만족하면 통과한다. 새로운 제약을 추가해도 기존 흐름을 건드릴 필요가 없다.
이것은 imperative vs declarative의 오래된 구분이기도 하다. 그리고 소프트웨어의 역사는 대체로 declarative 쪽으로 흘러왔다. SQL, React, Kubernetes. 우리는 "어떻게"보다 "무엇을"을 선언하는 방향으로 계속 이동해왔다. 에이전트 시스템도 같은 흐름 위에 있다고 본다.
TypeScript가 JavaScript 위에 얹히듯
중요한 지점이 있다. TypeScript는 JavaScript를 대체한 것이 아니다. JavaScript 위에 타입 레이어를 얹은 것이다. 컴파일하면 결국 JavaScript가 된다.
펜스 엔지니어링도 하네스를 대체하지 않는다. 하네스 위에 경계 레이어를 얹는 것이다. 내부적으로 서브에이전트의 관심사 분리, 오케스트레이터의 조합과 라우팅 — 이런 하네스의 구조는 그대로 유효하다. 다만 그 위에 "이 시스템 전체가 어디까지 움직일 수 있는가"라는 경계가 선언되는 것이다.
이 관계를 이해하는 것이 중요하다. 펜스를 하네스의 "다음 단계"로 보는 것은, TypeScript를 JavaScript의 "상위 버전"으로 보는 것만큼 부정확하다. 그것은 레이어다.
모델이 아직 충분히 강하지 않은 영역에서는 하네스의 구조가 더 촘촘해야 한다. 모델이 충분히 강한 영역에서는 펜스로 물러나는 것이 효율적이다. 하나의 시스템 안에서도 영역마다 하네스와 펜스의 비율이 다를 수 있다. TypeScript 프로젝트에서 특정 모듈만 any를 허용하는 것처럼.
strict 모드의 교훈
TypeScript의 strict: true는 흥미로운 선택이다. 기본적으로 모든 것을 엄격하게 잡되, 개발자가 명시적으로 느슨하게 풀 수 있게 한다. "기본값이 안전한 쪽"인 것이다.
펜스 설계에도 같은 원칙이 적용된다. 기본적으로 에이전트의 행동 범위를 좁게 잡고, 필요에 따라 명시적으로 권한을 확장하는 것이 안전하다. 처음부터 넓은 울타리를 치고 문제가 생기면 좁히는 것은, strict: false에서 시작해서 나중에 타입 에러를 잡아나가는 것과 같다. 가능은 하지만, 고통스럽다.
이것은 에이전트 시스템의 신뢰 문제와도 맞닿아 있다. strict 모드는 컴파일러가 개발자를 신뢰하지 않겠다는 선언이 아니다. 개발자가 실수할 수 있다는 것을 인정하고, 시스템 레벨에서 그 실수를 잡아주겠다는 계약이다. 에이전트에 대한 펜스도 마찬가지다. 에이전트를 불신하는 것이 아니라, 확률적 존재의 분산을 시스템 레벨에서 관리하겠다는 설계 철학이다.
결국은 같은 감각
이전 글의 결론과 같은 곳에 도달한다. 이것은 AI만의 새로운 문제가 아니다.
타입을 선언하고, 인터페이스를 설계하고, strict 모드의 강도를 조절하고, 구현이 아닌 계약에 의존하는 것. 우리가 코드를 짜면서 수없이 훈련해 온 감각이 에이전트 시스템의 경계를 설계하는 데 그대로 적용된다.
프롬프트 엔지니어링이 "에이전트에게 무엇을 시킬 것인가"의 문제였다면, 하네스 엔지니어링은 "에이전트들을 어떻게 구조화할 것인가"의 문제였다. 그리고 펜스 엔지니어링은 "에이전트가 어디까지 움직일 수 있는가"의 문제다.
세 가지는 대립하지 않는다. 레이어다. 좋은 프롬프트 위에 좋은 하네스가 있고, 좋은 하네스 위에 좋은 펜스가 있다. 그리고 그 모든 것을 관통하는 설계 감각은 — 결국 소프트웨어 엔지니어링이라는 하나의 뿌리에서 나온다.
AI 펜스 엔지니어링. 에이전트의 자율을 설계하는 기술이라기보다, 자율의 경계를 타입처럼 선언하는 기술. 나는 그렇게 이해하고 있다.