5.8.1 Python 환경: Pytest와 LLM 전용 플러그인 활용 패턴

5.8.1 Python 환경: Pytest와 LLM 전용 플러그인 활용 패턴

데이터 사이언스와 인공지능 분야의 사실상 표준(De facto standard) 언어인 Python은, 거대 언어 모델(LLM) 기반의 애플리케이션을 검증하기 위한 가장 거대하고 활발한 테스팅 생태계를 보유하고 있다. 그 중심에는 간결함과 확장성의 극치를 보여주는 강력한 테스팅 프레임워크인 Pytest가 자리 잡고 있다.

Pytest의 뛰어난 모듈화 능력과 써드파티(Third-party) 플러그인들을 결합하면, 비결정적인 AI 모델을 우아하게 통제하는 결정론적 오라클 시스템을 매우 적은 양의 코드로 구축할 수 있다.

1. conftest.py를 활용한 프롬프트 및 의존성 주입(Dependency Injection)

LLM 테스트의 핵심은 테스트 코드가 프롬프트 템플릿과 모델 연결(Connection) 객체에 강하게 결합되지 않도록 분리하는 것이다. Pytest의 conftest.py 파일과 @pytest.fixture 데코레이터를 활용하면, 전역적으로 사용 가능한 테스트 의존성을 깨끗하게 주입할 수 있다.

# conftest.py
import pytest
from my_ai_app import LlmClient, PromptTemplate

@pytest.fixture(scope="session")
def mock_llm_client():
    # 실제 비용이 발생하는 프로덕션 API 대신 테스트용 고정 클라이언트(Mock) 주입
    return LlmClient(api_key="TEST_DUMMY_KEY", mode="strict_json")

@pytest.fixture
def refund_prompt_template():
    return PromptTemplate.load("templates/refund_v2.prompt")

이렇게 설계하면 개별 단위 테스트 파일에서는 모듈의 내부 구현을 신경 쓸 필요 없이, 주입받은 픽스처를 이용해 오직 오라클의 조건(Assertion)을 검증하는 데만 집중할 수 있다.

2. pytest-recording (vcrpy) 플러그인을 통한 VCR 패턴의 안착

LLM 테스트가 외부 API를 직접 호출하면, 막대한 요금 과금과 네트워크 지연(Latency), 그리고 응답의 일관성 붕괴라는 세 가지 재앙에 직면한다. 이를 해결하기 위해 Python 진영에서는 HTTP 통신 내용을 카세트테이프처럼 녹화하고 재생하는 pytest-recording 플러그인을 활용한다.

# test_refund_logic.py
import pytest

@pytest.mark.vcr()
def test_llm_refund_approval(mock_llm_client, refund_prompt_template):
    # 최초 실행 시 외부 API로 통신하여 응답을 tests/cassettes 디렉토리에 YAML로 저장.
    # 두 번째 실행부터는 네트워크 소켓을 차단하고 0.01초 만에 YAML의 응답을 그대로 재현함.
    user_input = "어제 구매한 신발 사이즈가 안 맞아서 환불 요청합니다."
    
    response = mock_llm_client.generate(refund_prompt_template, user_input)
    assert response.status == "APPROVED"

이 플러그인 하나만으로도 CI/CD 서버에서의 테스트 실행 시간을 수백 배 단축하고, 결정론적 오라클이 언제나 고정된(Deterministic) 텍스트를 상대로 일관성 있는 채점을 할 수 있게 보장한다.

3. pytest-asyncio를 이용한 I/O 바운드 병목 타파

수십 개의 골든 샘플(Golden Sample)을 처리할 때, 동기적(Synchronous)인 LLM 호출 방식은 시스템을 치명적인 병목 상태로 몰아넣는다. Python의 async/await 문법과 최신 LLM 라이브러리(비동기 호환)를 결합하고, 테스트 단에서 pytest-asyncio 플러그인을 가동하면 유닛 테스트를 완벽한 병렬 I/O로 구동할 수 있다.

import pytest

# @pytest.mark.asyncio 데코레이터를 통해 비동기 테스트 케이스로 전환
@pytest.mark.asyncio
async def test_batch_prompt_processing(async_llm_client):
    inputs = ["안녕", "환불해 줘", "담당자 연결해"]
    
    # 세 개의 프롬프트를 동시에 모델로 쏘아 보내고, 응답을 병렬로 기다림
    responses = await async_llm_client.batch_generate(inputs)
    
    assert len(responses) == 3
    assert responses[1].intent == "REFUND_REQUEST"

Pytest는 단순한 테스팅 도구가 아니다. 철저하게 분리된 픽스처 관리, VCR을 통한 통신망 격리, 비동기 플러그인을 통한 연산의 극대화를 하나로 묶어내는 오케스트레이터(Orchestrator)이며, Python으로 AI 시스템을 지탱하려는 모든 엔지니어가 가장 먼저 마스터해야 할 궁극의 무기다.