본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 2장: 하네스 아키텍처 설계 패턴
2026년 3월 11일·AI / ML·

2장: 하네스 아키텍처 설계 패턴

래핑, 미들웨어, 사이드카, 파이프라인, 이벤트 기반 등 AI 시스템 하네스의 5가지 핵심 아키텍처 패턴과 적용 시나리오를 분석합니다.

21분855자10개 섹션
aitestingevaluationmlops
공유
harness-engineering2 / 10
12345678910
이전1장: 하네스 엔지니어링의 등장과 핵심 개념다음3장: AI 모델 래핑과 입출력 제어

건축에서 구조가 건물의 수명을 결정하듯, 하네스의 아키텍처는 AI 시스템의 확장성과 유지보수성을 결정합니다. 1장에서 하네스가 무엇인지를 정의했다면, 이번 장에서는 하네스를 어떤 구조로 설계할 것인지를 다룹니다. 프로덕션에서 검증된 5가지 아키텍처 패턴을 살펴보고, 각 패턴이 어떤 상황에 적합한지를 분석하겠습니다.

이 장에서 다루는 내용

  • 하네스 아키텍처를 결정하는 핵심 요소
  • 5가지 설계 패턴: 래핑, 미들웨어, 사이드카, 파이프라인, 이벤트 기반
  • 각 패턴의 장단점과 적용 시나리오
  • 패턴 간 조합 전략

아키텍처를 결정하는 3가지 질문

하네스 아키텍처를 선택하기 전에, 다음 세 가지 질문에 답해야 합니다.

  1. 결합도: 하네스가 모델과 얼마나 밀접하게 연결되어야 하는가?
  2. 확장성: 새로운 기능(도구, 가드레일, 평가)을 쉽게 추가할 수 있어야 하는가?
  3. 관측 가능성: 하네스의 각 단계를 독립적으로 모니터링하고 디버깅할 수 있어야 하는가?

이 질문에 대한 답에 따라 적합한 패턴이 달라집니다.

패턴 1: 래핑 패턴 (Wrapper Pattern)

가장 직관적인 패턴입니다. 모델 호출을 하나의 클래스나 함수로 감싸고, 전처리와 후처리를 같은 레이어에서 수행합니다.

wrapper_harness.py
python
from dataclasses import dataclass
from typing import Any
 
 
@dataclass
class HarnessConfig:
    model: str = "claude-sonnet-4-20250514"
    max_tokens: int = 4096
    temperature: float = 0.7
    max_retries: int = 3
 
 
class ModelWrapper:
    """래핑 패턴: 모델 호출을 단일 클래스로 감싸기"""
 
    def __init__(self, client: Any, config: HarnessConfig):
        self.client = client
        self.config = config
 
    def invoke(self, prompt: str, context: dict | None = None) -> str:
        # 전처리: 컨텍스트 주입 및 프롬프트 구성
        enriched_prompt = self._preprocess(prompt, context)
 
        # 모델 호출 (재시도 포함)
        raw_response = self._call_with_retry(enriched_prompt)
 
        # 후처리: 파싱, 검증, 변환
        return self._postprocess(raw_response)
 
    def _preprocess(self, prompt: str, context: dict | None) -> str:
        if context:
            context_str = "\n".join(
                f"{k}: {v}" for k, v in context.items()
            )
            return f"Context:\n{context_str}\n\nQuery: {prompt}"
        return prompt
 
    def _call_with_retry(self, prompt: str) -> str:
        for attempt in range(self.config.max_retries):
            try:
                response = self.client.messages.create(
                    model=self.config.model,
                    max_tokens=self.config.max_tokens,
                    messages=[{"role": "user", "content": prompt}],
                )
                return response.content[0].text
            except Exception as e:
                if attempt == self.config.max_retries - 1:
                    raise
                continue
 
    def _postprocess(self, response: str) -> str:
        # 출력 정제: 불필요한 공백 제거, 포맷 검증
        return response.strip()

장점

  • 구현이 단순하고 이해하기 쉽습니다
  • 단일 진입점으로 디버깅이 용이합니다
  • 프로토타이핑과 MVP에 적합합니다

단점

  • 기능이 추가될수록 래퍼 클래스가 비대해집니다
  • 전처리/후처리 로직을 독립적으로 테스트하기 어렵습니다
  • 다수의 모델이나 복잡한 워크플로우에는 부적합합니다
Tip

래핑 패턴은 "첫 번째 하네스"로 가장 적합합니다. 단일 모델, 단일 용도의 AI 기능을 빠르게 구현할 때 시작점으로 삼으세요. 복잡해지면 다른 패턴으로 전환하면 됩니다.

패턴 2: 미들웨어 패턴 (Middleware Pattern)

웹 프레임워크의 미들웨어와 동일한 개념입니다. 요청과 응답이 일련의 미들웨어 체인을 통과하며, 각 미들웨어가 독립적으로 특정 기능을 담당합니다.

middleware-harness.ts
typescript
type Context = {
  prompt: string
  response?: string
  metadata: Record<string, unknown>
}
 
type Middleware = (
  ctx: Context,
  next: () => Promise<void>
) => Promise<void>
 
class MiddlewareHarness {
  private middlewares: Middleware[] = []
 
  use(middleware: Middleware): this {
    this.middlewares.push(middleware)
    return this
  }
 
  async execute(prompt: string): Promise<string> {
    const ctx: Context = { prompt, metadata: {} }
 
    // 미들웨어 체인 구성 (역순으로 감싸기)
    let index = 0
    const dispatch = async (): Promise<void> => {
      if (index < this.middlewares.length) {
        const mw = this.middlewares[index++]
        await mw(ctx, dispatch)
      }
    }
 
    await dispatch()
    return ctx.response ?? ""
  }
}
 
// 미들웨어 정의
const loggingMiddleware: Middleware = async (ctx, next) => {
  const start = Date.now()
  console.log(`[REQUEST] ${ctx.prompt.slice(0, 50)}...`)
  await next()
  console.log(`[RESPONSE] ${Date.now() - start}ms`)
}
 
const guardrailMiddleware: Middleware = async (ctx, next) => {
  // 입력 검증
  if (ctx.prompt.includes("ignore previous instructions")) {
    ctx.response = "요청을 처리할 수 없습니다."
    return // next를 호출하지 않아 체인 중단
  }
  await next()
  // 출력 검증
  if (ctx.response && ctx.response.length > 10000) {
    ctx.response = ctx.response.slice(0, 10000)
  }
}
 
const modelMiddleware: Middleware = async (ctx, next) => {
  ctx.response = await callModel(ctx.prompt)
  await next()
}
 
// 조립
const harness = new MiddlewareHarness()
  .use(loggingMiddleware)
  .use(guardrailMiddleware)
  .use(modelMiddleware)

장점

  • 각 미들웨어를 독립적으로 개발하고 테스트할 수 있습니다
  • 미들웨어 순서 변경만으로 동작을 바꿀 수 있습니다
  • 플러그인 방식으로 기능을 추가하거나 제거할 수 있습니다

단점

  • 미들웨어 간의 실행 순서에 대한 이해가 필요합니다
  • 미들웨어가 많아지면 디버깅 시 추적이 복잡해질 수 있습니다
  • 미들웨어 간 상태 공유가 암묵적이라 결합이 생길 수 있습니다
Info

NVIDIA의 NeMo Guardrails가 미들웨어 패턴의 대표적인 구현체입니다. 사용자와 LLM 사이에 프로그래밍 가능한 가드레일 미들웨어 계층을 배치하여 입출력을 제어합니다.

패턴 3: 사이드카 패턴 (Sidecar Pattern)

사이드카 패턴(Sidecar Pattern)은 마이크로서비스 아키텍처에서 차용한 패턴입니다. 하네스 기능을 모델 서비스와 별도의 프로세스로 분리하되, 같은 네트워크 컨텍스트에서 실행합니다. 모델 서비스를 수정하지 않고도 하네스 기능을 추가할 수 있다는 것이 핵심 장점입니다.

sidecar_harness.py
python
from fastapi import FastAPI, Request
import httpx
 
app = FastAPI()
MODEL_SERVICE_URL = "http://localhost:8080"
 
 
@app.middleware("http")
async def harness_middleware(request: Request, call_next):
    """사이드카 하네스: 모델 서비스를 수정하지 않고 감싸기"""
 
    # 입력 단계: 요청 가로채기 및 전처리
    body = await request.json()
    enriched_body = await preprocess(body)
 
    # 모델 서비스로 전달
    async with httpx.AsyncClient() as client:
        response = await client.post(
            f"{MODEL_SERVICE_URL}/generate",
            json=enriched_body,
        )
 
    # 출력 단계: 응답 후처리 및 메트릭 수집
    result = response.json()
    processed = await postprocess(result)
    await collect_metrics(body, processed)
 
    return processed
 
 
async def preprocess(body: dict) -> dict:
    """입력 가드레일 및 컨텍스트 보강"""
    # 프롬프트 인젝션 검사
    # 컨텍스트 주입
    # 토큰 제한 확인
    return body
 
 
async def postprocess(result: dict) -> dict:
    """출력 검증 및 변환"""
    # PII 마스킹
    # 출력 스키마 검증
    # 품질 점수 계산
    return result
 
 
async def collect_metrics(request: dict, response: dict):
    """메트릭 수집 및 전송"""
    # 토큰 사용량, 지연시간, 비용 등
    pass

장점

  • 기존 모델 서비스를 전혀 수정하지 않아도 됩니다
  • 하네스와 모델 서비스를 독립적으로 배포하고 스케일링할 수 있습니다
  • 여러 모델 서비스에 동일한 하네스를 재사용할 수 있습니다

단점

  • 네트워크 호출이 추가되어 지연시간이 증가합니다
  • 운영 복잡도가 높아집니다 (별도 프로세스 관리)
  • 모델 서비스의 내부 상태에 접근할 수 없습니다

패턴 4: 파이프라인 패턴 (Pipeline Pattern)

파이프라인 패턴(Pipeline Pattern)은 데이터가 정의된 스테이지를 순차적으로 통과하는 구조입니다. 각 스테이지는 입력을 받아 변환하고 다음 스테이지로 전달합니다. ETL 파이프라인이나 컴파일러 파이프라인과 동일한 개념입니다.

pipeline_harness.py
python
from typing import Callable, TypeVar
from dataclasses import dataclass, field
 
T = TypeVar("T")
 
 
@dataclass
class PipelineContext:
    """파이프라인 전체에서 공유되는 컨텍스트"""
    raw_input: str
    prompt: str = ""
    model_output: str = ""
    parsed_output: dict = field(default_factory=dict)
    final_output: str = ""
    metadata: dict = field(default_factory=dict)
 
 
# 타입 안전한 스테이지 정의
Stage = Callable[[PipelineContext], PipelineContext]
 
 
def validate_input(ctx: PipelineContext) -> PipelineContext:
    """스테이지 1: 입력 유효성 검사"""
    if not ctx.raw_input.strip():
        raise ValueError("빈 입력은 허용되지 않습니다")
    if len(ctx.raw_input) > 100_000:
        raise ValueError("입력이 최대 길이를 초과합니다")
    return ctx
 
 
def enrich_context(ctx: PipelineContext) -> PipelineContext:
    """스테이지 2: 컨텍스트 보강"""
    # RAG 검색, 사용자 프로필 조회 등
    ctx.metadata["enriched"] = True
    return ctx
 
 
def compose_prompt(ctx: PipelineContext) -> PipelineContext:
    """스테이지 3: 최종 프롬프트 조립"""
    system_prompt = "당신은 도움이 되는 AI 어시스턴트입니다."
    ctx.prompt = f"{system_prompt}\n\n{ctx.raw_input}"
    return ctx
 
 
def call_model(ctx: PipelineContext) -> PipelineContext:
    """스테이지 4: 모델 호출"""
    # 실제로는 API 호출
    ctx.model_output = model.generate(ctx.prompt)
    return ctx
 
 
class Pipeline:
    def __init__(self):
        self.stages: list[Stage] = []
 
    def add_stage(self, stage: Stage) -> "Pipeline":
        self.stages.append(stage)
        return self
 
    def execute(self, raw_input: str) -> PipelineContext:
        ctx = PipelineContext(raw_input=raw_input)
        for stage in self.stages:
            ctx = stage(ctx)
        return ctx
 
 
# 파이프라인 조립
pipeline = (
    Pipeline()
    .add_stage(validate_input)
    .add_stage(enrich_context)
    .add_stage(compose_prompt)
    .add_stage(call_model)
)

장점

  • 각 스테이지가 명확한 책임을 가져 이해하기 쉽습니다
  • 스테이지 단위로 테스트, 교체, 재사용이 가능합니다
  • 데이터 흐름이 명확하여 디버깅이 용이합니다

단점

  • 순차 실행이 기본이라 병렬 처리에 추가 설계가 필요합니다
  • 스테이지 간 데이터 전달을 위한 컨텍스트 객체 설계가 중요합니다
  • 조건부 분기나 반복이 필요한 경우 복잡해질 수 있습니다

패턴 5: 이벤트 기반 패턴 (Event-Driven Pattern)

이벤트 기반 패턴(Event-Driven Pattern)은 하네스의 각 컴포넌트가 이벤트를 발행하고 구독하는 방식으로 통신합니다. 가장 유연하지만 가장 복잡한 패턴이기도 합니다.

event-driven-harness.ts
typescript
type EventType =
  | "request.received"
  | "input.validated"
  | "context.enriched"
  | "model.called"
  | "output.validated"
  | "response.ready"
  | "error.occurred"
 
interface HarnessEvent {
  type: EventType
  payload: unknown
  timestamp: number
  traceId: string
}
 
type EventHandler = (event: HarnessEvent) => Promise<void>
 
class EventDrivenHarness {
  private handlers = new Map<EventType, EventHandler[]>()
 
  on(eventType: EventType, handler: EventHandler): void {
    const existing = this.handlers.get(eventType) ?? []
    existing.push(handler)
    this.handlers.set(eventType, existing)
  }
 
  async emit(event: HarnessEvent): Promise<void> {
    const handlers = this.handlers.get(event.type) ?? []
    // 동일 이벤트의 핸들러들은 병렬 실행
    await Promise.all(handlers.map((h) => h(event)))
  }
}
 
// 핸들러 등록
const harness = new EventDrivenHarness()
 
harness.on("request.received", async (event) => {
  // 입력 검증 후 다음 이벤트 발행
  const validated = await validateInput(event.payload)
  await harness.emit({
    type: "input.validated",
    payload: validated,
    timestamp: Date.now(),
    traceId: event.traceId,
  })
})
 
harness.on("model.called", async (event) => {
  // 모델 응답 로깅 (비동기, 메인 흐름 차단 없음)
  await logToAnalytics(event)
})
 
harness.on("model.called", async (event) => {
  // 출력 검증 (같은 이벤트에 여러 핸들러)
  const validated = await validateOutput(event.payload)
  await harness.emit({
    type: "output.validated",
    payload: validated,
    timestamp: Date.now(),
    traceId: event.traceId,
  })
})

장점

  • 극도로 유연한 확장이 가능합니다 (핸들러 추가만으로 기능 확장)
  • 동일 이벤트에 대한 병렬 처리가 자연스럽습니다
  • 느슨한 결합으로 컴포넌트 간 의존성이 최소화됩니다

단점

  • 이벤트 흐름 추적이 어려워 디버깅 복잡도가 높습니다
  • 이벤트 순서 보장이 필요한 경우 추가 설계가 필요합니다
  • 작은 규모의 시스템에서는 과도한 설계가 될 수 있습니다
Info

CrewAI Flows가 이벤트 기반 패턴의 좋은 예시입니다. 에이전트 워크플로우를 이벤트 기반으로 구성하여, 각 단계의 결과에 따라 동적으로 다음 행동을 결정합니다.

패턴 비교와 선택 가이드

기준래핑미들웨어사이드카파이프라인이벤트 기반
구현 난이도낮음중간높음중간높음
확장성낮음높음높음중간매우 높음
테스트 용이성낮음높음중간높음중간
결합도높음중간낮음중간낮음
적합한 규모소규모중규모대규모중규모대규모
Warning

실전에서는 단일 패턴만 사용하는 경우가 드뭅니다. 대부분의 프로덕션 시스템은 파이프라인 패턴을 골격으로 하되, 각 스테이지 내부에서 미들웨어 패턴을 사용하고, 모니터링은 이벤트 기반으로 처리하는 식의 조합을 채택합니다.

핵심 요약

  • 래핑 패턴: 가장 단순하며, 프로토타이핑과 MVP에 적합합니다.
  • 미들웨어 패턴: 플러그인 방식의 확장이 필요할 때 적합하며, NeMo Guardrails가 대표적입니다.
  • 사이드카 패턴: 기존 모델 서비스를 수정하지 않고 하네스를 추가해야 할 때 사용합니다.
  • 파이프라인 패턴: 데이터 흐름이 명확한 순차적 처리에 적합하며, 가장 범용적입니다.
  • 이벤트 기반 패턴: 최대의 유연성이 필요하고 비동기 처리가 중요한 대규모 시스템에 적합합니다.
  • 실전에서는 여러 패턴의 조합이 일반적입니다.

다음 장 예고

3장에서는 하네스의 가장 기본적이면서도 핵심적인 기능인 AI 모델 래핑과 입출력 제어를 다룹니다. 모델 추상화 계층 설계, 프롬프트 구성과 컨텍스트 주입, 스키마 기반 출력 제어, 폴백 전략 등 실전 코드와 함께 살펴봅니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#ai#testing#evaluation#mlops

관련 글

AI / ML

3장: AI 모델 래핑과 입출력 제어

모델 추상화 계층 설계, 프롬프트 구성과 컨텍스트 주입, 스키마 기반 출력 제어, 폴백 전략 등 AI 모델의 입출력을 체계적으로 관리하는 방법을 다룹니다.

2026년 3월 13일·17분
AI / ML

1장: 하네스 엔지니어링의 등장과 핵심 개념

AI 에이전트에서 모델을 감싸는 모든 것, 하네스 엔지니어링의 정의와 등장 배경, 그리고 5가지 핵심 역할을 살펴봅니다.

2026년 3월 9일·17분
AI / ML

4장: 테스트 하네스 — AI 시스템의 품질 보증

비결정적 출력 테스트, 스냅샷 테스트, 속성 기반 테스트, 회귀 테스트, 에이전트 행동 테스트 등 AI 시스템 테스트의 핵심 기법을 다룹니다.

2026년 3월 15일·17분
이전 글1장: 하네스 엔지니어링의 등장과 핵심 개념
다음 글3장: AI 모델 래핑과 입출력 제어

댓글

목차

약 21분 남음
  • 이 장에서 다루는 내용
  • 아키텍처를 결정하는 3가지 질문
  • 패턴 1: 래핑 패턴 (Wrapper Pattern)
    • 장점
    • 단점
  • 패턴 2: 미들웨어 패턴 (Middleware Pattern)
    • 장점
    • 단점
  • 패턴 3: 사이드카 패턴 (Sidecar Pattern)
    • 장점
    • 단점
  • 패턴 4: 파이프라인 패턴 (Pipeline Pattern)
    • 장점
    • 단점
  • 패턴 5: 이벤트 기반 패턴 (Event-Driven Pattern)
    • 장점
    • 단점
  • 패턴 비교와 선택 가이드
  • 핵심 요약
  • 다음 장 예고