본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 26. 03:58

AI 에이전트 구성을 위한 함수형 옵션 패턴 (Functional Options Pattern)

요약

AI 에이전트 API가 복잡해짐에 따라 발생하는 '생성자 수프' 문제를 해결하기 위해 Go 언어의 함수형 옵션 패턴을 제안합니다. 이 패턴을 통해 에이전트 구성 시 가독성을 유지하고 확장 가능한 API를 구축하는 방법을 다룹니다.

핵심 포인트

  • 복잡한 에이전트 설정 시 발생하는 생성자 인자 과다 문제 지적
  • Go 언어의 함수형 옵션 패턴을 통한 API 설계 개선
  • With* 함수를 활용한 선택적 동작의 안전하고 깔끔한 노출
  • 확장성과 가독성을 동시에 확보하는 에이전트 구성 방식

대부분의 AI 에이전트 API는 생성자 수프 (constructor soup)로 변하고 있습니다. 도구(tools)를 추가하고, 그다음 메모리(memory), 훅(hooks), 승인(approvals), 재시도(retries), 핸드오프(handoffs), 모델 설정(model settings), 텔레메트리(telemetry)를 계속 추가하다 보면, 어느덧 당신의 "간단한" NewAgent(...) 호출은 6개월간의 제품 결정 사항을 파헤치는 고고학적 발굴 현장처럼 보이게 됩니다.

Go 언어는 수년 전에 이 문제를 해결했습니다. **함수형 옵션 패턴 (functional options pattern)**은 단순하게 시작하여 안전하게 확장되고 가독성을 유지하는 API를 구축하는 가장 깔끔한 방법 중 하나입니다. Harness as Code의 참조 구현체인 AI Harness를 구축하고 Harness as Code에 대해 글을 쓴 후, 저는 동일한 패턴이 **AI 에이전트 구성 (AI agent composition)**에도 놀라울 정도로 잘 적용된다는 확신을 갖게 되었습니다.

에이전트가 Go 언어로 작성되었기 때문이 아닙니다.

에이전트가 정확히 동일한 형태의 문제를 가지고 있기 때문입니다.

왜 Go는 애초에 함수형 옵션을 선택했는가

친절한 API를 위한 함수형 옵션 (functional options for friendly APIs)에 관한 Dave Cheney의 고전적인 포스트는 여전히 최고의 시작점입니다. 그의 논거는 간단했습니다. 선택적 동작(optional behavior)을 계속 추가하다 보면 생성자 시그니처(constructor signatures)가 빠르게 취약해진다는 것입니다. 팀들은 보통 다음과 같은 동일한 잘못된 과정을 반복합니다:

  1. 작고 깔끔한 생성자로 시작합니다.
  2. 더 많은 위치 인자(positional arguments)를 추가합니다.
  3. 포기하고 설정 구조체(config struct)를 도입합니다.
  4. 결국 "기본값 사용"을 나타내기 위해 제로 값(zero values)이나 nil을 전달하게 됩니다.

이 방식은 작동하다가 어느 순간 작동하지 않게 됩니다.

Cheney는 정확한 문제점들을 지적했습니다: 낮은 발견 가능성(discoverability), 어색한 기본값, 컴파일러를 만족시키기 위해서만 존재하는 nil 또는 빈 설정 값, 그리고 시간이 지날수록 진화하기 어려워지는 API입니다. 그의 대안은 우아했습니다: 기본 경로를 아주 작게 유지하고, 제어된 방식으로 설정을 변경하는 With* 함수들을 통해 동작을 노출하는 것입니다.

그 패턴은 이론에 머물지 않았습니다. 성숙한 Go 라이브러리 전반에서 동일한 형태를 볼 수 있습니다:

  • gRPC는 WithSharedWriteBuffer, WithAuthority, 그리고 인터셉터(interceptor) 관련 옵션과 같은 다양한 DialOption 값들을 노출합니다.
  • go-containerregistry는 옵션이 유효하지 않을 때의 검증을 포함하여 WithContext, WithPlatform, WithJobs, WithRetryBackoff와 같은 Option 함수들을 노출합니다.
  • 자신의 프로파일링 패키지를 리팩터링한 Cheney의 후속 작업은 이 패턴이 왜 강력한지를 보여줍니다. 기본값(defaults)은 더 단순해졌고, 유효하지 않은 조합을 추론하기가 더 쉬워졌으며, 새로운 기능이 나타날 때마다 공개 API(public API)가 계속해서 커지는 현상도 멈췄습니다.

이것이 진정한 승리입니다.

함수형 옵션(Functional options)은 단순히 Go 언어의 귀여운 관용구(idiom)가 아닙니다. 그것은 API를 위한 성장 전략입니다.

AI 에이전트도 동일한 API 성장 문제를 겪고 있습니다

The inevitable API growth progression from a clean constructor to constructor hell, with functional options as the exit ramp

모든 에이전트 SDK가 따르게 되는 필연적인 과정 — 깔끔한 생성자(constructor)에서 혼돈으로. 함수형 옵션은 탈출로를 제공합니다.

현대적인 에이전트 시스템이 패키징해야 하는 것들을 살펴보십시오.

OpenAI의 에이전트 정의 가이드에 따르면, 에이전트는 모델(model), 지침(instructions), 도구(tools), 핸드오프(handoffs), 가드레일(guardrails), 승인(approvals), 구조화된 출력(structured output), 그리고 MCP 기반 기능(MCP-backed capabilities)을 포함할 수 있습니다. 오케스트레이션 및 핸드오프가드레일 및 인간 검토에 관한 OpenAI의 문서는 이 점을 더욱 명확하게 짚어줍니다: 실제 에이전트의 표면적(surface area)은 빠르게 확장됩니다.

Anthropic 또한 런타임 (runtime) 측면에서 유사한 이야기를 해왔습니다. Building Effective Agents에서 그들의 팀은 가장 성공적인 시스템은 불필요한 프레임워크 복잡성 대신 **단순하고 조합 가능한 패턴 (simple, composable patterns)**을 사용한다고 주장합니다. 또한 Effective harnesses for long-running agents에서 그들은 하네스 (harness)를 에이전트가 여러 컨텍스트 윈도우 (context windows)에 걸쳐 계속해서 진전을 이룰 수 있도록 돕는 계층으로 설명합니다.

바로 이 지점에서 함수형 옵션 (functional options)이 빛을 발합니다.

만약 당신의 에이전트 생성자 (constructor)가 다음과 같다면, 당신은 이미 실패한 것입니다:

agent := NewAgent(
    model,
    instructions,
...

아무도 일곱 번째 파라미터 (parameter)를 기억하지 못합니다. 어떤 것이 진정으로 선택 사항 (optional)인지 아무도 모릅니다. 그리고 다음 기능 요청이 들어오는 순간, 시그니처 (signature)가 더 나빠질 것이라는 점은 보장된 것이나 다름없습니다.

함수형 옵션 버전은 에이전트 시스템이 실제로 진화하는 방식에 훨씬 더 가깝습니다:

type AgentOption func(*Agent)

func WithTool(t Tool) AgentOption {
...

이제 기본 경로는 명확해졌고, 고급 경로는 마치 하나의 문장처럼 읽힙니다:

agent := NewAgent(
    claudeSonnet,
    researcherPrompt,
...

이것은 더 나은 API 설계이지만, 더 중요한 것은 더 나은 **아키텍처 커뮤니케이션 (architecture communication)**이라는 점입니다.

에이전트에서 옵션의 최선의 활용: 단순 설정이 아닌 조합 (Composition)

Hub-spoke diagram showing an Agent Core surrounded by orthogonal composable concerns: tools, safety, hooks, memory, routing, and observability

각 옵션은 작은 아키텍처적 움직임을 나타냅니다 — 직교하는 관심사 (orthogonal concerns)들이 안정적인 코어를 중심으로 독립적으로 조합됩니다.

이 부분이 대부분의 팀이 놓치고 있는 지점이라고 생각합니다.

함수형 옵션은 종종 필드 (fields)를 설정하는 더 깔끔한 방법으로 설명되곤 합니다. 그것도 맞는 말이지만, 이 패턴의 가치를 과소평가하는 것입니다. 에이전트 시스템의 경우, 더 큰 보상은 옵션이 동작을 위한 **조합 언어 (composition language)**가 된다는 점에 있습니다.

각 옵션은 다음과 같은 기능적 표면 (capability surface)을 추가하거나 변경할 수 있습니다:

  • 도구 (tools)
  • 미들웨어 (middleware)
  • 도구 실행 전/후 훅 (pre/post tool hooks)
  • 승인 게이트 (approval gates)
  • 메모리 백엔드 (memory backends)
  • 모델 라우팅 규칙 (model routing rules)
  • 텔레메트리 싱크 (telemetry sinks)
  • 재시도 정책 (retry policies)
  • 전문 에이전트로의 핸드오프 (handoffs to specialist agents)
  • 컨텍스트 필터 (context filters)

다시 말해, 옵션은 단순한 "파라미터 (parameters)"를 넘어 **작은 아키텍처적 움직임 (small architectural moves)**이 됩니다.

이는 제가 컨텍스트 엔지니어링 (context engineering)과 하네스 (harness) 설계를 생각하는 방식과 깔끔하게 일치합니다. 모든 미래의 동작을 미리 알고 있는 하나의 거대한 생성자 (god constructor)를 원하는 것이 아닙니다. 아주 작은 코어와 명시적인 조합 지점 (composition points)을 원하는 것입니다.

이 패턴이 AI 하네스에 특히 잘 맞는 이유

AI Harness는 이미 매우 문자 그대로 이 방향을 지향하고 있습니다.

이 프로젝트의 아티팩트 컴포저 (artifact composer)는 artifact/options.go에 구현된 함수형 옵션 (functional options)을 기반으로 하는 ComposeWith API를 노출합니다. 여기서 옵션은 무작위적인 토글 (toggles)이 아닙니다. 그것들은 조합 (composition)이 어떻게 동작할지를 결정합니다:

// 활성화된 아티팩트만 포함 (기본값)
result, _ := composer.ComposeWith()

...

이것은 단순히 보기 좋은 API가 아닙니다. 이는 해당 리포지토리 자체의 제품 테제 (product thesis)를 반영합니다: 코어를 작게 유지하고 조합을 명시적으로 만드는 것입니다.

중요한 부분은 해당 옵션들이 무엇을 의미하는가 하는 점입니다:

  • WithTypeFilter(...)는 어떤 아티팩트 클래스가 참여해야 하는지를 지정합니다.
  • WithTagFilter(...)는 이번 조합 단계에서 어떤 관심사 (concerns)가 중요한지를 지정합니다.
  • WithEvalFn(...)은 조합이 단순한 시작 시점의 설정이 아니라, 동적이고 상태를 인식 (state-aware)한다는 것을 의미합니다.
  • WithIncludeInactive()는 관측 가능성 (observability)을 일급 디버깅 모드로 전환합니다.

이것이 바로 제가 진지한 에이전트 인프라가 진화할 것이라고 기대하는 방식입니다.

하나의 거대한 Config 덩어리를 통해서가 아닙니다.

필요에 따라 결합할 수 있는 **작고 이름이 지정된 조합 결정 (small, named composition decisions)**을 통해서입니다.

여기에 턴별 평가 모델 (per-turn evaluation model)을 결합하면, 이 패턴은 더욱 강력해집니다. 옵션은 단순히 정적인 설정(static setup)뿐만 아니라, 하네스(harness)가 라이브 세션 상태(live session state)에 따라 동작을 어떻게 결정할지까지 제어할 수 있습니다.

실제 운영되는 에이전트 시스템에서의 모습

제가 권장하는 멘탈 모델(mental model)은 다음과 같습니다.

에이전트 런타임(agent runtime) 주변에 **직교하는 동작 (orthogonal behaviors)**을 구성해야 할 때 함수형 옵션(functional options)을 사용하세요.

관심사적절한 옵션 형태
도구 액세스 (Tool access)WithTool(...), WithTools(...)
...

이를 통해 세 가지 큰 이점을 얻을 수 있습니다.

1. 기본 에이전트의 가독성 유지

이는 사람들이 인정하는 것보다 훨씬 더 중요합니다. 단순한 케이스가 지저분하다면, 팀은 즉시 래퍼(wrapper)를 만들게 되고, 결국 유지보수해야 할 API가 두 개가 되어버립니다.

2. 고급 동작의 발견 가능성 유지

잘 명명된 WithApprovalPolicy(...)는 "6번 인자가 nil이 아닐 때만 8번 인자가 선택 사항입니다"라는 설명보다 훨씬 이해하기 쉽습니다.

3. 새로운 기능 추가 시 기존 코드의 파손 방지

이것이 Go 언어의 원래 동기였으며, 기능의 표면(capability surface)이 매 분기 계속 확장되는 에이전트 플랫폼에서는 더욱 중요합니다.

함수형 옵션이 잘못될 수 있는 경우

Four failure modes of functional options: hidden side effects, order-sensitive behavior, no validation layer, and config blob in disguise

주의해야 할 네 가지 안티 패턴(anti-patterns) — 옵션이 신중하게 설계되지 않으면 깨끗한 API라도 복잡성을 숨길 수 있습니다.

저는 이 패턴을 매우 좋아하지만, 이것이 마법은 아닙니다.

언급할 만한 몇 가지 실패 모드(failure modes)가 있습니다.

숨겨진 부작용 (Hidden side effects)

만약 WithMemoryStore(...)가 배경에서의 지속성(persistence), 텔레메트리(telemetry), 재시도(retries)를 조용히 활성화한다면, 해당 API는 정직함을 잃게 됩니다. 옵션은 구성적(compositional)이어야 하며, 예상치 못한 동작을 일으켜서는 안 됩니다.

순서에 민감한 동작 (Order-sensitive behavior)

WithTool(A) 다음에 WithTool(B)를 호출하는 것이 그 반대 순서와 다른 의미를 갖는다면, 이를 매우 강력하게 문서화해야 합니다. 더 좋은 방법은 결정론적 병합 규칙 (deterministic merge rules)을 중심으로 설계하는 것입니다.

검증 레이어의 부재 (No validation layer)

제가 go-containerregistry 버전을 좋아하는 이유 중 하나는 Option 타입이 에러를 반환하기 때문입니다. 이를 통해 라이브러리는 모순되는 인증 설정과 같은 잘못된 조합을 거부할 수 있는 깔끔한 방법을 갖게 됩니다. 에이전트 시스템 역시 호환되지 않는 메모리 백엔드 (memory backends), 상호 배타적인 승인 모드 (mutually exclusive approval modes), 또는 불가능한 재시도 설정 (retry settings)에 대해 동일한 규율이 필요합니다.

변장한 설정 블롭 (Config blob in disguise)

만약 모든 옵션이 단순히 하나의 거대한 비구조화된 구조체 (unstructured struct)에 값을 쓰는 방식이라면, 아키텍처를 개선하지 못한 채 가독성만 높였을 뿐일 수도 있습니다. 가장 좋은 옵션은 시스템 내에서 의미 있는 접점 (seams)을 노출하는 것입니다.

이것이 제가 단순한 필드 변이 (field mutation) 대신 실제 에이전트의 관심사 (concerns)를 나타내는 옵션을 선호하는 이유입니다.

더 중요한 점: 이것은 하네스 패턴 (Harness Pattern)입니다

저는 함수형 옵션 패턴이 단순히 AI를 위한 더 나은 생성자 트릭이라고 생각하지 않습니다.

저는 이것이 더 깊은 아이디어를 표현하는 가장 깔끔한 방법 중 하나라고 생각합니다: 에이전트의 동작은 애플리케이션의 접착제 (application glue)나 비대해진 프롬프트 (prompts)에 파묻히는 것이 아니라, 하네스 레이어 (harness layer)에서 구성되어야 합니다.

이는 제가 하네스 엔지니어링 (harness engineering)에 대해 주장해 온 모든 내용과 일치합니다:

  • 핵심을 작게 유지할 것
  • 동작을 명시적으로 만들 것
  • 거버넌스 (governance)가 깔끔하게 구성되도록 할 것
  • 런타임 결정을 검사 가능하게 (inspectable) 만들 것
  • 거대한 프롬프트/설정 블롭 (monolithic prompt/config blobs)을 피할 것

Go 개발자들은 API가 계속 커지면서 이 교훈을 배웠습니다. 에이전트 빌더들은 런타임이 계속 커짐에 따라 동일한 교훈을 배우게 될 것입니다.

이 분야에서 승리하는 팀은 가장 화려한 프롬프트를 가진 팀이 아니라, 가장 깔끔한 구성 모델 (composition model)을 가진 팀이 될 것입니다.

결론 (The Bottom Line)

함수형 옵션 패턴은 에이전트 API를 구축하는 더 깔끔한 방법을 제공하지만, 그것만으로는 이 패턴의 가치를 과소평가하는 것입니다.

이 패턴이 실제로 제공하는 것은 **에이전트 동작을 구성하기 위한 규율 (discipline for composing agent behavior)**입니다. 즉, 도구 (tools), 훅 (hooks), 메모리 (memory), 가드레일 (guardrails), 라우팅 (routing), 그리고 관찰 가능성 (observability)을 생성자의 혼돈이 아닌, 이름이 지정된 재사용 가능한 동작 (named, reusable moves)으로 제공하는 것입니다.

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0