본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 30. 09:22

십 대 Discord 서버를 위한 AI 가드레일 (Guardrails): 모델 호출 주변의 코드

요약

미성년자가 포함된 Discord 서버에서 안전한 AI 어시스턴트를 구축하기 위한 가드레일 설계 전략을 다룹니다. 시스템 프롬프트에 의존하지 않고 로컬 규칙 기반의 사전 체크를 통해 보안 경계를 구축하는 실무적인 방법을 제안합니다.

핵심 포인트

  • AI 사용을 선택 사항(Opt-in)으로 제한하여 데이터 노출 최소화
  • 시스템 프롬프트 대신 결정론적 로컬 사전 체크(Pre-check) 활용
  • 비공개 DM 대신 공개 채널 답변을 통해 투명성 확보
  • 규칙 매칭 시 구체적인 규칙이 광범위한 규칙보다 먼저 실행되도록 설계

저는 제 13살 딸과 몇몇 친구들이 대화할 수 있는 AI 어시스턴트를 제공하는 Discord 봇을 만들었습니다. 모델 호출 (Model call)은 프로젝트 전체에서 가장 흥미가 떨어지는 한 줄입니다. 기록할 가치가 있는 모든 것은 그 주변을 감싸고 있는 코드입니다. 즉, AI가 어디에서 실행될 수 있는지, 무엇이 AI 실행 전에 작동하는지, 그리고 그 과정에서 발생한 몇 가지 문제들입니다.

이 글은 실무자용 버전입니다. 만약 여러분이 소규모 비공개 서버, 특히 미성년자가 포함된 서버를 위한 봇을 구축하고 있다면, 수치들을 익명화한 아키텍처 (Architecture)와 구체적인 실패 사례를 여기 공유합니다.

격리가 우선: 하나의 채널, 하나의 명령어

본능적으로는 봇이 모든 것에 응답하게 하고 싶을 것입니다. 그러지 마세요. 모든 메시지를 읽는 봇은 소란스럽고, 사용자 텍스트를 모델로 끊임없이 전송하며, 감사 (Audit)가 거의 불가능합니다. 저는 AI를 선택 사항 (Opt-in)으로 만들었습니다: 하나의 채널, 하나의 슬래시 명령어 (Slash command), 공개적인 답변입니다.

# AI는 선택 사항입니다: 하나의 채널, 하나의 명령어, 공개 답변
AI_ASK_ENABLED=true
AI_ASK_CHANNEL=ask-ai
...

슬래시 명령어 전용 방식은 의도가 명확하고, 채널이 조용하게 유지되며, 모든 상호작용이 한 곳에서 이루어짐을 의미합니다. 또한 메시지 내용을 긁어모으는 대신 Discord의 애플리케이션 명령어 모델 (Application command model)에 의존하게 됩니다. 답변은 의도적으로 채널 내에 공개됩니다. 에페머럴 답변 (Ephemeral replies, 일시적 답변)이나 DM (Direct Message)은 사용하지 않습니다. 왜냐하면 그것은 미성년자와의 숨겨진 AI 대화가 되며, 이는 제가 구축하면서 가장 피하고자 했던 것이기 때문입니다.

중요한 패턴: 모델 호출 전의 결정론적 체크 (Deterministic check)

시스템 프롬프트 (System prompt)는 보안 경계 (Security boundary)가 아닙니다. 그것은 소프트 레이어 (Soft layer)이며, 결심을 굳힌 프롬프트는 이를 우회하여 논쟁할 수 있습니다. 강력한 경계는 모델이 말을 통해 넘어설 수 없는 곳에 존재해야 합니다. 따라서 어떤 것이 모델에 도달하기 전에, 제 자체 서버에서 고정 규칙 기반의 사전 체크 (Pre-check)가 실행됩니다.

async function handleAsk(interaction, prompt) {
  const verdict = localPrecheck(prompt); // 고정 규칙, 로컬 실행, 모델 개입 없음

...

차단된 프롬프트는 짧은 공개 거부 메시지, 개인 채널로의 알림, 그리고 로그 이벤트 기록을 받습니다. 하지만 삭제, 타임아웃, 또는 모델로의 전달은 이루어지지 않습니다. 어떠한 경우에도 처벌은 없습니다. 봇은 플래그(flag)를 표시하고 사람이 결정합니다. 왜냐하면 모델은 비꼬는 말투(sarcasm)나 십 대들의 슬랭(slang)을 끊임없이 잘못 읽으며, 아이에 대한 오탐(false positive)은 쉽게 회복할 수 없는 신뢰를 잃게 만들기 때문입니다.

규칙 순서 버그 (The rule-order bug)

how do I steal someone's password라는 문구로 사전 검사(pre-check)를 테스트했습니다. 차단은 되었지만, 잘못된 규칙에 의해 차단되었습니다. 더 구체적인 규칙이 존재하든 아니든, 광범위한 패턴이 먼저 매칭되어 일반적인 거부 메시지를 반환했습니다.

// 잘못된 예: 광범위한 규칙이 구체적인 규칙을 가림
const RULES = [
  { category: "illegal_or_dangerous", test: p => /\bsteal\b/i.test(p) },     // 먼저 매칭됨
...

규칙 순서는 단순한 세부 사항이 아니라 로직의 일부입니다. steal과 같은 광범위한 토큰은 더 좁고 스마트한 규칙을 앞에 두기 전까지 프롬프트를 가로챕니다. 이는 라우트(route)나 방화벽 규칙을 설정할 때와 같은 함정입니다. 구체적인 것을 먼저, 광범위한 것을 마지막에 배치해야 합니다.

봇에 대한 최소 권한 원칙 (Least privilege on the bot)

봇은 일반적인 운영을 위해 관리자(Administrator) 권한을 보유하지 않습니다. 비공개 카테고리 덮어쓰기(private category overwrites)를 설정하던 중 50013 Missing Permissions 벽을 넘기 위해 잠시 한 번 권한을 부여한 후 다시 회수했습니다. 토큰이 유출되더라도 피해 범위(blast radius)가 아주 작기를 원하기 때문입니다. 초대 생성은 @everyone 및 멤버 역할에 대해 잠겨 있어, 초대가 스스로 퍼져나갈 수 없도록 했습니다.

이름이 아닌 ID로 길드(guild) 타겟팅하기

초기 헬퍼 스크립트들은 서버를 이름으로 찾았습니다. 그러다 아이들이 서버 이름을 바꾸자 모든 스크립트가 즉시 깨져버렸습니다.

// 취약함: 서버 이름이 바뀌는 순간 깨짐
const guild = client.guilds.cache.find(g => g.name === TARGET_GUILD_NAME);

...

이름은 사람을 위한 것입니다. 자동화는 ID를 유지해야 합니다.

Deprecation 경고 정리하기

discord.js에서 ephemeral: true가 플래그(flags)로 대체되어 지원 중단(deprecated)될 것이라는 경고를 보내기 시작했습니다. 핵심 동작이 안정화되면 한 번쯤 수행할 가치가 있는 쉬운 수정 사항입니다. 무해한 소음으로 가득 찬 로그는 결국 진짜 문제가 숨어드는 장소가 되기 때문입니다.

// deprecated (사용 중단됨)
await interaction.deferReply({ ephemeral: true });

...

패치 루프(patch loop)와 런타임(runtime)

봇은 systemd 서비스로 실행되므로 대화형 세션 없이도 재부팅 후에도 계속 유지됩니다. 전체 반복 루프(iteration loop)는 의도적으로 작게 구성했습니다: 백업, 패치, 구문 검사(syntax-check), 재시작, 로그 읽기, 한 가지 사항 테스트.

node --check logger-bot.js                 # 구문 오류(syntax error) 발생 시 절대 재시작하지 않음
sudo systemctl restart family-discord-logger
journalctl -u family-discord-logger -n 50 --no-pager

상태(State)는 데이터베이스가 아닌 일반 JSON 파일로 관리합니다. 서버 규모가 작고, 제가 직접 파일을 열어서 읽고 싶기 때문입니다. 일일 보고서는 서버 장비 내에서 생성되는 로컬 HTML 대시보드입니다. 봇이 이를 Discord에 업로드하지는 않으며, 제가 원할 때 secure copy(scp)로 가져옵니다. 가족용 서버로서는 분명 과한 설정(overkill)이지만, 덕분에 검토를 실제로 수행하게 됩니다.

코드가 아닌 한 가지: 공개(disclosure)

다른 사람의 아이들이 가득한 공유 공간을 대상으로 하는 로깅(logging) 설정은, 그 공간에 있는 사람들이 해당 설정의 존재를 알고 있을 때만 정당화될 수 있습니다. 따라서 공개(disclosure) 절차를 서버에 내장했습니다. 한 채널에서는 아이들에게 AI가 틀릴 수 있다는 점, 답변이 공개된다는 점, 개인 정보를 공유하지 말 것, 그리고 관리자가 활동을 검토할 수 있다는 점을 알립니다. 다른 채널에서는 이 모든 시스템이 어떻게 구축되었는지 설명합니다. 만약 당신이 방 안에 있는 사람들에게 당신의 시스템이 무엇을 기록하는지 편안하게 말할 수 없다면, 그것은 문서(docs)의 공백이 아니라 설계상의 악취(design smell)입니다.

솔직한 트레이드오프(tradeoff)

모델은 로컬이 아닌 네트워크를 통해 접근하는 클라우드 호스팅(cloud-hosted) 방식입니다. 제공업체는 프롬프트(prompt)가 저장되거나 학습에 사용되지 않으며, 오직 요청을 처리하기 위해서만 처리된다고 밝히고 있습니다. 저는 어쨌든 모델에 도달하는 데이터의 양을 줄이는 방향으로 설계했습니다: 단일 채널 사용, 사전 검사(pre-check), 사용자에게 명시적 경고 제공, 그리고 차단된 프롬프트는 절대 서버를 떠나지 않는다는 규칙입니다. 이는 노출(exposure)을 줄여줍니다. 그렇다고 이것이 로컬 전용(local-only) 방식과 동일해지는 것은 아니며, 저는 그런 척하지 않을 것입니다.

이 아키텍처는 의도적으로 지루하게 설계되었습니다. 모델은 답변할 수 있습니다. 제가 계속해서 던졌던 질문은, 모델이 여기서, 이런 방식으로, 이 정도의 가시성과 이 정도의 권위를 가지고 답변해도 되는가 하는 점이었습니다. 십 대들로 가득 찬 서버라면, 지루함이야말로 핵심입니다.

AI 자동 생성 콘텐츠

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

원문 바로가기
1

댓글

0