본문 바로가기
일반IT/AI

방어선 조립하기: 직렬 가드레일과 Control Plane

by gasbugs 2026. 9. 3.

직렬 가드레일은 하나의 요청이 여러 검사 관문을 정해진 순서로 통과하게 만드는 구조다. Control Plane은 그 관문을 직접 통과하는 길이 아니라, 어떤 정책과 Model을 어느 순서로 실행할지 배포하고 Version을 관리하는 제어 계층이다.

 

앞선 실습에서 NeMo Guardrails, Nova Lite 기반 Safety 분류와 Microsoft Presidio를 각각 확인했다. 이제 중요한 질문은 “도구를 몇 개 켰는가”가 아니라 “누가 순서를 정하고, 실패를 어떻게 합치며, 정책 변경을 어떻게 추적하는가”다. 이번 글에서는 합성 Testcase로 직렬 Data Plane을 만들고 작은 Control Plane 계약을 추가한다.

사용자 요청이 여러 보안 관문을 직렬로 통과하고 상단 Control Plane이 정책과 상태를 관리하는 모습

Data Plane과 Control Plane을 먼저 구분하자

Data Plane은 실제 사용자 요청과 검색 문서, 모델 응답이 흐르는 실행 경로다. Control Plane은 정책 Bundle, Prompt Version, Model 설정, Threshold와 배포 상태를 관리한다. Control Plane 장애가 발생했다고 검증되지 않은 새 정책을 즉시 내려받거나 모든 요청을 허용해서는 안 된다.

구분 맡는 일 다루지 말아야 할 것
Data Plane 요청 검사, RAG 정제, LLM·Tool 실행, 출력 검사 임의 정책 변경과 승인 없는 배포
Control Plane 정책 Version, 승인, 서명·Hash, 배포, Rollback 사용자 요청 원문과 최종 업무 권한
Application 인증, Tenant, 업무 권한, 최종 승인, 감사 LLM에게 접근 권한 판단 위임
NeMo Runtime Rail·Action·Retrieval·LLM 흐름 조정 사용자 최종 인가의 단독 판단

여기서 Control Plane은 NVIDIA 제품의 별도 필수 구성요소라는 뜻이 아니다. NeMo 공식 문서는 config.yml, Prompt, Colang Flow와 Runtime Orchestration을 설명한다. 중앙 정책 저장소와 승인·배포 절차는 이를 운영하기 위한 이 글의 권장 설계안이다.

권장 직렬 흐름

Browser
  ↓
Application: 인증·Tenant·요청 한도
  ↓
Nova Lite 분류: 명시적 Safety Category
  ↓
NeMo Input Rail: Self-check·Jailbreak·주제 정책
  ↓
Retrieval API: Tenant Filter와 Security Trimming
  ↓
NeMo Retrieval Rail: Presidio로 원문 Chunk 정제
  ↓
Dialog·Execution Rail: 대화 흐름과 Tool 호출 검증
  ↓
Main LLM
  ↓
Output Rail: Safety·PII·형식 검사
  ↓
Application: 최종 업무 권한·감사·응답

NVIDIA 공식 문서는 Input Rail이 Main LLM 전에 입력을 검사하고, Retrieval Rail이 검색된 문서와 Chunk를 Prompt에 넣기 전에 처리하며, Execution Rail이 Action과 Tool을 제어하고, Output Rail이 응답을 반환하기 전에 검사한다고 설명한다. 정확한 실행 순서는 선택한 Engine과 설정에 따라 달라질 수 있다.

 

직렬 구조의 장점은 앞단에서 차단하면 뒤의 비싼 호출을 생략하고, 어느 관문이 결정했는지 설명하기 쉽다는 점이다. 단점은 각 단계의 지연시간이 더해지고, 여러 정책이 같은 요청을 중복 차단하며, 앞단 장애가 전체 가용성에 영향을 줄 수 있다는 점이다.

실습 1: 공통 판정 계약 만들기

도구마다 결과 형식이 다르면 결합 코드가 예외를 허용으로 바꾸기 쉽다. 먼저 모든 Adapter가 같은 형태를 반환하도록 한다.

from dataclasses import dataclass, field
from typing import Literal

Decision = Literal["ALLOW", "BLOCK", "MODIFY", "ERROR"]

@dataclass
class GuardrailResult:
    decision: Decision
    stage: str
    rule_id: str
    policy_version: str
    sanitized_text: str | None = None
    labels: list[str] = field(default_factory=list)

reason에 사용자 원문이나 탐지된 개인정보를 복사하지 않는다. 운영 기록에는 Stage, Rule ID, Decision, Policy Version, Latency와 상관관계 ID만 남긴다.

실습 2: 직렬 Pipeline 조립하기

아래 코드는 실제 Provider 대신 동일한 Interface를 가진 비동기 Adapter를 연결하는 뼈대다. 각 함수 내부에 Nova, NeMo와 Presidio 호출을 넣는다.

import asyncio

class Blocked(Exception):
    def __init__(self, result: GuardrailResult):
        self.result = result

async def enforce(result: GuardrailResult, text: str) -> str:
    if result.decision in {"BLOCK", "ERROR"}:
        raise Blocked(result)
    if result.decision == "MODIFY":
        if result.sanitized_text is None:
            raise Blocked(GuardrailResult(
                "ERROR", result.stage, "missing_sanitized_text",
                result.policy_version,
            ))
        return result.sanitized_text
    return text

async def run_pipeline(user_text: str, tenant_id: str) -> str:
    text = await enforce(await nova_classify(user_text), user_text)
    text = await enforce(await nemo_input_check(text), text)

    chunks = await authorized_retrieve(
        query=text,
        tenant_id=tenant_id,
    )
    safe_chunks = await presidio_sanitize_chunks(chunks)

    tool_plan = await nemo_dialog_and_execution(text, safe_chunks)
    answer = await call_main_llm(text, safe_chunks, tool_plan)
    answer = await enforce(await nemo_output_check(answer), answer)
    return answer

이 예제는 ERROR를 기본 차단한다. 읽기 전용 FAQ처럼 가용성이 중요한 서비스는 RAG와 Tool을 제거한 제한 모드로 전환할 수 있지만, 민감정보나 쓰기 권한이 있는 Agent에서는 Fail-closed가 안전한 기본값이다.

실습 3: NeMo 설정의 순서를 눈에 보이게 만들기

NeMo config.yml에서는 활성 Rail Flow를 선언한다. 공식 설정 문서는 Input과 Output Flow를 순서대로 나열할 수 있고, 일부 환경에서는 병렬 실행도 지원한다고 설명한다.

rails:
  input:
    parallel: false
    flows:
      - self check input
      - check jailbreak
      - mask sensitive data on input

  retrieval:
    flows:
      - check retrieval sensitive data

  output:
    parallel: false
    flows:
      - self check output
      - check output sensitive data

tracing:
  enabled: true
  enable_content_capture: false

입력을 수정하는 Rail은 순서에 따라 다음 검사 결과가 달라진다. NVIDIA 공식 문서도 Input Rail Mutation을 병렬 실행하면 경쟁 조건으로 결과가 달라질 수 있어 이런 경우 직렬 실행을 사용하라고 경고한다. 독립적인 읽기 전용 분류기를 병렬화할 수는 있지만, 성능을 이유로 의미를 바꾸지 않는지 회귀 Test가 필요하다.

 

또한 NeMo Tracing의 Message Content Capture는 기본적으로 꺼져 있다. 운영에서 원문 Capture를 켜면 Prompt, 응답과 Tool 결과가 Trace에 복제될 수 있으므로 별도 승인·보존기간·접근통제 없이 활성화하지 않는다.

실습 4: 가장 작은 Control Plane 계약

Control Plane은 실행 코드가 임의의 최신 파일을 받게 하는 Download Server가 아니다. 승인된 Policy Bundle의 Version과 Hash를 제공하고, Data Plane은 시작 시 검증한 Bundle만 활성화한다.

bundle_id: llm-guardrails-prod
version: 2026.09.03-1
status: approved
minimum_runtime: "0.18"
default_failure_mode: fail_closed

stages:
  - id: nova-input-safety
    policy: policies/nova-input-v3.txt
    timeout_ms: 1200
  - id: nemo-input
    policy: configs/nemo/config.yml
    timeout_ms: 1800
  - id: presidio-retrieval
    policy: policies/pii-entities-v5.yml
    timeout_ms: 500

artifacts:
  manifest_sha256: "배포 과정에서 계산한 값"

Manifest에는 실제 Secret, API Key, 개인정보 Sample을 넣지 않는다. Bundle은 Git의 Pull Request나 승인 Workflow를 거쳐 Immutable Artifact로 만들고, Data Plane은 허용된 서명자·Hash·Runtime Version을 검증한다.

실습 5: 배포와 Rollback 상태 만들기

정책 배포는 코드 배포처럼 취급한다.

draft → reviewed → approved → canary → active
                     ↘ rejected
active → rollback_previous

Canary에서는 일부 합성 Test Traffic으로 정상 허용률, 공격 차단률, Timeout, P95 Latency와 단계별 비용을 비교한다. 기준을 통과하지 못하면 이전 Bundle로 되돌린다. “최신 정책이니 안전하다”는 가정 대신 이전 Version과 비교 가능한 증거를 남긴다.

변경 필수 검증
Safety Prompt 정상·공격 분류 회귀, 출력 형식
Presidio Recognizer False Positive·False Negative, 언어 변형
Rail 순서 Mutation 전후 결과, 후속 호출 발생 여부
Model 교체 정책 준수, Latency, 비용, Timeout
Tool Schema 허용 함수·인자·결과 검증
Failure Mode 장애 주입 시 차단 또는 제한 모드

관측해야 할 최소 신호

요청마다 다음 값을 구조화된 Log와 Trace Attribute로 남긴다.

  • request_id, 검증된 tenant_id의 비식별 내부 식별자
  • policy_bundle, policy_version, runtime_version
  • 단계별 decision, rule_id, latency_ms
  • 실행한 LLM·Retriever·Tool의 종류와 성공 여부
  • 최종 blocked_by와 제한 모드 여부

Prompt와 응답 원문, PII, Credential은 기본 Telemetry에서 제외한다. Metric Label에도 사용자 입력이나 고유 식별자를 넣지 않는다. 추적 가능성은 원문을 많이 저장하는 것이 아니라 같은 Request ID로 정책 Version과 실행 경로를 연결하는 데서 나온다.

직렬·병렬·혼합형 선택

방식 장점 위험 적합한 경우
완전 직렬 순서와 차단 이유가 명확 누적 Latency, 앞단 병목 Mutation, 민감 RAG, Tool Agent
완전 병렬 독립 검사 Latency 단축 가능 경쟁 조건, 중복 비용, 결합 복잡도 읽기 전용 독립 분류기
혼합형 의미 순서는 보존하며 일부 최적화 설계·관측 복잡도 운영 규모가 커진 서비스

권장 출발점은 직렬이다. 측정 결과가 쌓인 뒤 서로 입력을 수정하지 않는 독립 검사만 병렬화한다. 병렬 호출 중 하나가 먼저 허용했다고 Main Response를 곧바로 반환해서는 안 되며, 최종 결합 정책이 모든 필수 판정을 기다려야 한다.

실습 완료 체크리스트

  • Data Plane과 Control Plane의 책임을 구분했다.
  • 인증·Tenant·업무 권한은 Application과 Downstream에 남겼다.
  • 모든 Guardrail Adapter가 공통 판정 계약을 반환한다.
  • Rail의 실행 순서와 Short-circuit 규칙을 문서화했다.
  • 오류·Timeout·Unknown의 기본 동작을 정했다.
  • Policy Bundle에 Version, Hash, 승인 상태와 Runtime 조건을 기록했다.
  • Canary와 이전 Version Rollback 경로를 준비했다.
  • Prompt·PII·Credential을 기본 Telemetry에서 제외했다.
  • 정책·Model·순서 변경마다 합성 회귀 Test를 실행한다.
  • 차단 뒤 RAG·Tool·Main LLM 호출이 없는지 Trace로 확인한다.

마무리

직렬 가드레일은 여러 보안 제품을 한 줄로 붙이는 작업이 아니다. 각 단계의 입력과 출력, 수정 권한, 실패 정책과 최종 결정자를 명확히 만드는 일이다. Application은 인증·인가와 업무 제어를 맡고, NeMo는 LLM 처리 흐름을 조정하며, Nova와 Presidio 같은 검사는 정해진 경계에서 전문 판정을 제공한다.

 

Control Plane은 이 실행 경로 위에서 정책 Version, 승인, 배포와 Rollback을 관리한다. 먼저 단순한 직렬 경로로 정확성을 확보하고, 측정한 근거가 있을 때만 독립 검사를 병렬화하자. 다음 편에서는 Promptfoo로 알려진 공격과 정상 요청을 반복 실행해 정책 변경이 기존 방어선을 깨뜨리지 않는지 검증한다.

공식 자료

※ Control Plane Bundle과 승인·배포 State는 공식 제품 요구사항이 아니라 이 글의 권장 운영 설계안이다. NeMo API와 Configuration은 Version에 따라 달라질 수 있으므로 고정한 Release 문서와 합성 Testcase로 확인하자.