AI 기능 배포 전날 밤, 아무도 말 안 해준 것들

AI 기능 배포 전날 밤, 아무도 말 안 해준 것들

설계 실수·API 키 노출·MCP 운영 함정—프로토타입이 프로덕션을 만나는 순간 터지는 세 가지 현실

AI 에이전트 Firebase AI Logic MCP Server 클라이언트 보안 API 키 보호 프로토타입 배포 App Check
광고

프로토타입이 동작하는 순간은 짜릿하다. 모델이 뭔가 인상적인 걸 해냈고, 당신은 천재가 된 기분이다. 그런데 배포 전날 밤 1시, 코드를 다시 들여다보면 전혀 다른 감정이 밀려온다. API 키가 클라이언트 번들 안에 평문으로 박혀 있고, 에이전트 루프는 어딘가 계속 틀어지고, MCP 서버는 튜토리얼에서 봤던 것과 전혀 다른 문제를 뱉어낸다. 빠르게 만드는 것과 실제로 운영하는 것 사이에는 아무도 친절하게 알려주지 않는 간극이 있다.

추상화부터 쌓으면 무너진다

dev.to에 올라온 AI 에이전트 실패 회고는 이 간극을 가장 정직하게 드러내는 사례 중 하나다. 필자는 채용 과제로 50문제짜리 벤치마크를 받았고, 플러그인 기반 툴링과 반자율 파이프라인이라는 깔끔한 아키텍처를 설계해 제출했다. 결과는 50점 만점에 3점. 더 강력한 Claude Opus 모델로 돌려도 2점으로 오히려 낮아졌다.

문제는 모델이 아니었다. 도메인을 이해하기 전에 제약과 추상화를 먼저 설계했다는 것이 문제였다. 인터뷰에서 깨달은 정답은 의외로 단순했다. 에이전트 루프 하나, Python 샌드박스 하나. 그게 전부였다. 이 구성으로 돌렸다면 80%를 맞힐 수 있었다. 프롬프트 엔지니어링도, 특수 툴도, 정교한 추상화도 필요 없었다. 이 사례가 던지는 교훈은 명확하다. 툴링·제약·추상화는 동작하는 베이스라인 위에서 신뢰성·비용·지연시간을 개선하기 위해 존재한다. 베이스라인 없이 추상화부터 쌓는 건 공중에 뼈대를 세우는 것과 같다.

API 키, 당신도 틀렸을 가능성이 높다

에이전트 설계 실수가 '무엇을 만들 것인가'의 문제라면, 클라이언트 사이드 AI 기능의 보안은 '어떻게 운영할 것인가'의 문제다. 그리고 여기서도 대부분의 개발자는 비슷한 덫에 빠진다.

dev.to의 Firebase AI Logic 관련 아티클에서 필자는 Next.js 15 프론트엔드와 Flask 백엔드로 AI 커리어 플랫폼을 구축하면서 겪은 경험을 공유한다. 실시간 모의 면접 기능에서 레이턴시를 낮추려면 Gemini 호출을 클라이언트 쪽으로 당겨야 했다. 그런데 클라이언트에서 API를 직접 호출하는 순간 키를 어딘가 노출해야 한다는 딜레마가 생긴다.

흔히 택하는 우회책들—환경변수에 키 넣기, 백엔드 프록시 경유, 단기 토큰 발급—은 각각 다른 방식으로 불완전하다. 환경변수는 번들러가 가끔 누출시킨다. 프록시는 레이턴시를 올리고 세션 상태 관리를 복잡하게 만든다. 단기 토큰은 그 자체가 하나의 별도 프로젝트 규모의 작업이다. 그리고 이 중 어느 것도 쿼터 드레인 공격을 막지 못한다. API 키를 잘 숨겨도 유효한 인증 토큰을 중간에 가로채 재사용하면 요금 청구서가 폭발한다.

구글 I/O에서 발표된 Firebase AI Logic의 업데이트가 주목받는 이유가 여기 있다. 템플릿 전용 모드는 시스템 프롬프트와 모델 설정을 클라이언트 번들이 아닌 Firebase 서버에 보관하고, 클라이언트는 템플릿 ID만 참조한다. 클라이언트 코드에서 프롬프트를 조작할 경로 자체가 없어진다. App Check 일회용 토큰은 한번 사용된 토큰을 즉시 무효화해 재사용 공격을 인프라 레벨에서 차단한다. 그리고 iOS·Android·Chrome에 걸친 하이브리드 추론은 경량 작업을 온디바이스로 처리해 네트워크가 불안정한 환경에서도 앱이 살아남을 수 있게 한다.

단, 트레이드오프는 직시해야 한다. 일회용 토큰은 요청마다 네트워크 라운드트립을 하나씩 추가한다. 실시간 전사처럼 몇 초 간격으로 Gemini를 호출하는 경로에서는 이 비용이 누적된다. 레이턴시에 민감한 경로를 먼저 프로파일링하고, 어디에 어떤 설정을 켤지 의식적으로 결정해야 한다. 두 옵션을 기본값으로 전부 켜두는 건 해결책이 아니다.

MCP 서버, 튜토리얼이 끝난 자리에서 시작되는 것

에이전트 아키텍처를 정리하고 보안 구조를 잡았다면, 다음 장벽은 실제 MCP 서버를 운영하면서 맞닥뜨리는 프로덕션 함정들이다. dev.to에 공개된 TypeScript MCP 서버 실전 가이드는 오디오 분리 API를 위한 오픈소스 MCP 서버를 3주간 직접 운영하며 발견한 패턴을 담고 있다.

튜토리얼은 대개 "SDK 연결하고, Zod 스키마로 툴 등록하고, 배포하라"는 세 줄로 끝난다. 실제로는 네 가지 문제가 그 뒤에 기다리고 있다.

첫째, 재시도는 요청의 성격에 따라 달라야 한다. GET 요청의 502 오류는 공격적으로 재시도해도 된다. 하지만 POST /jobs처럼 과금이 발생하는 변이 요청의 5xx는 서버가 이미 처리하고 응답만 유실된 경우일 수 있다. 같은 재시도 로직을 적용하면 사용자에게 이중 청구가 발생한다. mutating 플래그로 요청 성격을 분류하고, 429 응답의 Retry-After 헤더를 명시적으로 처리하는 withRetry 헬퍼가 이 구분을 코드 레벨에서 강제한다.

둘째, LLM이 전달하는 파일 경로는 반드시 앞단에서 검증해야 한다. LLM은 song.mp3, ./song.mp3, file:///Users/me/Music/song.mp3 같은 형식을 모두 건넨다. 검증 없이 Node.js가 이를 처리하면 MCP 서버의 프로세스 워킹 디렉터리 기준으로 경로를 해석해 ENOENT 오류가 난다. LLM은 왜 오류가 났는지 모르고 같은 경로로 재시도하거나 포기한다. 오류 메시지 자체가 LLM이 다음 행동을 결정할 수 있도록 설계되어야 한다. "invalid path"가 아니라 "절대 경로를 알 수 없다면 사용자에게 물어보라"는 구체적인 지시가 담겨야 한다.

셋째, 에러는 문자열이 아닌 머신 리더블 구조여야 한다. "크레딧 부족"과 "요청 한도 초과"와 "파일 크기 초과"는 LLM 입장에서 전혀 다른 복구 경로를 요구한다. 에러를 단순 문자열로 던지면 LLM은 이를 분류할 수 없다. AUTH_INVALID, INSUFFICIENT_CREDITS, RATE_LIMIT_EXCEEDED 같은 코드 필드를 가진 구조화된 에러 클래스가 LLM이 상황에 맞는 행동을 선택할 수 있게 한다.

세 가지 실패가 가리키는 하나의 패턴

에이전트 설계 실수, 클라이언트 보안 공백, MCP 운영 함정. 표면상 다른 세 문제지만 이 모두를 관통하는 공통 구조가 있다. '동작하는 것'과 '운영되는 것' 사이의 간극을 프로토타이핑 단계에서 보이지 않는 부채로 쌓아두는 것이다.

AI 기능을 앱에 붙이는 속도는 그 어느 때보다 빠르다. v0.dev로 UI를 뽑고, Claude로 로직을 짜고, Cursor로 연결하면 하루 만에 그럴듯한 데모가 나온다. 그 데모가 실제 사용자 트래픽을 받는 순간 터지는 것들은 모델 품질이나 프롬프트 설계가 아니다. 재시도 전략, 인증 토큰 생명주기, 에러 분류 체계, 클라이언트-서버 경계 설계 같은, 빠르게 만들던 시간 동안 미뤄둔 결정들이 한꺼번에 청구서로 날아온다.

프로토타입을 빠르게 만드는 것은 여전히 옳다. 하지만 그 속도 안에서 어떤 결정을 '의도적으로 미룬 것'인지와 '존재 자체를 몰랐던 것'인지를 구분하는 게 중요하다. 배포 전날 밤 1시에 발견하는 것들은 대부분 후자다. 그것들을 조금 더 일찍, 설계 단계에서 마주치는 것—그게 프로토타이핑 속도를 잃지 않으면서 프로덕션 품질을 확보하는 유일한 경로다.

출처

더 많은 AI 트렌드를 Seedora 앱에서 확인하세요