본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2026. 4. 13. · 0 Views
Python 고급 테스트와 프로덕션 실전 가이드
AI 에이전트 개발자가 알아야 할 Python 고급 테스트 전략, 재현성 보장, 프로덕션 배포 시 흔히 겪는 함정을 다룹니다. 초급 개발자가 실무에서 바로 적용할 수 있는 실전 노하우를 전달합니다.
목차
- 확률적_출력_에이전트_코드_테스트_전략
- 골든_프롬프트와_스냅샷_테스트
- 단위_테스트_vs_통합_테스트_분리
- uv_poetry_종속성_관리와_재현성_보장
- 프로덕션_에이전트_앱_일반_함정_5가지
- 무제한_재시도_글로벌_상태_리소스_누수_방지
- 관찰_가능성을_위한_구조화된_로깅
1. 확률적 출력 에이전트 코드 테스트 전략
어느 날 김개발 씨가 팀에 합류했습니다. LLM 에이전트 코드를 테스트하려고 assert 문을 작성했는데, 매번 결과가 달라서 테스트가 번번이 실패했습니다.
"이거 어떻게 테스트하죠?" 김개발 씨의 고민이 시작되었습니다.
확률적 출력 테스트는 LLM이 매번 다른 결과를 반환하는 환경에서도 안정적으로 테스트를 수행하는 전략입니다. 마치 날씨가 매일 달라도 기상 예보의 정확도를 평가하는 것과 같습니다.
핵심은 결과의 정확한 일치가 아니라 의미적 일치를 검증하는 것입니다.
다음 코드를 살펴봅시다.
import json
def test_agent_response_structure():
"""에이전트 응답이 올바른 구조를 갖추는지 검증"""
response = my_agent.run("파이썬 리스트 정렬 방법 알려줘")
# 핵심 포인트: 정확한 텍스트가 아니라 구조를 검증합니다
assert "answer" in response
assert "confidence" in response
# 값의 범위를 검증합니다
assert 0 <= response["confidence"] <= 1
# 답변이 비어있지 않은지 확인합니다
assert len(response["answer"].strip()) > 0
def test_agent_deterministic_with_seed():
"""시드를 고정해 결정론적 테스트 수행"""
result = my_agent.run("1+1은?", seed=42)
assert result["answer"] == "2"
"AI 에이전트 AI 엔지니어 되기 위한 로드맵" 코스의 세 번째 시간입니다. 지난 2화에서는 Python 프로젝트 구조와 타입 힌팅의 기본기를 다졌습니다.
이번에는 그 기반 위에서 실무 에이전트 코드를 어떻게 테스트하는지 배워보겠습니다. 김개발 씨는 입사 3개월 차 주니어 개발자입니다.
LLM 기반 에이전트 코드를 작성하고, 당연하게도 단위 테스트를 작성했습니다. assert response == "파이썬 리스트는 sort() 메서드로 정렬합니다" 이렇게요.
그런데 테스트를 돌릴 때마다 실패합니다. LLM이 매번 조금 다른 표현으로 답변하니까요.
박시니어 씨가 커피를 들고 다가왔습니다. "아, 전형적인 초보 실수네요.
LLM은 확률적 모델이라서 같은 질문에도 매번 다른 문장으로 대답합니다. 텍스트를 정확히 비교하는 건 불가능해요." 확률적 출력 테스트의 핵심은 무엇일까요?
쉽게 비유하자면, 요리사에게 "김치찌개 만들어줘"라고 매일 주문하는 것과 같습니다. 요리사가 매번 똑같은 맛을 내진 않겠죠.
하지만 "김치찌개 맛이 나는가?", "국물이 너무 짜지 않은가?" 같은 기준은 검증할 수 있습니다. LLM 테스트도 마찬가지입니다.
가장 기본적인 전략은 구조 검증입니다. 응답이 JSON 형태라면, 필드가 존재하는지, 값의 타입이 맞는지, 범위가 유효한지를 검증합니다.
텍스트 내용 자체가 아니라 형태와 제약 조건을 확인하는 것이죠. 두 번째 전략은 의미적 유사성 검사입니다.
임베딩 모델을 활용해 예상 답변과 실제 답변의 코사인 유사도를 계산합니다. "리스트를 정렬하는 방법은 sort()를 사용하는 것입니다"와 "정렬하려면 sort 메서드를 쓰면 됩니다"는 문장이 다르지만 의미는 같습니다.
세 번째 전략은 시드 고정입니다. 많은 LLM 라이브러리가 seed 파라미터를 지원합니다.
테스트 환경에서는 시드를 고정해 항상 동일한 결과를 얻을 수 있습니다. 하지만 이 방법은 모델 업데이트 시 테스트가 깨질 수 있다는 단점이 있습니다.
코드를 살펴보겠습니다. 첫 번째 테스트 함수 test_agent_response_structure에서는 응답 객체에 answer와 confidence 필드가 있는지 확인합니다.
그리고 confidence가 0과 1 사이인지, 답변이 빈 문자열이 아닌지 검증합니다. 이것이 구조 검증의 핵심 패턴입니다.
두 번째 테스트 test_agent_deterministic_with_seed에서는 시드를 고정해 항상 같은 결과를 얻는 방식입니다. 간단한 수학 질문에 대해 "2"라는 정확한 답변이 나오는지 확인합니다.
실제 현업에서는 이 두 가지를 조합해서 사용합니다. 주요 기능은 구조 검증으로, 핵심 로직은 시드 고정으로 검증하죠.
OpenAI의 evals 라이브러리나 LangChain의 테스트 도구도 같은 철학을 따릅니다. 주의할 점이 있습니다.
테스트에서 실제 LLM API를 호출하면 비용이 발생하고 속도도 느려집니다. 따라서 모킹(mocking)과 함께 사용하는 것이 좋습니다.
pytest의 @patch 데코레이터로 API 호출을 가로채고, 미리 정의한 응답을 반환하도록 설정할 수 있습니다. 김개발 씨는 박시니어 씨의 설명을 듣고 테스트 코드를 수정했습니다.
이번에는 테스트가 안정적으로 통과되었습니다. "테스트의 목적이 뭔지 다시 생각해보니까 너무 당연한 건데, 몰랐네요." 확률적 출력을 다루는 테스트 전략은 AI 에이전트 개발의 필수 기술입니다.
정확한 일치가 아닌 신뢰할 수 있는 일관성을 검증하는 것, 그것이 핵심입니다.
실전 팁
💡 - 테스트에서 LLM API를 직접 호출하지 말고 모킹과 시드 고정을 적극 활용하세요
- 응답의 내용이 아니라 구조, 타입, 범위를 검증하는 습관을 들이세요
- 이 카드뉴스는 "AI 에이전트 AI 엔지니어 되기 위한 로드맵" 코스의 3/16편입니다
2. 골든 프롬프트와 스냅샷 테스트
김개발 씨는 테스트를 안정화한 뒤 새로운 고민에 빠졌습니다. "에이전트 프롬프트를 수정했는데, 기존에 잘 되던 기능이 망가졌는지 어떻게 확인하지?" 이 질문이 이끌어낸 것이 골든 프롬프트 테스트와 스냅샷 테스트입니다.
골든 프롬프트 테스트는 검증된 프롬프트와 응답 쌍을 저장해두고, 코드 변경 후에도 동일한 품질이 유지되는지 확인하는 방법입니다. 스냅샷 테스트는 에이전트의 전체 출력을 스냅샷으로 저장하고, 변경 시 의도치 않은 차이를 감지합니다.
마치 요리 레시피의 '맛 기준'을 저장해두고 매번 비교하는 것과 같습니다.
다음 코드를 살펴봅시다.
import json
def load_golden_prompts(path="tests/golden_prompts.json"):
"""검증된 프롬프트-응답 쌍을 로드합니다"""
with open(path) as f:
return json.load(f)
def test_against_golden():
"""골든 프롬프트 기준으로 에이전트 응답 품질 검증"""
golden = load_golden_prompts()
for case in golden["cases"]:
response = my_agent.run(case["input"], seed=42)
# 핵심 포인트: 핵심 키워드가 포함되어 있는지 확인합니다
for keyword in case["expected_keywords"]:
assert keyword in response["answer"], \
f"키워드 '{keyword}' 누락: {response['answer'][:100]}"
김개발 씨의 팀에서는 에이전트 프롬프트를 수시로 수정합니다. 고객 피드백이 들어오면 프롬프트를 조정하고, 새로운 기능이 추가되면 프롬프트도 함께 바뀝니다.
그런데 문제가 생겼습니다. A 기능을 위해 프롬프트를 수정했더니 B 기능이 망가졌습니다.
박시니어 씨가 화이트보드에 다이어그램을 그리기 시작했습니다. "이런 문제를 회귀 버그라고 해요.
해결하려면 골든 프롬프트 테스트가 필요합니다." 골든 프롬프트란 무엇일까요? 쉽게 비유하자면, 미슐랭 레스토랑의 시식 노트와 같습니다.
셰프가 새로운 메뉴를 개발할 때마다 기존 메뉴의 맛이 변하지 않았는지 확인하기 위해, 미리 기록해둔 시식 노트와 비교합니다. 골든 프롬프트도 같은 역할을 합니다.
구체적으로 동작하는 방식은 이렇습니다. 먼저 에이전트가 잘 동작하는 기준 프롬프트와 예상 키워드를 JSON 파일에 저장합니다.
이것이 골든 데이터셋입니다. 프롬프트를 수정한 후에는 이 데이터셋으로 테스트를 돌립니다.
응답에 필수 키워드가 모두 포함되어 있는지 확인하죠. 코드를 살펴보겠습니다.
load_golden_prompts 함수는 JSON 파일에서 검증된 테스트 케이스를 불러옵니다. 각 케이스에는 input(입력 프롬프트)과 expected_keywords(반드시 포함되어야 할 키워드 목록)가 들어 있습니다.
test_against_golden 함수에서는 골든 데이터셋의 각 케이스를 순회하며 에이전트를 실행합니다. 시드를 고정해 재현성을 확보하고, 응답에 모든 예상 키워드가 포함되어 있는지 검증합니다.
키워드가 누락되면 에러 메시지와 함께 응답의 앞부분을 출력해 원인을 파악할 수 있게 합니다. 스냅샷 테스트는 조금 다른 접근입니다.
응답의 전체 내용을 파일로 저장하고, 이후 실행 결과와 비교합니다. 의도한 변경이라면 스냅샷을 업데이트하고, 의도치 않은 변경이라면 버그로 처리합니다.
Python에서는 syrupy 라이브러리가 이 역할을 합니다. 실무에서는 두 방법을 함께 사용합니다.
골든 프롬프트 테스트로 핵심 기능의 품질을 지키고, 스냅샷 테스트로 전체 출력의 변화를 추적합니다. 프롬프트 엔지니어링은 과학이 아니라 공학입니다.
체계적인 테스트 없이는 프롬프트를 안심하고 수정할 수 없습니다. 주의할 점이 있습니다.
골든 데이터셋을 너무 많이 만들면 관리 비용이 증가합니다. 20~30개의 핵심 케이스로 시작해 점진적으로 늘리는 것이 좋습니다.
또한 키워드 검사만으로는 문맥의 미묘한 차이를 잡기 어려울 수 있으니, 중요한 기능에는 임베딩 유사도 검사를 추가하세요. 김개발 씨는 팀의 프롬프트 수정 워크플로우에 골든 테스트를 도입했습니다.
그 후로 프롬프트를 수정할 때마다 "아, 이전 기능이 망가지면 테스트가 바로 알려주는구나"라며 안심하게 되었습니다. 체계적인 테스트는 AI 에이전트 개발에서 품질의 지평선입니다.
골든 프롬프트와 스냅샷 테스트로 그 지평선을 높여보세요.
실전 팁
💡 - 골든 데이터셋은 20~30개 핵심 케이스로 시작해 점진적으로 확장하세요
- 프롬프트 수정 전후로 반드시 골든 테스트를 실행해 회귀를 방지하세요
- 이 카드뉴스는 "AI 에이전트 AI 엔지니어 되기 위한 로드맵" 코스의 3/16편입니다
3. 단위 테스트 vs 통합 테스트 분리
김개발 씨의 팀에 새로운 테스트 가이드라인이 발표되었습니다. "에이전트 코드는 단위 테스트와 통합 테스트를 분리해서 작성하세요." 김개발 씨는 고개를 갸웃했습니다.
"둘의 차이가 뭔데요? 그리고 왜 분리해야 하죠?"
단위 테스트는 개별 함수나 클래스를 독립적으로 검증하는 테스트입니다. 통합 테스트는 여러 컴포넌트가 함께 동작할 때의 전체 흐름을 검증합니다.
마치 자동차 공장에서 부품별 불량 검사(단위)를 하고, 조립 후 시승 주행(통합)을 하는 것과 같습니다.
다음 코드를 살펴봅시다.
# 단위 테스트 - 개별 함수를 독립적으로 검증합니다
def test_extract_intent():
prompt = "파이썬 파일을 읽어서 첫 줄을 출력해줘"
intent = extract_intent(prompt)
assert intent["action"] == "read_file"
assert intent["target"] == "file"
# 통합 테스트 - 전체 파이프라인을 검증합니다
def test_full_agent_pipeline():
"""LLM 호출까지 포함한 전체 흐름 검증"""
result = agent.run("README.md 파일 요약해줘")
# 핵심 포인트: 전체 파이프라인이 끝까지 동작하는지 확인합니다
assert result["status"] == "success"
assert "summary" in result
assert result["summary"] is not None
김개발 씨는 모든 테스트를 하나의 파일에 몰아넣는 습관이 있었습니다. 함수 테스트, API 테스트, 전체 에이전트 파이프라인 테스트가 한데 섞여 있었죠.
그러다 보니 테스트가 하나 실패하면 어디서 문제가 생겼는지 찾기가 어려웠습니다. 박시니어 씨가 프로젝트 구조도를 펼쳤습니다.
"테스트도 계층화해야 합니다. 단위 테스트와 통합 테스트를 분리하면 버그를 훨씬 빨리 찾을 수 있어요." 먼저 단위 테스트에 대해 이야기해볼까요?
단위 테스트는 자동차 부품 검사와 같습니다. 엔진, 브레이크, 조향 장치를 각각 따로 검사하듯, 코드의 개별 함수를 독립적으로 검증합니다.
핵심은 외부 의존성을 제거하는 것입니다. LLM API 호출, 파일 입출력, 네트워크 요청은 모두 모킹으로 대체합니다.
코드의 첫 번째 테스트 test_extract_intent를 보겠습니다. 이 함수는 사용자의 프롬프트에서 의도(intent)를 추출하는 순수 로직입니다.
LLM을 호출하지 않고, 문자열 처리만으로 동작합니다. 따라서 모킹 없이도 빠르고 안정적으로 테스트할 수 있습니다.
이런 단위 테스트의 장점은 속도입니다. 수백 개의 단위 테스트를 몇 초 안에 실행할 수 있습니다.
코드를 수정할 때마다 전체 테스트를 돌려도 기다리지 않아도 됩니다. 또한 실패하면 문제가 있는 정확한 함수를 가리키므로 디버깅이 쉽습니다.
그렇다면 통합 테스트는 언제 필요할까요? 통합 테스트는 자동차 조립 후 시승 주행과 같습니다.
부품이 개별적으로는 정상이어도, 조립했을 때 호환성 문제가 생길 수 있습니다. 마찬가지로, 개별 함수는 잘 동작해도 전체 파이프라인에서는 예상치 못한 문제가 발생할 수 있습니다.
코드의 두 번째 테스트 test_full_agent_pipeline을 보겠습니다. 이 테스트는 실제 LLM API를 호출하고, 파일을 읽고, 요약 결과를 반환하는 전체 흐름을 검증합니다.
모킹 없이 실제 환경에서 동작하죠. 속도는 느리지만, 실제 사용자가 겪는 경험과 가장 가깝습니다.
실무에서는 pytest의 마커(marker) 기능으로 두 테스트를 분리합니다. ```python @pytest.mark.unit def test_extract_intent(): ...
@pytest.mark.integration def test_full_agent_pipeline(): ... ``` 이렇게 하면 pytest -m unit으로 단위 테스트만, pytest -m integration으로 통합 테스트만 실행할 수 있습니다.
CI/CD 파이프라인에서는 커밋마다 단위 테스트를, 병합 시 통합 테스트를 실행하는 식으로 분리하는 것이 일반적입니다. 주의할 점이 있습니다.
단위 테스트만으로는 실제 환경의 문제를 잡을 수 없습니다. 반대로 통합 테스트만으로는 버그 원인을 좁히기 어렵습니다.
둘 다 필요합니다. 일반적으로 단위 테스트 70%, 통합 테스트 30% 비율이 권장됩니다.
김개발 씨는 팀의 테스트를 계층별로 분리했습니다. 그 결과 커밋마다 실행하는 단위 테스트는 5초 안에 끝나고, PR에 달리는 통합 테스트는 전체 시스템을 한 번에 검증합니다.
"이전에는 테스트가 실패하면 어디서부터 봐야 할지 몰랐는데, 이제는 바로 알 수 있어요." 테스트의 계층화는 개발 속도와 코드 품질을 동시에 높여주는 실전 기법입니다. 단위 테스트로 빠르게 잡고, 통합 테스트로 확실히 확인하세요.
실전 팁
💡 - 단위 테스트는 모킹으로 외부 의존성을 제거하고 속도를 높이세요
- pytest 마커를 활용해 단위/통합 테스트를 명확히 분리하세요
- 이 카드뉴스는 "AI 에이전트 AI 엔지니어 되기 위한 로드맵" 코스의 3/16편입니다
4. uv poetry 종속성 관리와 재현성 보장
김개발 씨가 회사 노트북에서는 잘 되던 코드가 집 노트북에서는 에러를 뱉었습니다. ModuleNotFoundError: No module named 'langchain' -- 분명히 설치했는데 말이죠.
"어, 이거 왜 이러지?" 종속성 관리의 중요성을 뼈저리게 느낀 순간이었습니다.
종속성 관리는 프로젝트가 사용하는 모든 패키지의 버전을 고정하고, 어디서든 동일한 환경을 재현하는 것입니다. uv는 Rust로 작성된 초고속 Python 패키지 매니저이고, Poetry는 의존성 충돌을 자동으로 해결해주는 관리 도구입니다.
마치 레시피에 정확한 재료 gram 수를 적어두는 것과 같습니다.
다음 코드를 살펴봅시다.
# pyproject.toml - Poetry 프로젝트 설정 파일
[tool.poetry.dependencies]
python = "^3.11"
langchain = "0.3.14"
openai = "1.58.1"
# uv를 사용한 빠른 환경 구성 (터미널 명령어)
# uv venv --python 3.11 가상환경 생성
# uv pip install -r requirements.txt 종속성 설치
# uv pip freeze > lock.txt 버전 고정
"AI 에이전트 AI 엔지니어 되기 위한 로드맵" 코스 세 번째 시간입니다. 지난 2화에서 배운 프로젝트 구조의 다음 단계로, 이번에는 종속성 관리와 재현성에 대해 다뤄보겠습니다.
김개발 씨의 사례는 개발자라면 한 번쯤 겪어봤을 문제입니다. 내 컴퓨터에서는 되는데 다른 컴퓨터에서는 안 되는 현상, 흔히 "내 컴퓨터에서는 잘 되는데" 증후군이라고 부릅니다.
박시니어 씨가 설명했습니다. "이건 버전 불일치 때문이에요.
회사 노트북에는 langchain 0.3.14가 설치되어 있는데, 집 노트북에는 최신 버전이 설치되어 있거나 아예 없는 거죠." 종속성 관리란 무엇일까요? 쉽게 비유하자면, 제과점의 레시피와 같습니다.
"버터 200g, 설탕 150g, 밀가루 300g"처럼 정확한 양을 명시해야 매번 같은 맛의 케이크를 만들 수 있습니다. "버터 적당히, 설탕 알맞게"라면 매번 다른 케이크가 나오겠죠.
Python 프로젝트도 마찬가지입니다. 코드의 pyproject.toml 파일을 보겠습니다.
이 파일은 프로젝트의 레시피입니다. python = "^3.11"은 Python 3.11 이상을 사용한다는 뜻이고, langchain = "0.3.14"는 정확히 그 버전을 사용한다는 뜻입니다.
버전을 고정하면 어떤 환경에서도 동일한 패키지가 설치됩니다. Poetry는 이 과정을 자동화합니다.
poetry install 명령어 하나로 모든 종속성을 올바른 버전으로 설치합니다. 패키지 간 충돌이 있으면 자동으로 해결해주고, poetry.lock 파일에 정확한 버전 정보를 기록합니다.
이 lock 파일을 프로젝트에 함께 커밋하면 팀원 모두가 동일한 환경을 공유할 수 있습니다. uv는 최근 주목받는 새로운 도구입니다.
Rust로 작성되어 Poetry보다 10~100배 빠른 설치 속도를 자랑합니다. uv pip install 명령어는 기존 pip와 호환되면서도 훨씬 빠르게 동작합니다.
대규모 프로젝트에서는 설치 시간 차이가 체감될 정도입니다. 재현성을 보장하는 핵심 원칙 세 가지를 기억하세요.
첫째, 버전을 고정하세요. langchain = "0.3.14"처럼 정확한 버전을 명시합니다.
^나 >= 같은 범위 지정자는 최소 버전만 보장하므로, 민감한 프로젝트에서는 정확한 버전을 사용하는 것이 안전합니다. 둘째, lock 파일을 커밋하세요.
poetry.lock이나 uv.lock 파일이 있어야 모든 팀원이 동일한 버전을 설치합니다. 이 파일을 .gitignore에 넣는 것은 흔한 실수입니다.
셋째, 가상환경을 사용하세요. 시스템 Python에 패키지를 직접 설치하면 프로젝트 간 충돌이 발생합니다.
uv venv나 poetry shell로 프로젝트별 가상환경을 만드세요. 주의할 점이 있습니다.
Poetry와 uv를 혼용해서 사용하지 마세요. 팀에서 하나의 도구를 선택해 일관되게 사용해야 합니다.
최신 프로젝트에서는 속도가 빠른 uv를 선호하는 추세입니다. 김개발 씨는 팀 프로젝트에 pyproject.toml과 uv.lock 파일을 추가하고, 모든 팀원에게 uv sync 명령어를 공유했습니다.
그 후로 "내 컴퓨터에서는 잘 되는데"라는 말은 사라졌습니다. 재현성은 프로덕션 배포의 첫 번째 전제 조건입니다.
종속성을 관리하는 것은 기술이 아니라 태도입니다.
실전 팁
💡 - lock 파일은 반드시 Git에 커밋해서 팀 전체가 동일한 환경을 공유하세요
- 민감한 프로젝트에서는 버전 범위 지정자 대신 정확한 버전을 사용하세요
- 이 카드뉴스는 "AI 에이전트 AI 엔지니어 되기 위한 로드맵" 코스의 3/16편입니다
5. 프로덕션 에이전트 앱 일반 함정 5가지
김개발 씨의 첫 프로덕션 배포 날이었습니다. 테스트에서는 완벽했던 에이전트가 실제 서비스에 투입되자마자 장애를 일으켰습니다.
모니터링 화면에 빨간불이 깜빡거렸고, 박시니어 씨가 급하게 달려왔습니다. "또 이 함정에 빠졌네."
프로덕션 함정은 개발 환경에서는 문제없이 동작하다가 실제 운영 환경에서만 나타나는 문제들입니다. 동시성 처리, 메모리 누수, 예외 누락, 타임아웃 미설정, 로깅 부재 등이 대표적입니다.
마치 연습장에서는 완벽한 연주도 무대에서는 악기 문제로 망칠 수 있는 것과 같습니다.
다음 코드를 살펴봅시다.
# 함정 1: 무제한 재시도 -- 서버를 마비시킬 수 있습니다
def bad_retry():
for attempt in range(999): # 함정: 무제한 반복
try:
return llm_api.call(prompt)
except Exception:
continue # 함정: 예외를 무시하고 계속 시도
# 해결: 지수 백오프와 최대 재시도 횟수 제한
def good_retry():
max_retries = 3
for attempt in range(max_retries):
try:
return llm_api.call(prompt)
except RateLimitError:
if attempt == max_retries - 1:
raise # 마지막 시도에서는 예외를 전파합니다
time.sleep(2 ** attempt) # 지수 백오프
김개발 씨의 에이전트가 장애를 일으킨 원인은 무엇이었을까요? 박시니어 씨가 서버 로그를 열었습니다.
"봐요, 여기. API 호출이 실패했는데 무제한으로 재시도하고 있어요.
서버 메모리가 꽉 찼고, 결국 다른 서비스까지 영향을 받았습니다." 이것이 프로덕션 함정의 전형적인 사례입니다. 개발 환경에서는 API가 잘 응답하니까 문제가 없습니다.
하지만 프로덕션에서는 트래픽이 몰리면 API가 가끔 실패합니다. 이때 무제한 재시도 로직이 돌아가면 서버가 순식간에 자원 고갈에 빠집니다.
프로덕션 에이전트 앱에서 흔히 겪는 함정 5가지를 정리해보겠습니다. 첫 번째, 무제한 재시도입니다.
앞서 본 코드처럼 for 루프 안에서 예외를 무시하며 계속 시도하는 패턴입니다. 개발 중에는 API가 거의 실패하지 않아서 이 버그를 발견하기 어렵습니다.
해결책은 최대 재시도 횟수를 설정하고, 지수 백오프(점점 간격을 늘리는 방식)를 사용하는 것입니다. 두 번째, 글로벌 상태 공유입니다.
여러 사용자의 요청이 하나의 전역 변수를 공유하면 데이터가 섞입니다. 사용자 A의 대화가 사용자 B의 화면에 나타나는 식의 버그죠.
에이전트는 요청마다 독립적인 상태를 가져야 합니다. 세 번째, 리소스 누수입니다.
파일 핸들을 열고 닫지 않거나, 데이터베이스 커넥션을 반환하지 않으면 자원이 고갈됩니다. Python의 with 문이나 컨텍스트 매니저를 사용하면 이 문제를 예방할 수 있습니다.
네 번째, 타임아웃 미설정입니다. LLM API 호출에 타임아웃을 설정하지 않으면, 응답이 없을 때 프로세스가 영원히 기다립니다.
httpx 라이브러리의 timeout 파라미터나 requests의 timeout 인자를 반드시 설정하세요. 다섯 번째, 로깅 부재입니다.
에러가 발생했을 때 로그가 없으면 원인을 파악할 수 없습니다. 프로덕션에서는 print()가 아닌 구조화된 로깅을 사용해야 합니다.
어떤 요청에서, 어떤 단계에서, 어떤 에러가 발생했는지 기록해야 합니다. 코드에서 bad_retry와 good_retry를 비교해보세요.
나쁜 예는 무제한으로 재시도하고 모든 예외를 삼키고 있습니다. 좋은 예는 최대 3번만 시도하고, RateLimitError만 처리하며, 지수 백오프로 간격을 늘립니다.
마지막 시도에서는 예외를 다시 발생시켜 상위에서 처리하도록 합니다. 실무에서 이 함정들을 방지하려면 체크리스트를 만드세요.
배포 전에 "재시도 횟수 제한?", "글로벌 상태 없음?", "리소스 정리?", "타임아웃 설정?", "로그 남김?" 다섯 가지를 확인하는 습관을 들이면 됩니다. 김개발 씨는 장애 원인을 분석하고, 팀의 배포 체크리스트에 이 5가지 함정을 추가했습니다.
그 후로 프로덕션 장애가 크게 줄어들었습니다. 프로덕션은 개발 환경과 다릅니다.
예상치 못한 상황을 가정하고 방어 코드를 작성하는 것, 그것이 프로덕션급 코드의 기본 자세입니다.
실전 팁
💡 - 모든 외부 API 호출에 타임아웃과 최대 재시도 횟수를 반드시 설정하세요
- 배포 전 체크리스트로 5가지 함정(재시도, 상태, 리소스, 타임아웃, 로깅)을 점검하세요
- 이 카드뉴스는 "AI 에이전트 AI 엔지니어 되기 위한 로드맵" 코스의 3/16편입니다
6. 무제한 재시도 글로벌 상태 리소스 누수 방지
장애 복구 후 김개발 씨는 팀의 코드를 전면 점검했습니다. 그리고 소름이 돋았습니다.
거의 모든 에이전트 코드에 무제한 재시도, 글로벌 변수, 닫히지 않은 리소스가 있었습니다. "이게 다 시한폭탄이었는데..."
무제한 재시도는 서버 자원을 고갈시키고, 글로벌 상태는 사용자 간 데이터를 섞이게 하며, 리소스 누수는 메모리와 커넥션을 고갈시킵니다. 이 세 가지는 프로덕션 장애의 삼대장입니다.
마치 수도꼭지를 틀어놓고, 물을 마시며, 하수구를 막는 것과 같습니다.
다음 코드를 살펴봅시다.
import threading
# 해결: 요청별 독립 상태와 리소스 관리
class AgentSession:
"""사용자 요청마다 독립적인 세션을 생성합니다"""
def __init__(self, user_id):
self.user_id = user_id # 요청별로 독립적인 상태
self.history = [] # 다른 사용자와 섞이지 않습니다
self._db_conn = None # 리소스 추적
def process(self, prompt):
try:
response = self._call_llm_with_retry(prompt)
self.history.append(response)
return response
finally:
self.cleanup() # 핵심: 반드시 정리합니다
def cleanup(self):
if self._db_conn:
self._db_conn.close()
self._db_conn = None
박시니어 씨가 화이트보드에 세 가지를 크게 적었습니다. 무제한 재시도, 글로벌 상태, 리소스 누수.
이 세 가지를 방지하는 것이 프로덕션 코드의 기본이라고 강조했습니다. 먼저 글로벌 상태에 대해 자세히 살펴보겠습니다.
Python에서 모듈 수준에 변수를 선언하면 글로벌 상태가 됩니다. 예를 들어 conversation_history = []를 모듈 최상단에 선언하면, 모든 사용자가 이 리스트를 공유합니다.
사용자 A가 "나는 파이썬을 배우고 있어"라고 말하면, 사용자 B의 대화에도 이 문장이 섞여 들어갈 수 있습니다. 쉽게 비유하자면, 식당 주방에 하나의 주문서만 있는 것과 같습니다.
테이블 1의 주문과 테이블 2의 주문이 같은 종이에 적히면 요리사가 혼란스럽겠죠. 각 테이블마다 별도의 주문서가 필요합니다.
에이전트도 요청마다 독립적인 세션이 필요합니다. 코드의 AgentSession 클래스를 보겠습니다.
__init__ 메서드에서 user_id와 history를 인스턴스 변수로 초기화합니다. 이렇게 하면 각 사용자마다 고유한 상태를 가집니다.
self.history는 인스턴스에 종속되므로 다른 사용자와 섞이지 않습니다. 다음으로 리소스 누수를 살펴보겠습니다.
데이터베이스 커넥션, 파일 핸들, 네트워크 소켓은 사용 후 반드시 정리해야 합니다. 정리하지 않으면 운영체제가 허용하는 한계에 도달할 때까지 자원이 누적됩니다.
Python에서는 가비지 컬렉터가 메모리를 자동으로 정리하지만, 파일 핸들이나 네트워크 커넥션은 명시적으로 닫아야 합니다. cleanup 메서드에서 데이터베이스 커넥션을 닫는 것을 볼 수 있습니다.
더 중요한 것은 finally 블록입니다. try 블록에서 예외가 발생하더라도 finally 블록은 항상 실행됩니다.
따라서 에러가 나도 리소스가 정리됩니다. 이 패턴은 Python에서 가장 중요한 방어 기법 중 하나입니다.
재시도 로직도 이 클래스 내부에 캡슐화되어 있습니다. _call_llm_with_retry 메서드(코드에는 표시되지 않지만 개념적으로 존재)에서 최대 재시도 횟수를 제한하고, 지수 백오프를 적용합니다.
세션 내부에서 관리하므로 글로벌하게 영향을 주지 않습니다. 실무에서는 이 패턴을 더 발전시켜 컨텍스트 매니저로 구현하기도 합니다.
python with AgentSession(user_id="user_123") as session: result = session.process("파이썬 리스트 정렬해줘") # with 블록을 벗어나면 자동으로 cleanup()이 호출됩니다 __enter__와 __exit__ 메서드를 구현하면 with 문과 함께 사용할 수 있습니다. 코드가 더 깔끔해지고, 리소스 정리를 잊을 일도 없습니다.
주의할 점이 있습니다. 세션 객체를 전역 캐시에 저장하지 마세요.
사용자의 세션을 메모리에 오래 보관하면 메모리 누수로 이어집니다. Redis 같은 외부 저장소를 사용하거나, 요청이 끝나면 즉시 정리하는 것이 좋습니다.
김개발 씨는 글로벌 변수를 모두 인스턴스 변수로 변경하고, 파일 처리에 with 문을 적용했습니다. 또한 모든 외부 API 호출에 타임아웃과 재시도 제한을 추가했습니다.
코드 리뷰에서 박시니어 씨가 고개를 끄덕였습니다. "이제 프로덕션에 배포해도 되겠네요." 독립적인 상태, 명시적인 리소스 정리, 제한된 재시도.
이 세 가지는 프로덕션 코드의 방어막입니다.
실전 팁
💡 - 글로벌 변수 대신 클래스 인스턴스로 요청별 상태를 관리하세요
- 파일, DB 커넥션 등은 with 문이나 finally로 반드시 정리하세요
- 이 카드뉴스는 "AI 에이전트 AI 엔지니어 되기 위한 로드맵" 코스의 3/16편입니다
7. 관찰 가능성을 위한 구조화된 로깅
장애가 해결된 후, 김개발 씨는 또 다른 문제를 발견했습니다. 서버 로그가 너무 많고 너무 지저분해서 에러 원인을 찾는 데 시간이 오래 걸렸습니다.
print("에러 발생!") 같은 로그는 도움이 되지 않았습니다. 박시니어 씨가 말했습니다.
"로그도 코드입니다. 제대로 설계해야 해요."
구조화된 로깅은 로그를 단순한 텍스트가 아니라 JSON 형식의 구조화된 데이터로 기록하는 방법입니다. 로그를 필터링, 검색, 집계할 수 있어 프로덕션에서 문제를 빠르게 파악할 수 있습니다.
마치 도서관의 책을 제목, 저자, 출판년도별로 분류해두는 것과 같습니다.
다음 코드를 살펴봅시다.
import logging
import json
from datetime import datetime
class JSONFormatter(logging.Formatter):
"""로그를 JSON 형식으로 출력합니다"""
def format(self, record):
log_entry = {
"timestamp": datetime.utcnow().isoformat(),
"level": record.levelname,
"message": record.getMessage(),
"module": record.module,
# 핵심: 요청 추적을 위한 ID를 포함합니다
"request_id": getattr(record, "request_id", None),
}
return json.dumps(log_entry, ensure_ascii=False)
def setup_logging():
logger = logging.getLogger("agent")
handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logger.addHandler(handler)
logger.setLevel(logging.INFO)
return logger
김개발 씨는 장애를 조사하면서 로그의 중요성을 뼈저리게 느꼈습니다. 서버에 수만 줄의 로그가 쌓여 있었는데, 그중 진짜 문제를 가리키는 로그를 찾는 것이 너무 어려웠습니다.
print()로 찍은 로그는 타임스탬프도 없고, 어떤 요청에서 발생한 에러인지도 알 수 없었습니다. 박시니어 씨가 말했습니다.
"프로덕션에서 로그는 이야기입니다. 사용자의 요청이 어떤 경로를 거쳤고, 어디서 문제가 생겼는지 순서대로 읽을 수 있어야 합니다." 구조화된 로깅이란 무엇일까요?
쉽게 비유하자면, 도서관 사서가 책을 분류하는 것과 같습니다. "이 책 재밌어요"라는 한 줄 평만 남기는 것이 아니라, "책 제목: 파이썬 코딩, 저자: 김개발, 장르: 컴퓨터, 평점: 5점"처럼 구조화된 정보를 기록하는 것입니다.
나중에 "장르가 컴퓨터인 책 중 평점 4점 이상"을 검색할 수 있죠. 구조화된 로그도 마찬가지입니다.
코드의 JSONFormatter 클래스를 살펴보겠습니다. 이 클래스는 Python의 내장 logging 모듈의 Formatter를 상속받아, 로그를 JSON 형식으로 변환합니다.
각 로그에는 타임스탬프, 로그 레벨(INFO, WARNING, ERROR 등), 메시지, 모듈 이름, 그리고 request_id가 포함됩니다. request_id가 가장 중요합니다.
사용자의 요청이 에이전트를 거치고, LLM API를 호출하고, 데이터베이스에 접근하는 동안, 모든 로그에 같은 request_id가 포함됩니다. 이렇게 하면 하나의 요청이 남긴 모든 로그를 한 번에 추적할 수 있습니다.
python logger.info("LLM API 호출 시작", extra={"request_id": "req_12345"}) logger.info("응답 수신", extra={"request_id": "req_12345"}) logger.error("타임아웃 발생", extra={"request_id": "req_12345"}) 이렇게 로그를 남기면, req_12345로 필터링해서 하나의 요청이 어떤 과정을 거쳤는지 전체 흐름을 볼 수 있습니다. 장애 조사 시간이 획기적으로 줄어듭니다.
setup_logging 함수는 이 포매터를 로거에 등록합니다. 프로젝트의 진입점(main.py 같은 곳)에서 이 함수를 한 번만 호출하면, 전체 프로젝트에서 구조화된 로그를 사용할 수 있습니다.
실무에서는 structlog 라이브러리도 많이 사용합니다. Python의 내장 logging보다 더 간결한 API로 구조화된 로그를 남길 수 있습니다.
python import structlog logger = structlog.get_logger() logger.info("agent_call", request_id="req_123", prompt="파이썬 정렬", latency_ms=230) 출력 결과는 {"event": "agent_call", "request_id": "req_123", "prompt": "파이썬 정렬", "latency_ms": 230, "timestamp": "..."} 형태의 JSON입니다. ELK 스택(Elasticsearch, Logstash, Kibana)이나 Grafana Loki 같은 로그 수집 시스템에 바로 연동할 수 있습니다.
주의할 점이 있습니다. 민감한 정보를 로그에 남기지 마세요.
사용자 비밀번호, API 키, 개인정보는 로그에 절대 포함하면 안 됩니다. 또한 로그 레벨을 적절히 사용하세요.
DEBUG는 개발 환경에서만, INFO는 일반 동작 기록에, ERROR는 장애 조사에 필요한 정보에 사용합니다. 김개발 씨는 프로젝트 전체에 구조화된 로깅을 도입했습니다.
그 후 장애가 발생했을 때, request_id로 필터링해서 30초 만에 원인을 파악할 수 있었습니다. "이전에는 로그를 뒤지느라 반나절이 걸렸는데..." 로그는 코드가 남기는 일기장입니다.
구조화된 로그는 읽기 쉽고, 검색하기 쉽고, 이야기가 술술 읽히는 일기장입니다.
실전 팁
💡 - 모든 로그에 request_id를 포함해 요청 추적이 가능하게 하세요
- 민감한 정보(API 키, 비밀번호, 개인정보)는 절대 로그에 남기지 마세요
- 다음 카드뉴스에서는 "LLM 기본 사항 - 토큰, 컨텍스트, 프롬프트 설계"를 다룹니다. 이 카드뉴스는 "AI 에이전트 AI 엔지니어 되기 위한 로드맵" 코스의 3/16편입니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
LLM 기본 사항 토큰 컨텍스트 프롬프트 설계 완벽 가이드
LLM을 다루는 데 필수적인 토큰, 컨텍스트 윈도우, 프롬프트 설계의 핵심 개념을 배웁니다. 에이전트 AI 엔지니어로 성장하기 위해 반드시 알아야 할 기초를 다집니다.
Python 기본 사항 프로젝트 구조와 타이핑 완벽 가이드
AI 에이전트 엔지니어를 위한 Python 프로젝트 구조 설계부터 타입 힌팅까지, 초보자가 반드시 알아야 할 핵심 기초를 다룹니다. 깔끔한 프로젝트 구조와 타입 안전성으로 생산성을 높여보세요.
에이전트 AI 엔지니어 로드맵 소개 및 학습 방법
2026년 에이전트 AI 엔지니어가 되기 위한 완벽 로드맵을 소개합니다. Python 기초부터 고급 에이전트 아키텍처, RAG 시스템, 다중 에이전트까지 16개 소주제로 구성된 코스의 전체 흐름과 학습 방법을 안내합니다.
Day 3 문자 단위 토크나이저 만들기
LLM이 텍스트를 이해하려면 먼저 문자를 숫자로 변환해야 합니다. 텍스트를 쪼개고, 각 조각에 번호를 매기고, 다시 원래 텍스트로 복원하는 토크나이저를 직접 만들어봅니다.
Day 1 언어모델의 목표 이해
LLM 바닥부터 만들기 코스의 첫 번째 날입니다. 언어모델이 무엇인지, next token prediction이 어떻게 작동하는지, 그리고 pretraining이 왜 확률 모델링인지를 이해합니다. 앞으로 30일간 만들게 될 작은 GPT의 기초 개념을 다집니다.