Reopt Agentic Governance 구현기: 진단에서 시스템까지

Case Study — 사례 연구

8개 AI 에이전트를 운영하는 프로덕션 SaaS에서 reopt architecture 원칙으로 거버넌스 공백을 진단하고, 범용 감사 로깅, 영속적 승인 기록, MCP 추적·속도 제한, 에이전트 제약, Admin 대시보드까지 구현한 과정.


배경 — 진단이 먼저, 구현은 그 다음

Reopt는 브랜드, 콘텐츠, 고객 운영을 하나의 워크스페이스에서 관리하는 B2B SaaS 플랫폼이다. Next.js 16, React 19, Turborepo 모노레포 위에 35개 패키지와 4개 앱이 올라가 있다. 8개의 AI 에이전트가 문서 생성, 브랜드 전략, 고객 분석, 페이지 빌딩, 워크플로 자동화를 수행한다.

reopt architecture의 8개 패턴으로 기존 시스템을 진단하자 네 가지 부채(Agentic Debt)가 선명하게 드러났다. 감사 로깅이 BrandDefinition에만 있어 도메인 간 일관성이 없고(Observability Gap), 승인 결정이 UI 상태에만 머물러 영속되지 않으며(Contract Gap), MCP 핸들러에 사용량 추적과 속도 제한이 없고(Authority Sprawl), 에이전트 실행에 구조적 제한이 없었다(Validation Gap).

이 글은 진단 결과를 바탕으로 거버넌스 시스템을 실제 구현한 과정을 다룬다.

Responsibility Partitioning — Agent Registry + Versioning

Reopt의 8개 에이전트는 각자의 책임 영역을 갖고 전역 레지스트리에 등록된다. 거버넌스 구현에서 추가된 것은 버전 관리와 실행 제약이다. 모든 에이전트가 semver 버전을 갖고, constraints 필드로 실행 범위를 제한한다.

type AgentDefinition = {
  id: string;
  displayName: string;
  description: string;
  version?: string;               // semver — "1.0.0"

  buildSystemPrompt: (ctx: AgentContext) => string | Promise<string>;
  createTools: (params: { context: AgentContext; dataStream: ... }) => ToolSet;
  activeToolNames: (ctx: AgentContext) => string[];

  defaultModel: string;
  allowedModels?: string[] | null;
  uiFeatures: AgentUIFeatures;

  constraints?: {
    maxToolCallsPerTurn?: number;   // 턴당 도구 호출 상한 (기본 10)
    maxTokensPerSession?: number;   // 세션당 토큰 상한
    maxSteps?: number;              // 최대 실행 단계 (기본 5)
  };
};

AgentDefinition — 버전과 실행 제약이 추가된 에이전트 계약

AgentContext에 agentId가 추가되어, 도구 실행부터 감사 로깅까지 에이전트 정체성이 전체 호출 체인을 관통한다. 라우트 핸들러 → 에이전트 선택 → 컨텍스트 생성 → 도구 팩토리 → 감사 로그로 이어지는 흐름에서 agentId가 끊기지 않는다.

Module Contract — Tool Registry 확장

Tool Registry가 15개에서 32개로 확장되면서, 메타데이터도 강화되었다. 기존의 입출력 선언에 더해 실패 모드, LLM 호출 여부, 시간당 속도 제한이 계약에 포함된다.

type ToolDefinition = {
  id: string;
  displayName: string;
  category: ToolCategory;
  action: ToolAction;
  needsApproval: boolean;
  parameters?: ToolParamDefinition[];
  outputFields?: ToolParamDefinition[];
  executeSummary?: string;
  detailUrlPattern?: string;

  // 거버넌스 구현에서 추가된 필드
  involvesLLM?: boolean;           // 내부적으로 Claude를 호출하는 도구
  rateLimitPerHour?: number;       // 도구별 시간당 호출 제한
  failureModes?: Array<{
    code: string;
    retryable: boolean;
    userMessage: string;
  }>;
};

ToolDefinition — 실패 모드, LLM 호출 여부, 속도 제한이 추가된 계약

involvesLLM 플래그는 비용 추적의 핵심이다. requestSuggestions, createDocument, createPostDraft 같은 도구는 내부적으로 Claude를 호출하므로 이중 과금이 발생한다. 이 플래그가 있으면 비용 대시보드에서 LLM-in-LLM 패턴을 식별할 수 있다.

failureModes는 이전에 코드 안에 분산되어 있던 거부 조건을 레지스트리로 끌어올린 것이다. reopt architecture의 판단 질문 '어떤 조건에서 거부하거나 실패해야 하는가?'에 대한 답이 이제 선언적으로 존재한다.

Human Approval — 영속적 승인 기록

기존에는 승인 결정이 UI 메시지 상태(approval-pending → approval-responded)에만 존재했다. 채팅이 끝나면 누가 무엇을 승인했는지 추적할 방법이 없었다. AiToolApprovalRecord 모델이 이 공백을 메운다.

model AiToolApprovalRecord {
  id          String   @id @default(cuid())
  workspaceId String
  chatId      String                // 어떤 대화에서
  messageId   String                // 어떤 메시지에서
  agentId     String                // 어떤 에이전트가
  toolId      String                // 어떤 도구를 요청했고
  toolArgs    Json                  // 어떤 인자로
  status      String   @default("pending")  // pending | approved | denied
  resolvedAt  DateTime?             // 언제 결정되었는지
  createdAt   DateTime @default(now())

  @@index([workspaceId, status])
  @@index([chatId])
}

AiToolApprovalRecord — 승인 결정의 영속적 기록

승인 플로에서 도구 호출이 감지되면 레코드가 pending으로 생성되고, 사용자 응답 시 approved 또는 denied로 갱신된다. 이로써 '지난 달 cx 에이전트의 승인 거절률은?', '어떤 도구가 가장 많이 거절되는가?'에 답할 수 있게 되었다.

Decision Traceability — 범용 감사 로깅

이전에는 BrandDefinitionChange 테이블에서만 변경 이력이 기록되었다. 고객 태그, 콘텐츠 퍼블리싱, CMS 속성 변경은 추적되지 않았다. AiToolAuditLog가 모든 데이터 변경 도구에 일관된 감사 로깅을 제공한다.

model AiToolAuditLog {
  id          String   @id @default(cuid())
  workspaceId String
  agentId     String                // 어떤 에이전트가
  toolId      String                // 어떤 도구로
  userId      String                // 어떤 사용자의 세션에서
  entityType  String                // 어떤 엔티티를
  entityId    String                // 어떤 ID의
  action      String                // create | update | delete
  before      Json?                 // 변경 전 상태
  after       Json?                 // 변경 후 상태
  approvalId  String?               // 승인 기록 참조
  durationMs  Int?                  // 실행 소요 시간
  createdAt   DateTime @default(now())

  @@index([workspaceId, createdAt(sort: Desc)])
  @@index([agentId, createdAt(sort: Desc)])
  @@index([entityType, entityId])
}

AiToolAuditLog — 에이전트·도구·엔티티를 관통하는 범용 감사 로그

// logToolAction — 트랜잭션 안팎 모두에서 동작하는 감사 헬퍼
export async function logToolAction(
  client: PrismaClient | PrismaTransaction,
  params: {
    workspaceId: string;
    agentId: string;
    toolId: string;
    userId: string;
    entityType: string;
    entityId: string;
    action: "create" | "update" | "delete";
    before?: unknown;
    after?: unknown;
    approvalId?: string;
    durationMs?: number;
  },
) {
  await client.aiToolAuditLog.create({ data: params });
}

logToolAction — Prisma 트랜잭션과 호환되는 감사 헬퍼

핵심 설계 결정 두 가지. 첫째, 워크스페이스에 FK를 걸지 않는다. 워크스페이스가 삭제되어도 감사 로그는 남아야 한다(retention-first). 둘째, 로깅 실패가 도구 실행을 중단시키지 않는다(fire-and-forget). 감사는 관찰이지 차단이 아니다.

현재 6개 데이터 변경 도구(updateCustomerTag, createCustomerTask, updatePostTags, updatePostProperties, savePostDraft, publishPost)에 적용되어 있다. before/after 스냅샷으로 정확히 무엇이 바뀌었는지, approvalId로 어떤 승인 아래 실행되었는지 추적 가능하다.

MCP Governance — 추적, 속도 제한, 감사

이전에 MCP 핸들러에는 사용량 추적, 속도 제한, 감사 로깅이 없었다. 외부 에이전트(Claude Code 등)의 행동이 완전한 블랙박스였다. 세 가지 계층으로 이 Observability Gap을 해소했다.

// 1. McpToolInvocation — 호출 기록
model McpToolInvocation {
  id          String   @id @default(cuid())
  workspaceId String
  clientId    String              // 어떤 클라이언트가
  userId      String
  toolName    String              // 어떤 도구를
  status      String              // success | error
  durationMs  Int?
  createdAt   DateTime @default(now())
  // args와 result는 민감 데이터 가능성으로 의도적 제외

  @@index([workspaceId, createdAt(sort: Desc)])
  @@index([clientId, createdAt(sort: Desc)])
}

McpToolInvocation — 외부 에이전트의 도구 호출 기록

// 2. MCP 속도 제한 — Redis 슬라이딩 윈도우
const READ_LIMIT  = 100;  // 분당 100회
const WRITE_LIMIT = 10;   // 분당 10회

// 읽기/쓰기를 구분하여 차등 제한
const WRITE_TOOLS = new Set(["reopt_eav_record_bulk_update"]);

// Redis 장애 시 fail-open — 속도 제한 장애가 도구를 차단하지 않음
// 429 응답에 Retry-After 헤더 포함

MCP Rate Limiting — 읽기/쓰기 차등 속도 제한

// 3. withAudit — 비차단 감사 래퍼
export function withAudit(handler: McpHandler): McpHandler {
  return async (params) => {
    const start = Date.now();
    try {
      const result = await handler(params);
      // fire-and-forget: 감사 로깅 실패가 응답을 지연시키지 않음
      void logMcpInvocation({ ...params, status: "success", durationMs: Date.now() - start });
      return result;
    } catch (err) {
      void logMcpInvocation({ ...params, status: "error", durationMs: Date.now() - start });
      throw err;
    }
  };
}

withAudit — 감사 실패가 도구 실행을 차단하지 않는 비차단 래퍼

세 계층의 합성 순서는 rate-limit → audit → handler다. 속도 제한을 먼저 걸어 과도한 호출을 차단하고, 통과한 호출에 감사를 적용한 뒤, 실제 핸들러를 실행한다. 각 계층은 독립적이며 한 계층의 장애가 다른 계층을 무너뜨리지 않는다.

Governance Dashboard — 운영 가시성

감사 로그와 MCP 추적 데이터가 쌓여도 볼 수 없으면 거버넌스가 아니다. Admin 앱에 두 개의 대시보드를 추가했다.

  • AI Audit 페이지 — 감사 로그와 승인 기록을 탭으로 분리. agentId, toolId, 상태, 날짜 필터링. before/after JSON 확장 뷰. 페이지당 50건.
  • MCP Usage 페이지 — 총 호출 수, 성공률, 평균 응답 시간 요약 카드. 도구별 호출 빈도 상위 20개 차트. 최근 호출 로그. 7/30/90일 범위 선택.

Agent Usage Statistics는 AiCreditLedger와 Chat 테이블에서 일별 롤업(세션 수, 토큰, 크레딧)을 집계한다. 에이전트별 비용을 추적할 수 있는 기반이다.

이 대시보드는 reopt architecture OCLS 루프의 SHARPEN 단계를 가능하게 한다. 운영 데이터로 경계를 보정하려면, 먼저 운영 데이터를 볼 수 있어야 한다.

남은 과제

거버넌스 시스템 구현으로 Observability Gap과 Contract Gap의 상당 부분이 해소되었다. 그러나 여전히 남은 과제가 있다.

  • 위험 등급 분류 없음 — 모든 승인 대상 도구가 동일한 수준의 승인을 요구한다. low/medium/high 매트릭스가 필요하다.
  • Cross-Agent Collaboration 없음 — 에이전트 간 핸드오프와 컨텍스트 전달이 불가능하다. Stage 3(Multi-Agent Collaboration)로의 전환이 남아 있다.
  • rationale 부재 — 감사 로그에 '무엇이 바뀌었는가'는 기록되지만 '왜 이 결정을 했는가'는 아직 기록되지 않는다.
  • 나머지 데이터 변경 도구 — 32개 등록 도구 중 6개에만 감사 로깅이 적용되어 있다. Canvas, Document 도메인의 CUD 도구로 확장이 필요하다.

reopt architecture의 진화 단계로 보면, Reopt는 Stage 2(Responsibility Split)에서 Stage 3(Multi-Agent Collaboration)로 넘어가는 전환점에 있다. 거버넌스 인프라가 깔렸으므로, 이제 에이전트 간 협업 규칙을 설계할 기반이 만들어진 셈이다.

결론 — 구조는 한 번에 완성되지 않는다

Reopt의 거버넌스 구현 과정은 두 단계로 나뉜다. 첫째, reopt architecture 패턴으로 기존 시스템을 진단하여 공백을 식별했다. 둘째, 식별된 공백을 인프라 수준에서 메웠다. 범용 감사 로그(AiToolAuditLog), 영속적 승인 기록(AiToolApprovalRecord), MCP 추적·속도 제한(McpToolInvocation + Redis rate limiter), 에이전트 제약(constraints), 관리 대시보드가 그 결과다.

핵심 설계 원칙은 세 가지였다. retention-first(로그는 원본 데이터보다 오래 산다), fire-and-forget(관찰이 실행을 차단하지 않는다), fail-open(인프라 장애가 기능을 무너뜨리지 않는다). 이 원칙들은 프로덕션 SaaS에서 거버넌스가 성능과 안정성을 희생하지 않아야 한다는 현실적 제약에서 나왔다.

reopt architecture의 OCLS 루프가 말하듯, 거버넌스는 한 번에 완성되는 것이 아니라 운영 데이터를 기반으로 반복 개선되는 것이다. 이번 구현은 첫 번째 루프를 완주한 것이다. SHARPEN 단계에서 수집되는 데이터가 다음 OWN 단계—위험 등급 분류, 에이전트 간 협업, rationale 기록—을 촉발할 것이다.

태그

reoptproductionauditapprovalMCPgovernanceguardrails

관련 패턴

  • Responsibility Partitioning결과를 소유할 주체와 책임 경계를 명확히 정의한다.
  • Module Contract실행 단위의 조건, 권한, 실패 경로를 계약으로 선언한다.
  • Context Routing각 주체에 필요한 정보만 전달되도록 정보 흐름을 설계한다.
  • Human Approval고비용·고위험·고영향 의사결정은 인간 승인 흐름 안에 둔다.
  • Decision Traceability판단 근거, 선택 사유, 협업 경로를 구조화된 로그로 남긴다.
  • Evaluation and Guardrails허용되는 판단과 위험한 판단을 평가 기준과 안전 규칙으로 구분한다.