LLM 통합이 무너지는 진짜 이유는 프롬프트가 아니라 계약의 부재입니다
백엔드에서 LLM 응답을 받아 파싱하고 저장하는 파이프라인을 운영해본 엔지니어라면, 이 글에서 프로덕션이 새벽에 조용히 죽는 이유 하나를 정확히 짚어낼 수 있습니다. 결론부터 말하면 원인은 대개 프롬프트 품질이 아니라 API 경계에 계약이 없다는 사실입니다.
많은 팀이 LLM 통합 문제를 프롬프트 튜닝으로 풀려고 합니다. 응답이 이상하면 지시문을 다듬고, 형식이 깨지면 예시를 몇 개 더 넣습니다. 이 접근이 완전히 틀린 건 아니지만 근본 원인은 비껴갑니다. 모델은 함수가 아니라 원격 서비스이고, 원격 서비스는 지연되고 실패하고 때로는 형식을 어깁니다. 이 글은 프롬프트를 계속 다듬는 대신, API 경계 자체에 명시적인 계약을 세우는 다섯 가지 설계 원칙을 다룹니다.
글의 핵심 개념을 형상화했습니다.
LLM은 함수가 아니라 원격 서비스입니다
전통적인 함수 호출은 같은 입력에 같은 출력을 돌려줍니다. 이 결정론적 특성 덕분에 테스트를 짤 수 있고, 실패했을 때 원인을 좁혀갈 수 있습니다. LLM은 다릅니다. 같은 프롬프트를 넣어도 온도 파라미터, 모델 내부 상태, 서빙 인프라의 부하에 따라 다른 결과가 나옵니다. 이건 버그가 아니라 확률적 시스템의 본질입니다.
더 중요한 건 호출 경로입니다. 프롬프트를 보내는 순간부터 네트워크 지연, 프로바이더 측 순간 장애, 속도 제한, 토큰 할당량 소진 같은 변수가 끼어듭니다. REST API를 호출하는 것과 똑같은 위험을 안고 있는데, 많은 팀이 이 사실을 인식하지 못한 채 로컬 함수처럼 코드를 짭니다. 응답이 오면 그대로 파싱해서 다음 단계로 넘기고, 실패는 예외 처리 한 줄로 뭉갭니다.
실제로 흔히 벌어지는 장애 시나리오는 이렇습니다. 시스템이 모델 응답을 JSON으로 파싱해서 데이터베이스에 저장합니다. 모델이 평소에는 깔끔한 JSON만 뱉다가, 어느 순간 앞에 짧은 설명 문장을 붙여서 응답합니다. 파싱은 그 자리에서 실패하고, 예외 처리가 허술하면 파이프라인 전체가 중단됩니다. 계약이 있었다면 출력 형식을 사전에 명시하고, 형식을 벗어난 응답은 별도 경로로 처리했을 것입니다.
이 관점 전환이 나머지 모든 설계 원칙의 출발점입니다. LLM을 네트워크 서비스로 대하기 시작하면, 지금까지 익숙했던 회복탄력성 패턴, 즉 타임아웃, 재시도, 서킷브레이커, 폴백이 그대로 적용됩니다. 새로 발명할 필요가 없습니다. 이미 알고 있는 분산 시스템 원칙을 LLM 경계에 옮겨 적용하면 됩니다.
입력을 게이트 없이 모델에 넘기지 마라
입력 계약의 핵심은 모델을 호출하기 전에 입력이 유효한지 검증하는 데 있습니다. 이 단계를 게이트라고 부릅니다. 게이트를 통과한 입력만 모델로 전달되고, 통과하지 못한 입력은 즉시 거부되거나 별도로 처리됩니다.
프롬프트는 자유 형식 텍스트처럼 보이지만 실제로는 구조화된 메시지의 조합입니다. 시스템 메시지는 모델의 행동 규범을 정의하고, 사용자 입력은 실제로 풀어야 할 문제를 전달하며, 컨텍스트는 모델이 과거 대화를 기억하지 못한다는 한계를 보완합니다. 세 요소를 조합하는 방식이 모델 응답의 일관성을 결정합니다.
여기서 자주 놓치는 부분이 있습니다. 사용자 입력의 길이나 형식을 검증하지 않고 그대로 컨텍스트에 밀어넣는 경우입니다. 컨텍스트가 지나치게 길어지면 모델이 중간 정보를 잊어버리거나 비용이 불필요하게 늘어납니다. 반대로 필수 필드가 빠진 요청을 그대로 넘기면 모델은 없는 정보를 추측해서 채우고, 그 결과는 겉보기에는 그럴듯하지만 사실과 다른 응답으로 이어집니다.
효과적인 입력 검증은 여러 층으로 구성합니다. 첫 번째 층은 스키마 검증으로, 필수 필드의 존재와 데이터 타입, 문자열 길이 범위를 확인합니다. 두 번째 층은 의미 검증으로, 값 자체는 형식에 맞지만 비즈니스 규칙에 어긋나는 입력을 걸러냅니다. 이 두 층을 통과한 요청만 모델에 도달하게 하면, 모델의 출력 품질 문제 상당수가 애초에 발생하지 않습니다.
출력은 반드시 폴백을 가진 상태로 설계합니다
LLM의 출력은 다음 컴포넌트로 들어가는 입구입니다. 파싱 로직, 데이터베이스, 다른 API, 사용자 화면까지 이 출력을 받아 움직입니다. 문제는 모델의 출력이 본질적으로 확정적이지 않다는 데 있습니다. 같은 입력을 줘도 형태가 조금씩 다른 출력이 나올 수 있고, 이 불확실성을 어떻게 다루느냐가 출력 계약 설계의 핵심입니다.
출력을 구조화하는 방법은 크게 세 가지로 나뉩니다. 강제 구조화는 모델이 특정 스키마 형태로만 출력하도록 강제하는 방법입니다. 파싱이 쉬운 대신, 모델이 구조를 따르지 못하면 곧바로 파싱 실패로 이어집니다. 가이드 구조화는 모델이 비교적 자유롭게 출력하게 두고, 그 뒤에 구조화된 형태로 변환하는 로직을 붙이는 방법입니다. 강제 구조화보다 유연하지만 변환 과정에서 정보 손실이나 오류가 생길 수 있습니다. 사후 구조화는 모델의 자유 출력을 정규 표현식이나 파싱 로직으로 나중에 해석하는 방법으로, 가장 유연한 대신 가장 취약합니다.
| 전략 | 장점 | 위험 | 적합한 상황 |
|---|---|---|---|
| 강제 구조화 | 파싱이 단순하고 예측 가능 | 형식 이탈 시 즉시 실패 | 스키마가 고정된 API 응답, 폼 자동완성 |
| 가이드 구조화 | 유연하면서도 구조를 확보 | 변환 단계에서 정보 손실 가능 | 긴 서술형 응답을 구조로 옮길 때 |
| 사후 구조화 | 가장 유연, 모델 제약 최소 | 출력이 조금만 달라져도 파서가 깨짐 | 초기 프로토타입, 낮은 신뢰도 허용 구간 |
실무에서는 강제 구조화와 가이드 구조화를 조합하는 편이 효과적입니다. 모델에게 가능한 한 구조를 따르도록 요구하되, 따르지 않을 경우를 대비해 폴백 로직을 미리 마련해두는 것입니다. 폴백은 단순히 오류를 로깅하고 끝내는 게 아닙니다. 파싱에 실패한 응답을 재요청 대상으로 분류하거나, 이전 성공 응답을 캐시에서 꺼내 대체하거나, 최소한의 안전한 기본값으로 시스템이 계속 동작하게 만드는 대안까지 포함합니다. 폴백 경로가 없는 출력 계약은 계약이 아니라 희망 사항에 가깝습니다.
장애는 예외가 아니라 전제입니다
아무리 견고하게 설계해도 장애는 발생합니다. 네트워크 단절, API 서버의 일시적 과부하, 속도 제한 초과, 모델 서버의 예기치 않은 종료까지 다양한 원인으로 호출이 실패할 수 있습니다. 관건은 장애가 발생했을 때 시스템이 어떻게 행동하느냐입니다. 대비하지 않으면 단일 장애가 전체 시스템의 중단으로 번집니다.
재시도는 가장 기본적인 장애 복원 수단입니다. 다만 아무 조건 없이 재시도하면 오히려 문제가 악화됩니다. 일시적인 장애 상황에서 무분별한 재시도는 서버에 추가 부하를 주고 속도 제한에 걸릴 위험도 키웁니다. 고정 간격 재시도는 구현이 간단하지만, 서버가 이미 과부하 상태라면 상황을 악화시킬 수 있습니다. 더 나은 전략은 지수 백오프입니다. 재시도할 때마다 대기 시간을 두 배로 늘리는 방식으로, 첫 번째 재시도는 1초 후, 두 번째는 2초 후, 세 번째는 4초 후로 이어집니다. 서버가 스스로 회복할 시간을 벌어주는 동시에 불필요한 재시도도 줄여줍니다.
재시도 전략을 설계할 때는 최대 재시도 횟수, 초기 대기 시간, 최대 대기 시간, 재시도할 예외 유형을 명확히 정해야 합니다. 최대 재시도 횟수를 정하지 않으면 무한 재시도로 이어질 수 있고, 대기 시간이 너무 길면 불필요한 지연이 생기며 너무 짧으면 서버에 부하가 집중됩니다. 재시도할 예외 유형도 신중히 골라야 합니다. 네트워크 단절이나 서버 오류처럼 일시적인 장애라면 재시도가 유효하지만, 요청 자체가 잘못된 경우라면 재시도해도 같은 실패가 반복될 뿐입니다.
재시도만으로는 충분하지 않은 경우도 있습니다. 서버가 완전히 다운되었거나 속도 제한에 계속 걸리는 상황에서는 서킷브레이커 패턴이 필요합니다. 연속 실패가 임계값을 넘으면 일정 시간 동안 호출 자체를 차단하고, 대신 폴백 경로로 즉시 전환합니다. 이렇게 하면 이미 죽어가는 서비스에 계속 요청을 던져 상황을 악화시키는 대신, 시스템이 저하된 상태로라도 계속 응답할 수 있습니다.
비용은 사고 후 발견하는 것이 아니라 사전에 차단하는 것입니다
LLM API의 비용은 토큰 수를 기준으로 계산됩니다. 입력 토큰과 출력 토큰 각각에 비용이 부과되며, 모델마다 단가가 다릅니다. 문제는 이 비용이 눈에 잘 띄지 않는 방식으로 쌓인다는 점입니다. 컨텍스트가 길어질수록, 재시도가 반복될수록, 대화가 길게 이어질수록 비용은 조용히 누적됩니다.
가장 확실한 대응은 토큰 사용량을 매 호출마다 기록하는 것입니다. 대부분의 LLM API 제공자는 응답 헤더나 사용량 필드에 실제 소비된 토큰 수를 돌려줍니다. 이 값을 쌓아두면 일간이나 주간 단위로 실제 비용을 추적할 수 있고, 추정이 아니라 실측치로 예산을 관리할 수 있습니다.
비용 관리의 핵심은 예산을 초과하기 전에 감지하고 대응하는 데 있습니다. 예산 한도를 설정하고 사용량이 한도에 근접하면 알림을 발생시킵니다. 여기서 한 단계 더 나아가야 하는 지점이 있습니다. 알림만으로는 늦은 경우가 많다는 것입니다. 비정상적인 사용량 패턴, 예를 들어 평소보다 몇 배나 많은 토큰이 짧은 시간에 소비되는 상황이 감지되면, 사람이 확인하기 전에 시스템이 스스로 호출을 제한하는 서킷브레이커가 있어야 합니다. 버그로 인한 무한 루프나 악의적인 요청이 원인일 수 있는데, 이런 상황에서 청구서를 받고 나서야 알게 되는 것은 계약이 없다는 명백한 증거입니다.
모델 버전도 계약의 일부입니다
LLM 제공자는 간헐적으로 모델을 업데이트합니다. 새 버전은 종종 이전 버전과 다른 방식으로 응답하고, 기존에 잘 동작하던 프롬프트가 갑자기 다른 형식의 출력을 내기 시작합니다. 이 문제를 프롬프트 엔지니어링으로 해결하려는 시도는 매번 실패합니다. 원인이 프롬프트가 아니라 모델 버전이라는 외부 변수에 있기 때문입니다.
버전 관리의 첫 원칙은 가능하면 모델 버전을 명시적으로 고정하는 것입니다. 자동으로 최신 버전을 따라가는 별칭 대신 특정 버전을 지정하면, 최소한 언제 버전이 바뀔지는 통제할 수 있습니다. 물론 영원히 고정할 수는 없습니다. 결국 새 버전으로 전환해야 하는 시점이 옵니다. 이때 필요한 것이 회귀 테스트 세트입니다. 대표적인 입력 샘플과 기대하는 출력 형태를 미리 만들어두고, 새 버전으로 전환하기 전에 이 세트를 돌려 출력 계약이 여전히 지켜지는지 확인합니다.
버전 전환은 한 번에 전면 교체하기보다 점진적으로 진행하는 편이 안전합니다. 트래픽 일부만 새 버전으로 보내고 출력 품질과 실패율을 관찰한 뒤, 문제가 없으면 비중을 늘려갑니다. 이 과정 자체가 앞서 설명한 입력 게이트, 출력 폴백, 재시도 로직 위에서 돌아갑니다. 계약이 이미 갖춰져 있다면 버전 전환은 위험한 도박이 아니라 관리 가능한 운영 작업이 됩니다.
계약이 곧 신뢰성입니다
여기까지 다룬 다섯 가지 원칙, 즉 입력 게이트, 출력 폴백, 재시도와 서킷브레이커, 비용 감시, 버전 고정은 서로 독립된 팁이 아닙니다. 전부 하나의 관점에서 나옵니다. LLM을 네트워크 너머의 신뢰할 수 없는 서비스로 보고, 그 불확실성을 API 경계에서 코드로 흡수하라는 것입니다.
프롬프트를 잘 쓰는 것도 물론 중요합니다. 하지만 프롬프트는 모델의 평균적인 행동을 개선할 뿐, 꼬리 사례에서 벌어지는 실패까지 막아주지는 못합니다. 새벽에 파이프라인이 멈추는 사고는 대부분 이 꼬리 사례에서 시작됩니다. 그리고 꼬리 사례를 다루는 도구는 더 좋은 프롬프트가 아니라 명시적인 계약입니다.
지금 운영 중인 LLM 통합 코드를 한번 열어보길 권합니다. 모델 응답을 파싱하는 곳 바로 앞뒤에 입력 검증과 출력 폴백이 있는지, 호출 실패를 감싸는 재시도 로직에 백오프와 상한이 있는지, 토큰 사용량이 어딘가에 기록되고 있는지, 모델 버전이 명시적으로 고정되어 있는지 확인해보십시오. 이 다섯 개 질문에 모두 답할 수 있다면 계약이 있는 것이고, 하나라도 막히면 그 지점이 다음 장애가 시작될 자리입니다.
챕터 삽화
