목차
AI 애플리케이션 배포
노트북에서 graph.invoke()가 성공해도 서비스가 준비된 것은 아니다. 여러 사용자가 동시에 요청할 때 누구의 대화 상태인지, 도구 호출이 실패하면 어디서 다시 시작할지, 응답이 늦을 때 무엇을 보여줄지를 정해야 한다.
이 장에서는 앞 장에서 만든 LangGraph 그래프를 API 뒤에 놓는 과정을 예로 든다. 특정 호스팅 제품만을 전제로 하지 않고, 애플리케이션 경계와 검증할 항목을 먼저 살펴본다.
flowchart LR
U[브라우저·앱] --> API[인증된 API]
API --> G[LangGraph 실행]
G --> M[모델 제공자]
G --> T[검색·업무 도구]
G --> P[(체크포인트·저장소)]
G --> O[추적·지표]
API --> U
1. 로컬 실행과 서버 실행의 차이
| 항목 | 로컬 실습 | 운영 서비스 |
|---|---|---|
| 상태 | 프로세스 메모리로 충분할 수 있음 | 재시작 후 복구와 사용자별 격리 필요 |
| 입력 | 실습자가 직접 전달 | 외부 입력의 길이·형식·권한 확인 필요 |
| 비밀값 | 개발 환경 변수 | 배포 환경의 비밀 저장소와 접근 제한 |
| 실패 | 화면에서 오류를 확인 | 사용자 메시지와 내부 진단 정보를 분리 |
| 호출량 | 한 사람의 요청 | 동시 실행, 시간 제한, 사용량 제한 |
| 관찰 | print() | 요청 ID, 단계별 지연, 오류율, 비용 추적 |
먼저 애플리케이션의 입력과 출력 계약을 정한다. 예를 들어 입력은 question, 인증된 user_id, thread_id이고 출력은 answer, citations, status가 될 수 있다. user_id를 사용자가 보낸 JSON 그대로 신뢰하지 말고 인증 결과에서 얻는다. 존재하지 않는 문서 ID를 인용한 응답은 정상 응답으로 표시하지 않는다.
2. 그래프를 배포 단위로 만들기
LangSmith의 Agent Server 방식에서는 그래프 객체를 모듈에서 내보내고 langgraph.json에 그래프의 위치를 지정한다. 다음은 프로젝트 구조 예시다. 실제 의존성 버전과 모델 패키지는 사용하는 환경에 맞춰 고정한다.
my-agent/
├── my_agent/
│ └── agent.py # graph 객체를 내보내는 파일
├── pyproject.toml # Python 의존성
└── langgraph.json # 배포할 그래프의 진입점
{
"dependencies": ["."],
"graphs": {
"agent": "./my_agent/agent.py:graph"
},
"env": "./.env"
}
dependencies는 패키지를 찾을 위치이고, graphs.agent는 API에서 사용할 그래프 이름과 Python 객체의 위치다. env는 로컬 개발에서 사용할 파일을 가리킬 수 있지만 .env 파일을 저장소나 배포 이미지에 넣어 비밀값을 공개하지 않도록 관리해야 한다. 운영 환경에서는 배포 시스템의 비밀값 설정을 사용한다. 위 형식은 공식 로컬 개발 문서의 설정 예시를 기준으로 했다.
3. 개발 서버에서 API 확인하기
Agent Server를 쓰는 경우 langgraph dev는 빠른 로컬 반복에, langgraph up은 Docker와 데이터 저장소를 포함한 운영 유사 검증에 사용한다. 필요한 설치 방식과 실행 조건은 공식 로컬 개발 문서를 확인한다.
# 프로젝트에 langgraph-cli와 그래프 의존성을 설치한 뒤
langgraph dev
# 컨테이너 환경에서 의존성·시작·그래프 실행을 확인할 때
langgraph up --recreate
[로컬 API 호출 예시] langgraph_sdk를 설치하고 로컬 서버가 실행 중일 때의 코드다. 여기서는 대화 이력을 이어가지 않는 threadless run을 사용한다.
from langgraph_sdk import get_sync_client
client = get_sync_client(url="http://localhost:2024")
for chunk in client.runs.stream(
None,
"agent", # langgraph.json의 graphs 키
input={"messages": [{"role": "human", "content": "휴가 규정을 요약해줘"}]},
stream_mode="messages-tuple",
):
print(chunk)
실서비스에서는 서버 응답을 그대로 브라우저에 전달하지 말고, 공개 가능한 메시지 유형만 골라 UI 계약에 맞게 변환한다. 예를 들어 도구 호출 인자에 내부 경로나 사용자 데이터가 포함될 수 있다. Agent Server 로컬 테스트 예시에서 SDK 호출 형태를 확인할 수 있다.
4. 대화 상태와 재시작
4장에서 본 체크포인터는 스레드별 그래프 상태를 저장한다. 체크포인터는 대화 실행 상태, 스토어는 스레드 밖에서 사용할 장기 데이터를 다룬다. 로컬 그래프에서는 직접 설정하지만, Agent Server는 지속성 인프라를 제공한다. 세부 내용은 LangGraph Persistence에 정리돼 있다.
sequenceDiagram
participant U as 사용자
participant A as API
participant G as 그래프
participant P as 체크포인트
U->>A: 질문 + 대화 식별자
A->>A: 사용자와 대화 소유권 확인
A->>G: 인증된 thread_id로 실행
G->>P: 단계별 상태 저장
G-->>A: 답변 또는 중단 상태
A-->>U: 공개 가능한 결과
같은 thread_id를 다른 사용자에게 그대로 열어주면 대화 내용이 섞일 수 있다. 스레드 ID를 받더라도 서버에서 해당 사용자가 그 스레드에 접근할 권한이 있는지 검사한다. 메모리 내 체크포인터는 프로세스 재시작 후 상태가 사라지므로, 서버를 직접 운영한다면 영속 저장소와 백업 정책을 검토한다.
5. 외부 도구와 실패 처리
도구 호출은 일반 텍스트 생성보다 실패 방식이 다양하다. 검색 API의 시간 초과, DB 연결 실패, 중복 실행, 외부 서비스 요금 초과가 각각 다른 문제다.
[쓰기 도구를 실행할 때]
- 모델이 만든 인자를 스키마로 검증한다. 대상 ID와 사용자의 권한은 서버에서 다시 확인한다.
- 결제·메일 발송·데이터 수정처럼 재시도 시 중복되면 곤란한 작업에는 멱등 키나 중복 방지 절차를 둔다.
- 읽기와 쓰기 도구를 분리하고, 민감한 쓰기 작업은 사용자 승인 지점을 둔다.
- 실패한 단계와 재시도 가능 여부를 기록한다. 모든 예외를 “다시 시도해주세요”로만 숨기지 않는다.
LLM의 도구 호출 요청은 실행 권한의 증거가 아니다. 프롬프트에서 허락했다는 문장도 서버 권한 검사를 대신할 수 없다.
6. 스트리밍과 시간 제한
긴 답변에서는 완료까지 기다리는 대신 모델의 생성 내용을 점진적으로 보여줄 수 있다. LangGraph는 실행 중 메시지나 상태 업데이트를 스트리밍할 수 있다(Streaming 문서). 다만 화면에 보인 부분 답변이 최종 검증을 통과했다는 뜻은 아니다.
flowchart LR
A[요청] --> B[검색 상태 표시]
B --> C[모델 토큰 스트리밍]
C --> D[최종 근거·형식 검사]
D -->|통과| E[완료]
D -->|실패| F[오류 표시·부분 답변 폐기]
클라이언트가 연결을 끊거나 서버의 시간 제한에 걸렸을 때 그래프 실행을 어떻게 종료할지 정한다. 상태가 남아 있으면 이어서 실행할 수 있는지, 도구가 이미 실행됐는지 확인해야 한다. 응답 생성 시작 시간만 보지 말고 검색, 모델 첫 토큰, 전체 완료 시간을 따로 측정한다.
7. 배포 전후 확인 목록
[배포 전]
- 깨끗한 환경에서 의존성을 설치하고 서버가 시작되는가?
- 정상 질문, 자료 없는 질문, 매우 긴 입력, 잘못된 형식에 예상한 응답을 반환하는가?
- 각 사용자에게 자신의 스레드와 문서만 보이는가?
- 모델 또는 외부 도구가 실패할 때 비밀값이나 내부 오류가 응답에 노출되지 않는가?
- 정해 둔 평가 데이터에서 이전 버전보다 중요한 지표가 나빠지지 않았는가?
[배포 후]
- 오류율, 지연 시간, 토큰 사용량, 근거 없는 답변의 비율을 관찰한다.
- 새 버전은 일부 트래픽에서 먼저 확인하고, 문제가 생기면 이전 그래프·프롬프트·인덱스 버전으로 되돌릴 수 있게 준비한다.
- 문서가 바뀌면 인덱스 갱신 시점과 그래프 버전을 함께 기록한다.
LangSmith Deployment는 Agent Server를 Cloud, Hybrid, Self-hosted 등 여러 환경에서 운영하는 선택지를 제공한다. 선택에 따라 라이선스와 인프라 요구 사항이 다르므로 현재 배포 개요를 확인한다. 다음 장에서는 배포의 전후에 같은 사례를 어떻게 평가하고, 운영 중 발생한 실패를 개선에 반영할지 다룬다.
8. API 경계에서 대화 소유권 확인하기
thread_id는 그래프의 상태를 찾는 키이지 사용자를 인증하는 수단이 아니다. 클라이언트가 보낸 값을 그대로 configurable.thread_id로 넘기면 다른 사람의 대화 식별자를 추측하거나 얻은 사용자가 그 상태를 읽을 가능성이 생긴다. 서버는 인증 결과에서 사용자 ID를 얻고, 영속 저장소에서 그 사용자가 해당 스레드를 소유하는지 확인한 후 그래프를 호출해야 한다.
아래는 프레임워크에 관계없는 경계 함수의 예시다. get_owner는 서비스의 DB 조회 함수, graph는 배포된 그래프 객체라고 가정한다. 새 대화를 만들 때는 서버가 스레드 ID를 생성하고 소유자 행을 저장한다. user_id를 요청 본문에 넣어 받는 구조가 아니다.
from typing import Callable
def run_conversation(
*,
authenticated_user_id: str,
thread_id: str,
question: str,
get_owner: Callable[[str], str | None],
graph,
) -> dict:
if not question.strip() or len(question) > 500:
raise ValueError("질문은 1자 이상 500자 이하로 입력하세요.")
if get_owner(thread_id) != authenticated_user_id:
raise PermissionError("이 대화에 접근할 수 없습니다.")
return graph.invoke(
{"messages": [{"role": "user", "content": question.strip()}]},
config={"configurable": {"thread_id": thread_id}},
)
이 함수는 스레드 소유권만 확인한다. 검색 문서의 권한 필터도 별도로 필요하다. 한 스레드 안에서 이전에 조회했던 문서가 이후 권한 변경으로 더 이상 보이지 않아야 할 수 있기 때문이다. 그래프 내부의 검색 노드는 현재 사용자 권한으로 매 요청을 필터링하고, 체크포인트에 남은 민감한 텍스트의 보존 기간도 정한다. LangGraph persistence 문서는 thread_id를 사용한 상태 저장과 재개 방식을 설명한다.
9. 실패가 어디까지 진행됐는지 구분하기
graph.invoke()가 예외를 던졌다는 사실만으로 외부 작업이 실행되지 않았다고 판단할 수 없다. 메일 도구가 성공한 뒤 결과 저장에 실패했을 수 있고, 재시도하면 메일이 두 번 갈 수 있다. 도구별로 실행 전 / 외부 요청 성공 / 결과 기록 완료를 구분할 수 있는 작업 ID를 남긴다.
| 실패 위치 | 사용자에게 보일 상태 | 서버에서 확인할 것 |
|---|---|---|
| 입력 검증 전 | 입력 수정 요청 | 길이·형식·빈 문자열 |
| 검색 단계 | 근거를 찾지 못함 또는 일시 오류 | 결과 0건과 검색기 장애를 구별 |
| 모델 호출 | 일시 오류, 안전한 재시도 | 제공자 시간 초과·요청량 제한 |
| 쓰기 도구 직후 | 결과 확인 중 | 멱등 키로 외부 작업 완료 여부 조회 |
| 응답 전송 중 | 연결 끊김 | 실행 지속 여부와 체크포인트 위치 |
읽기 전용 검색의 일시적인 연결 오류는 제한된 횟수만 다시 시도할 수 있다. 쓰기 도구는 같은 멱등 키로 재호출해 중복을 막거나, 먼저 외부 시스템에서 작업 상태를 확인해야 한다. 승인 상태가 체크포인트에 저장된다면 재개 시에도 현재 사용자 권한과 승인 유효기간을 다시 검사한다. 프롬프트에 적힌 “승인됨”이라는 문자열은 승인 기록이 아니다.
10. 스트림을 UI에 보낼 때의 계약
검색 시작, 모델 초안, 최종 검증 완료를 같은 answer 이벤트로 보내면 사용자는 아직 검증되지 않은 문장을 확정 답변으로 오해할 수 있다. 클라이언트와 다음처럼 이벤트 의미를 합의한다.
| 이벤트 | 화면 처리 | 저장 여부 |
|---|---|---|
status: retrieving | “근거를 찾는 중” 표시 | 선택적 |
draft_delta | 임시 텍스트 표시 | 최종 답으로 저장하지 않음 |
completed | 인용 검증된 답으로 교체 | 저장 |
failed | 임시 답을 제거하고 오류 안내 | 실패 코드만 저장 |
부분 토큰의 순서가 뒤바뀌거나 연결이 끊길 때를 위해 요청 ID와 이벤트 순서를 붙인다. 브라우저 재연결 후 이전 스트림을 다시 보내는지, 저장된 최종 결과만 가져오는지를 결정해야 한다. 스트리밍 자체가 첫 토큰까지의 대기를 줄여 보일 수는 있어도 전체 작업 시간이나 모델 비용을 줄여주지는 않는다. LangGraph 스트리밍 문서에서 상태 업데이트와 메시지 스트림을 구분한다.
11. 작은 배포의 검증 순서
먼저 로컬 서버에서 근거 있음 / 근거 없음 / 검색 오류 / 다른 사용자 스레드 네 요청을 각각 확인한다. 그다음 컨테이너 환경에서 동일한 평가 데이터셋을 실행해 개발 PC에만 있던 파일이나 환경 변수에 의존하지 않는지 본다. 실제 운영 주소를 붙인 후에는 작은 트래픽으로 오류율과 지연을 살핀다.
롤백에는 그래프 코드만이 아니라 프롬프트, 모델 설정, 문서 인덱스 버전이 포함된다. 새 코드가 이전 인덱스 형식을 읽지 못하면 코드만 되돌려도 복구되지 않는다. 체크포인트 스키마가 바뀌었다면 이전 대화를 어떻게 읽을지 먼저 정한다. 운영 지표는 전체 평균뿐 아니라 긴 질문, 검색 결과가 많은 질문, 도구를 사용하는 질문을 나눠 본다. 다음 장의 오프라인 평가 결과와 운영 추적을 같은 버전 식별자로 연결하면 배포 후 악화 지점을 찾기 쉽다.