AI 에이전트 시대의 테스트: QA가 붕괴되는 것을 어떻게 막았는가

발행: (2026년 1월 12일 오후 04:11 GMT+9)
11 분 소요
원문: Dev.to

Source: Dev.to

AI 에이전트가 내 개발 속도를 하룻밤 사이에 바꾸어 놓았습니다. 이제 하루에 한 주 동안 작성하던 코드보다 더 많은 코드를 배포할 수 있게 되었고, 처음엔 멋져 보였지만 작은 엣지 케이스 하나가 전체 흐름을 무너뜨릴 때는 상황이 달라집니다.

이런 속도에서는 QA가 경쟁력의 핵심이 되거나 끊임없는 화재 훈련이 됩니다. 저는 첫 번째 옵션을 선택했고, d:\Coding\Company\Ochestrator 안에서 코드 양에 비례해 확장 가능한 소수의 테스트 설계 기법을 중심으로 테스트 방식을 재구성했습니다:

  • TDD
  • EP‑BVA (동등 분할 + 경계값 분석)
  • Pairwise (조합 테스트)
  • 상태 전이 테스트

Testing Toolbox Diagram

왜 나는 “테스트 설계”가 필요했는지, 단순히 “더 많은 테스트”가 아니라

코드 양이 늘어날수록 문제는 커버리지만이 아니다. 실제 문제는 가능한 입력과 상태의 공간이 내 시간보다 더 빠르게 증가한다는 것이다.

그래서 나는 더 이상 묻지 않았다:

  • “이 함수에 대한 테스트를 작성했는가?”

그리고 이렇게 물었다:

  • “실제로 실패 표면을 나타내는 테스트 케이스를 선택했는가?”

그러한 사고방식은 나를 구조화된 테스트‑설계 기법으로 이끌었다.

TDD: 처음부터 테스트 가능성 설계

원칙: TDD(테스트 주도 개발)는 전통적인 “코드를 작성하고, 그 다음 테스트한다” 워크플로우를 뒤집습니다. 이는 Red‑Green‑Refactor 사이클을 따릅니다:

PhaseDescription
Red새로운 요구사항에 대한 테스트를 작성하고 실패하도록 합니다. 이는 테스트가 실제로 무언가를 검증하고 있으며, 요구사항이 아직 충족되지 않았음을 확인해 줍니다.
Green테스트를 통과시키기 위해 최소한의 코드를 작성합니다. 이 단계에서는 “과도한 설계”를 피합니다.
Refactor테스트가 여전히 통과하는지 확인하면서 코드를 정리합니다.

Orchestrator에서:
AI 에이전트가 복잡한 비즈니스 로직을 빠르게 생성할 수 있기 때문에, 나는 TDD를 사용하여 로직이 설계 단계부터 테스트 가능하도록 했습니다. 예를 들어, 우리 Temporal 워크플로우용 RetryPolicy를 구현할 때, 정책 로직을 한 줄도 작성하기 전에 지수 백오프에 대한 테스트 케이스부터 시작했습니다.

# Simplified TDD Example for Retry Logic
def test_retry_interval_calculation():
    policy = ExponentialRetry(base_delay=1.0, max_delay=10.0)
    # 1st attempt: 1.0 s
    assert policy.get_delay(attempt=1) == 1.0
    # 2nd attempt: 2.0 s
    assert policy.get_delay(attempt=2) == 2.0
    # Capped at 10.0 s
    assert policy.get_delay(attempt=10) == 10.0

이것은 지연 계산을 재시도 실행과 분리하도록 강제했으며, 시스템을 모듈화하고 견고하게 만들었습니다.

EP‑BVA: 수학적 선택을 통한 효율성

원칙

  • 동등 분할 (Equivalence Partitioning, EP): 입력 영역을 시스템이 동일하게 동작할 것으로 기대되는 그룹(분할)으로 나눕니다. 각 그룹에서 대표값 하나만 테스트합니다.
  • 경계값 분석 (Boundary Value Analysis, BVA): 버그는 종종 이러한 분할의 “경계”에 숨어 있습니다. 정확한 경계값과 그 안·밖의 값을 테스트합니다.

Orchestrator에서:
사용자가 업로드하는 파일을 처리할 때, 우리는 엄격한 크기 제한을 두고 있습니다(예: 1 MB ~ 10 MB).

분할설명
Invalid10 MB

BVA 포인트: 0.99 MB, 1.0 MB, 1.01 MB, 9.99 MB, 10.0 MB, 10.01 MB.

제가 실제로 적용한 중요한 사례는 bcrypt의 72바이트 제한입니다. 많은 개발자들이 bcrypt가 72번째 바이트 이후의 문자를 무시한다는 사실을 인식하지 못합니다.

# apps/backend/tests/test_auth_service.py
def test_password_length_boundaries(self, auth_service):
    # Boundary: 72 bytes
    p72 = "a" * 72
    h72 = auth_service.get_password_hash(p72)

    # Just above the boundary: 73 bytes
    p73 = p72 + "b"
    # Bcrypt will treat p73 the same as p72 if only the first 72 bytes are used
    assert auth_service.verify_password(p73, h72) is True

이러한 특정 포인트에 집중함으로써 수백 개의 잠재적 테스트 케이스를 단 6‑10개의 매우 효과적인 테스트로 줄일 수 있었습니다.

Pairwise: 조합 폭발 억제

원칙: 대부분의 버그는 단일 입력 파라미터이거나 파라미터 간의 상호작용 때문에 발생합니다. Pairwise Testing은 모든 가능한 입력 파라미터 쌍을 최소 한 번씩 테스트하도록 보장하는 조합 방법입니다. 이는 테스트 케이스 수를 크게 줄이면서 높은 결함 탐지율을 유지합니다.

Orchestrator에서:
우리 AI 추론 엔진은 여러 구성 축을 가지고 있습니다:

AxisOptions
Execution ProviderCUDA, CPU, OpenVINO
Model SizeSmall, Medium, Large
QuantizationINT8, FP16, FP32
Async ModeEnabled, Disabled

전체 조합: (3 \times 3 \times 3 \times 2 = 54) 경우.

Pairwise를 사용하면 두 설정 간의 모든 상호작용을 대략 12‑15 케이스로 커버할 수 있습니다.

# Using allpairspy to generate the matrix
from allpairspy import AllPairs

parameters = [
    ["CUDA", "CPU", "OpenVINO"],
    ["Small", "Medium", "Large"],
    ["INT8", "FP16", "FP32"],
    ["Enabled", "Disabled"]
]

for i, combo in enumerate(AllPairs(parameters)):
    print(f"Test Case {i}: {combo}")

이렇게 하면 매 PR마다 전체 54‑케이스 스위트를 실행하지 않아도 하드웨어 호환성 매트릭스에 대한 높은 신뢰도를 유지할 수 있습니다.

상태 전이 테스트: 프로세스의 생명 주기 매핑

원칙: 이 기법은 시스템의 동작이 현재 상태와 발생하는 이벤트에 따라 달라질 때 사용됩니다. 상태 전이 다이어그램을 그려서 다음을 확인합니다:

  • 모든 유효한 전이가 가능한지.
  • 모든 무효한 전이가 거부되거나 정상적으로 처리되는지.

Orchestrator에서:
단순화된 주문 처리 워크플로를 Created → Approved → Shipped → Delivered 상태로 가정합니다. approve, ship, deliver, cancel 같은 이벤트가 전이를 트리거합니다. 각 상태/이벤트 쌍을 열거함으로써, 정상 흐름과 오류 처리(예: 아직 Created 상태인 주문을 ship 하려는 경우)를 검증하는 간결한 테스트 매트릭스를 생성합니다.

# Example state‑transition test matrix
states = ["Created", "Approved", "Shipped", "Delivered", "Cancelled"]
events = {
    "approve": {"Created": "Approved"},
    "ship":    {"Approved": "Shipped"},
    "deliver": {"Shipped": "Delivered"},
    "cancel": {"Created": "Cancelled", "Approved": "Cancelled"}
}

def test_state_transitions():
    for event, mapping in events.items():
        for src, dst in mapping.items():
            assert transition(src, event) == dst
        # Verify invalid transitions raise an error
        invalid_src = set(states) - set(mapping.keys())
        for src in invalid_src:
            with pytest.raises(InvalidTransition):
                transition(src, event)

상태 공간을 체계적으로 커버함으로써, 특정 순서의 동작 후에만 나타나는 버그를 잡을 수 있습니다—이는 순수 단위 테스트 커버리지만으로는 놓치기 쉬운 부분입니다.

상태 전이에 대한 부정 테스트

  • 시스템이 올바른 최종 상태에 도달하는지 확인합니다.

Orchestrator에서

KYC(고객 신원 확인) 검증 워크플로는 복잡한 상태 머신입니다. 사용자의 문서는 다음과 같이 이동합니다:

PENDING → UPLOADING → PROCESSING → VERIFIED or REJECTED

나는 REJECTED 문서가 PROCESSING을 다시 거치지 않고 VERIFIED로 바로 전이될 수 없도록 테스트를 구현했습니다.

# apps/backend/tests/test_integration_kyc_workflow.py
def test_invalid_state_transitions(workflow_engine):
    workflow_engine.set_state(ImageStatus.REJECTED)

    # This should be blocked by the business logic
    with pytest.raises(IllegalStateError):
        workflow_engine.transition_to(ImageStatus.VERIFIED)

이는 논리를 “우회”하려는 AI 에이전트에 대해 매우 중요합니다. 상태 머신을 엄격히 테스트함으로써 전체 비즈니스 프로세스의 무결성을 보장합니다.

결론

AI‑에이전트 시대에 코드는 저렴하지만 신뢰는 그렇지 않다.

내 QA가 무너지지 않게 한 것은 더 많은 테스트를 작성하는 것이 아니라 규모에 맞는 테스트 설계 기법을 채택한 것이다:

  • TDD: 빠른 피드백과 안전한 리팩터링을 위해
  • EP‑BVA: 경계 사례를 체계화하기 위해
  • Pairwise: 조합 폭증을 억제하기 위해
  • State Transition Testing: 실제 워크플로우를 검증하기 위해

코드 양이 계속 증가함에 따라 내가 계속 사용할 테스트 도구들이다.

Back to Blog

관련 글

더 보기 »

안녕, 뉴비 여기요.

안녕! 나는 다시 S.T.E.M. 분야로 돌아가고 있어. 에너지 시스템, 과학, 기술, 공학, 그리고 수학을 배우는 것을 즐겨. 내가 진행하고 있는 프로젝트 중 하나는...