당신의 AI 프로토타입은 운영 시스템이 아니다: 신뢰할 수 있는 AI 애플리케이션의 엔지니어링
요약
AI 애플리케이션을 프로토타입 단계에서 실제 운영 시스템으로 전환하는 엔지니어링 관행의 중요성을 강조합니다. 모델 자체보다 전체 시스템 아키텍처, 배포, 확장성, 관측 가능성이 핵심 과제입니다.
핵심 포인트
- AI 모델은 시스템의 하나의 구성 요소일 뿐이다.
- 프로덕션 AI는 배포, 진단, 보안, 확장이 필수적이다.
- 컨테이너화는 단순 패키징을 넘어 재현성을 보장한다.
- 시스템은 시간 초과, 중복 요청 등 운영 문제를 처리해야 한다.
당신의 AI 프로토타입은 운영 시스템이 아니다: 신뢰할 수 있는 AI 애플리케이션의 엔지니어링
컨테이너화 및 배포 파이프라인부터 관측 가능성(observability), 장애 복구, 확장성에 이르기까지 — 실제 세계에서 AI 소프트웨어를 운영하는 데 실제로 필요한 것들.
Ahmed Adawy 작성
AI 애플리케이션은 시연 중에는 인상적으로 보일 수 있지만, 여전히 프로덕션 준비 상태와는 거리가 멀 수 있습니다.
모델이 유용한 답변을 반환합니다. API가 올바르게 응답합니다. 검색(retrieval) 파이프라인이 관련 문서를 찾습니다. 한 개발자가 애플리케이션을 로컬에서 실행할 때는 모든 것이 작동하는 것처럼 보입니다.
하지만 환경이 변하면 상황은 달라집니다.
모델 제공업체(model provider)가 느려질 수 있습니다. 여러 요청이 동시에 도착합니다. 배포 과정에서 호환되지 않는 종속성(dependency)이 도입될 수 있습니다. 데이터베이스가 연결 한계에 도달할 수 있습니다. 백그라운드 작업이 문서를 부분적으로 처리한 후 실패할 수 있습니다. 애플리케이션 재시작은 영구 저장되지 않은 작업을 중단시킬 수 있습니다.
이러한 문제들이 반드시 근본적인 AI 모델의 결함을 의미하는 것은 아닙니다.
이것들은 더 근본적인 무언가를 드러냅니다: 즉, 모델은 소프트웨어 시스템의 단지 하나의 구성 요소일 뿐이라는 것입니다.
엔지니어링 과제는 전체 시스템을 배포(deploy), 진단(diagnose), 보안(secure), 확장(scale), 유지보수(maintain)할 수 있을 만큼 예측 가능하게 만드는 것입니다.
이러한 구분이 바로 프로덕션 AI 엔지니어링의 핵심 아이디어입니다.
제 책, **The AI Engineer — Volume II: Deploy. Scale. Operate.**는 AI 애플리케이션이 프로토타입 단계를 넘어설 때 필수적이 되는 엔지니어링 관행에 초점을 맞춥니다.
1. 실제 아키텍처는 모델보다 크다
파일을 받아 텍스트를 추출하고, 임베딩(embeddings)을 생성하며, 관련 정보를 검색하고, 선택된 컨텍스트를 LLM에 보내는 문서 지능 서비스(document intelligence service)를 생각해 봅시다.
눈에 보이는 기능은 다음과 같이 요약될 수 있습니다:
Document → Embeddings → Retrieval → LLM → Answer
하지만 배포 가능한 애플리케이션은 훨씬 더 큰 실행 경로를 가집니다:
Client Request
|
v
...
모든 외부 의존성은 잠재적인 실패 경계(failure boundary)를 도입합니다. 추가적인 처리 단계는 지연 시간(latency)을 발생시킬 수 있습니다. 저장된 모든 문서는 접근 제어, 보존, 데이터 무결성(data integrity)에 대한 질문을 야기합니다.
따라서 시스템은 성공적인 모델 호출만으로는 답변할 수 없는 질문들에 답해야 합니다:
- 상위 서비스가 시간 초과(timeout)되면 어떻게 되나요?
- 중복 요청은 어떻게 처리되나요?
- 실패한 작업을 재시도하는 책임 컴포넌트는 무엇인가요?
- 배포가 정상적인지 어떻게 알 수 있나요?
- 요청의 가장 느린 단계를 식별할 수 있나요?
- 재시작으로 중단된 작업은 어떻게 되나요?
- 로그에서 민감한 정보가 노출되는 것을 어떻게 방지하나요?
이것들은 단순한 운영 세부 사항(operational details)이 아니라 아키텍처적인 질문들입니다.
2. 컨테이너화는 단순히 패키징을 넘어 재현성(Reproducibility)에 관한 것이다
Python 애플리케이션은 소스 파일 이상의 것에 의존합니다. 그 동작은 Python 런타임, 패키지 버전, 운영체제 라이브러리, 환경 변수, 파일 시스템 가정, 그리고 시작 구성(startup configuration)에 따라 달라질 수 있습니다.
Docker는 애플리케이션과 그 의존성을 컨테이너 이미지로 패키징하는 방법을 제공합니다. 이 이미지는 개발, 테스트, 배포 환경을 거칠 수 있는 버전이 지정된 아티팩트(artifact)가 됩니다.
최소한의 FastAPI 예제가 이 아이디어를 보여줍니다:
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1
...
이것은 애플리케이션 진입점(entry point)이 app/main.py이고, FastAPI 인스턴스의 이름이 app이며, 의존성 파일에 필요한 패키지가 포함되어 있다고 가정합니다.
빌드하고 실행해 보세요:
docker build -t ai-service .
docker run --rm -p 8000:8000 ai-service
이 명령어들은 간단합니다. 하지만 이들을 둘러싼 엔지니어링 결정들이 더 중요합니다.
예를 들어, 프로덕션 이미지는 제어된 종속성 버전(controlled dependency versions)을 사용하고, 비밀 정보(secrets)를 이미지에 복사하는 것을 피하며, .dockerignore를 통해 관련 없는 파일을 제외하고, 비-루트 사용자(non-root user)로 실행하는 것을 고려해야 합니다. 빌드 프로세스는 또한 취약점 스캔(vulnerability scanning)과 베이스 이미지 유지 관리도 고려해야 합니다.
컨테이너화가 소프트웨어를 자동으로 안전하거나 재현 가능하게 만들지는 않습니다. 이는 팀에게 일관된 단위(consistent unit)를 제공하여 구축, 검사, 테스트, 배포 및 배치를 할 수 있게 해줍니다.
이러한 일관성이 가치 있는 부분입니다.
3. CI/CD는 애플리케이션을 배포하는 것을 넘어 검증해야 한다
배포 파이프라인(deployment pipeline)은 종종 자동화 단계의 순서로 설명됩니다:
Commit
↓
Tests
...
이는 유용한 출발점이지만, 성공적인 배포 명령어 자체가 애플리케이션이 건강하다는 증거는 아닙니다.
더 강력한 파이프라인은 여러 관심사(concerns)를 분리합니다:
- 코드 품질: 정적 검사 및 포맷팅 규칙.
- 행동: 단위 테스트(unit test) 및 통합 테스트(integration test).
- 아티팩트 무결성: 컨테이너 이미지 빌드 및 검사.
- 보안: 적절한 경우 종속성과 이미지를 스캔합니다.
- 배포: 알려진 아티팩트를 대상 환경에 출시합니다.
- 검증: 애플리케이션 상태와 중요한 행동을 확인합니다.
- 복구: 검증 실패 시 발생할 상황을 정의합니다.
AI 애플리케이션은 테스트 주위에 특별한 주의가 필요합니다.
모든 단언(assertion)마다 라이브 LLM을 호출하는 테스트 스위트는 느리고, 비용이 많이 들며, 비결정적(nondeterministic)이 될 수 있습니다. 대신 많은 애플리케이션 행동은 제어된 가짜 클라이언트(controlled fake clients)를 사용하여 테스트할 수 있습니다.
예를 들어, 서비스는 외부 모델 제공업체와 독립적으로 테스트 가능해야 합니다:
from typing import Protocol
class TextGenerator(Protocol):
...
이 예시는 네트워크 요청을 하거나 모델 토큰을 소비하지 않으면서 애플리케이션 로직을 테스트합니다.
이는 실제 제공업체의 행동을 검증하는 것은 아니므로, 여전히 제공업체 통합 테스트가 필요합니다. 핵심은 적절한 수준에서 각 경계(boundary)를 테스트하는 것입니다.
신뢰할 수 있는 파이프라인은 빠르고 결정론적인 테스트(deterministic tests)와 목표 지향적인 통합 테스트(integration tests), 배포 검사(deployment checks)를 결합합니다.
4. 전체 요청에 걸쳐 레이턴시 측정 필수
AI 엔드포인트가 응답을 반환하는 데 2.4초가 걸린다고 가정해 봅시다.
이 수치만으로는 무엇을 최적화해야 하는지 알 수 없습니다.
요청은 입력 유효성 검사(validating input), 데이터베이스 조회(querying a database), 임베딩 생성(generating an embedding), 문서 검색(retrieving documents), LLM 대기, 결과 직렬화(serializing the result) 등의 시간을 소비할 수 있습니다.
단순화된 레이턴시 모델은 다음과 같습니다:
$$
T_{\text{total}} =
T_{\text{validation}} +
T_{\text{database}} +
T_{\text{embedding}} +
T_{\text{retrieval}} +
T_{\text{LLM}} +
T_{\text{response}} $$
이것은 근사치입니다. 병렬 작업(Parallel operations), 큐잉(queueing), 재시도(retries), 그리고 중첩되는 작업(overlapping work)은 실제 요청 타이밍을 더 복잡하게 만들 수 있습니다.
그럼에도 불구하고, 이 모델은 중요한 규칙을 세우는 데 도움을 줍니다. 즉, 시스템을 최적화하기 전에 개별 단계를 측정해야 한다는 것입니다.
만약 LLM이 요청 시간의 대부분을 차지한다면, 작은 Python 함수를 최적화하는 것이 사용자 경험에 거의 영향을 미치지 못할 수 있습니다.
데이터베이스 연결 획득(database connection acquisition)이 지연 시간을 지배한다면, 애플리케이션 워커(application workers)를 더 추가하는 것이 데이터베이스에 가해지는 압력을 증가시켜 문제를 악화시킬 수 있습니다.
임베딩 생성이 동일한 입력에 대해 반복된다면, 캐싱(caching)이 도움이 될 수 있습니다. 단, 캐시 키(cache key), 모델 버전(model version), 그리고 무효화 동작(invalidation behavior)이 올바르게 설계되었다는 전제가 필요합니다.
성능 엔지니어링은 가설, 측정, 통제된 변화에서 시작됩니다. 애플리케이션이 느리게 느껴진다는 이유만으로 단순히 인프라를 추가하는 것에서 시작해서는 안 됩니다.
5. 재시도는 실패로부터 회복할 수도 — 혹은 증폭시킬 수도
네트워크 호출은 실패합니다. 제공업체(Providers)는 요청을 제한(throttle)합니다. 일시적인 오류가 발생합니다.
재시도(Retries)는 도움이 될 수 있지만, 자동으로 안전한 것은 아닙니다.
초당 100개의 요청을 받는 서비스를 상상해 보십시오. 만약 모든 요청이 실패하고 각 요청이 세 번 재시도한다면, 업스트림 종속성(upstream dependency)은 이미 어려움을 겪고 있는 바로 그 순간에 원래 트래픽보다 몇 배 더 많은 트래픽을 받을 수 있습니다.
기본적인 지수 백오프(exponential backoff) 전략은 시도 간의 지연 시간을 늘립니다:
$$
d_n = \min(d_{\max}, d_0 \cdot 2^n)
$$
여기서 $(d_0)$는 초기 지연 시간, $(n)$은 재시도 인덱스이며, $(d_{\max})$는 최대 지연 시간입니다.
실제로는 많은 클라이언트로부터 동기화된 재시도(synchronized retries)를 줄이기 위해 무작위 지터(randomized jitter)가 자주 추가됩니다.
재시도는 또한 전체 마감 시간(overall deadline)으로 제한되어야 합니다. 애플리케이션은 일시적인 실패(transient failures)와 영구적인 오류(permanent errors)를 구별해야 하며, 제공업체의 속도 제한(rate limits)을 준수하고, 안전한 중복 전략 없이 비멱등적(non-idempotent) 작업을 재시도하는 것을 피해야 합니다.
레코드를 생성하거나, 돈을 청구하거나, 작업을 대기열에 넣거나, 외부 부작용(external side effects)을 유발하는 작업의 경우 멱등성(idempotency)이 특히 중요할 수 있습니다.
올바른 질문은 단순히 "재시도해야 할까요?"가 아닙니다.
그것은 다음과 같습니다: "어떤 실패는 재시도가 가능한지, 허용되는 시도는 몇 번인지, 전체 시간 예산은 얼마이며, 무엇이 재시도를 통해 작업의 중복을 막아주는가?"
이것이 바로 프로덕션 엔지니어링(production engineering)입니다.
6. 관측 가능성(Observability)은 실제로 무슨 일이 일어났는지 알아내는 방법입니다
Request failed라는 로그 메시지만으로는 분산 애플리케이션을 진단하기에 거의 충분하지 않습니다.
유용한 관측 가능성은 세 가지 상호 보완적인 신호를 결합합니다.
**로그(Logs)**는 이벤트 수준의 컨텍스트를 제공합니다: 어떤 작업이 실패했는지, 어떤 유형의 오류가 발생했는지, 그리고 어떤 요청이 관련되었는지 등을 알려줍니다.
**메트릭(Metrics)**은 추세를 보여줍니다: 요청률, 오류율, 지연 시간 백분위수(latency percentiles), 리소스 사용량, 대기열 깊이(queue depth), 종속성 실패 등이 포함됩니다.
**추적(Traces)**은 요청을 여러 구성 요소(components)를 통해 따라가고 시간이 어디에 쓰였는지 식별하는 데 도움을 줍니다.
AI 서비스의 경우, 추가적인 측정 항목에는 다음이 포함될 수 있습니다:
- 모델 제공업체 지연 시간 및 타임아웃율 (Model-provider latency and timeout rate).
- 입력 및 출력 토큰 개수 (Input and output token counts).
- 임베딩 생성 지속 시간 (Embedding generation duration).
- 검색 지속 시간 및 결과 개수 (Retrieval duration and result counts).
- 재시도 빈도 및 속도 제한 응답 (Retry frequency and rate-limit responses).
- 백그라운드 작업 완료 및 실패율 (Background-job completion and failure rates).
- 신뢰할 수 있는 사용량 및 가격 책정 데이터가 있는 경우 요청당 예상 비용 (Estimated cost per request).
이러한 측정 항목들은 서로 다른 실패 모드를 구별하는 데 도움을 줍니다.
정상적인 데이터베이스 지연 시간에도 불구하고 응답 시간이 높아지는 것은 모델 제공업체(model-provider)의 문제일 수 있습니다. 대기열 깊이(queue depth)가 증가하는 것은 백그라운드 워커(background workers)가 들어오는 작업량을 따라가지 못하고 있음을 나타낼 수 있습니다. 배포 후 오류율이 증가하는 것은 설정 또는 종속성 변경을 시사할 수 있습니다.
관측 가능성(Observability)은 모든 실패를 예방하지는 않습니다. 하지만 실패를 이해하고 대응하는 데 필요한 시간을 줄여줍니다.
한 가지 주의할 점이 필수적입니다: 운영 가시성이 통제되지 않은 데이터 수집이 되어서는 안 됩니다. 정당하고 신중하게 관리되는 이유 없이는 자격 증명(credentials), 액세스 토큰(access tokens), 전체 개인 문서, 또는 민감한 프롬프트(prompts)를 로깅하는 것을 피해야 합니다.
7. Kubernetes는 도구이지, 운영 전략이 아니다
Kubernetes는 컨테이너화된 워크로드(containerized workloads)를 관리하고, 서비스를 스케줄링하며, 상태 기반 재시작을 지원하고, 확장 및 서비스 검색 메커니즘을 제공할 수 있습니다.
하지만 Kubernetes를 채택한다고 해서 애플리케이션이 자동으로 안정적이 되는 것은 아닙니다.
정교한 클러스터 내부에서 실행되는 서비스라도 여전히 건강 검사(health checks)가 미흡하거나, 안전하지 않은 데이터베이스 마이그레이션(database migrations), 알림 누락, 약한 접근 제어(access controls), 또는 테스트된 복구 절차(tested recovery procedure)가 없을 수 있습니다.
운영 복잡성 또한 중요합니다. 팀은 리소스 요청 및 제한(resource requests and limits), 배포 구성(deployment configuration), 서비스 네트워킹(service networking), 시크릿(secrets), 스토리지(storage), 관측 가능성(observability)에 대한 이해와 확장(scaling)의 결과에 대해 알아야 합니다.
작은 애플리케이션의 경우, 관리형 애플리케이션 플랫폼(managed application platform)이 더 합리적인 선택일 수 있습니다. 여러 워크로드, 복잡한 스케줄링 요구 사항, 확립된 운영 관행, 그리고 오케스트레이션(orchestration)에 대한 명확한 요구 사항을 가진 시스템의 경우 Kubernetes가 적절할 수 있습니다.
결정은 문제에 따라 이루어져야 합니다.
유용한 엔지니어링 원칙은 애플리케이션의 실제 안정성, 보안 및 확장 요구 사항을 충족하는 가장 단순한 인프라를 채택하고—이러한 요구 사항이 변경될 때 결정을 재검토하는 것입니다.
8. 확장은 병목 현상과 트레이드오프에 관한 것이다
애플리케이션 인스턴스를 추가한다고 해서, 해당 애플리케이션과 그 의존성들이 그 용량을 효과적으로 사용할 수 있을 때만 가용 컴퓨팅 용량이 증가합니다.
상태 비저장(stateless) API는 비교적 적은 어려움으로 수평 확장(scale horizontally)할 수 있습니다. 하지만 연결 풀(connection pool)이 제한된 데이터베이스는 그렇지 않을 수 있습니다. 외부 LLM 제공업체는 요청 또는 토큰 제한을 강제할 수 있으며, 벡터 데이터베이스가 지연 시간(latency)의 주요 원인이 될 수도 있습니다.
따라서 확장 결정은 전체 요청 경로를 고려해야 합니다.
일반적인 기법에는 다음이 포함됩니다:
- 반복되는 유효한 계산을 캐싱(Caching).
- 장시간 실행되는 작업을 백그라운드 워커(background workers)로 이동시키기.
- 데이터베이스 인덱스 및 쿼리 플랜 개선하기.
- 연결 재사용 안전하게 하기.
- 비용이 많이 드는 의존성에 동시성 제한(concurrency limits) 적용하기.
- 워크로드에서 지원하는 경우 작업을 배치(Batching) 처리하기.
- 불필요한 페이로드 크기 줄이기.
- 대표적인 부하 조건 하에서 리소스 사용량 및 큐잉 측정하기.
각 기법에는 비용이 따릅니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기