본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 26. 10:17

AI가 환각(Hallucination) 없이 실제로 유용한 단위 테스트를 작성하게 만든 방법

요약

LLM을 활용해 환각 현상 없이 유용한 단위 테스트를 작성하는 효과적인 워크플로우를 소개합니다. 단순 프롬프팅의 한계를 넘어, 함수 시그니처와 독스트링을 활용한 반복적이고 검증 가능한 접근 방식을 제안합니다.

핵심 포인트

  • 단순 프롬프팅이나 CoT 방식은 테스트 코드의 환각을 완전히 막지 못함
  • LLM은 정답을 자동 검증할 수 있는 제한된 환경에서 가장 강력함
  • 함수 시그니처와 독스트링을 추출하여 컨텍스트를 좁게 유지하는 것이 핵심
  • 한 번에 하나의 함수만 제공하여 AI의 집중도를 높여야 함

고백하자면 이렇습니다. 저는 레거시 코드베이스(legacy codebase)를 위한 단위 테스트(unit tests)를 작성하는 데 사흘을 보냈고, 그 모든 순간이 정말 싫었습니다. 그래서 ChatGPT가 그럴싸한 코드를 작성하기 시작했을 때, 저는 "좋아, 이 귀찮은 일은 AI에게 떠넘기자"라고 생각했습니다.

하지만 LLM에게 "이 모듈에 대한 테스트를 작성해줘"라고 요청하는 것이 아름다운 헛소리(nonsense)를 만드는 지름길이라는 것을 곧 깨달았습니다. 즉, 아무것도 검증하지 못하면서 통과하는 테스트, 존재하지 않는 것을 테스트하는 테스트, 혹은 실제 로직을 완전히 건너뛰는 테스트를 말합니다. 수많은 시행착오 끝에, 저는 실제로 유용하고 정확한 단위 테스트를 제공하는 워크플로우(workflow)를 찾아냈습니다. 완벽하지는 않지만, 제 시간을 몇 시간이나 아껴줄 만큼 충분히 유용합니다.

이것은 특정 AI 도구에 관한 이야기가 아닙니다(물론 제가 사용한 도구 하나를 언급하긴 하겠지만요). 핵심은 접근 방식에 관한 것입니다. AI에게 무엇을 제공할 것인지, 출력을 어떻게 검증할 것인지, 그리고 언제 그것을 버려야 하는지에 대한 것입니다.

문제점: 환각(Hallucinated)된 테스트

저는 CSV 데이터를 변환하는 Python API를 작업하고 있었습니다. 코드에는 다음과 같은 함수가 있었습니다:

def parse_row(row: dict, mapping: dict) -> dict:
    result = {}
    for source_field, target_field in mapping.items():
...

저는 LLM에게 "parse_row에 대한 포괄적인 단위 테스트를 작성해줘"라고 요청했습니다. AI는 다음과 같이 응답했습니다:

def test_parse_row_basic():
    row = {'name': 'Alice', 'age': '30 '}
    mapping = {'name': 'full_name', 'age': 'age'}
...

이것은 합리적으로 보입니다. 하지만 AI는 다음과 같은 코드도 생성했습니다:

def test_parse_row_with_default():
    row = {}
    mapping = {'name': 'full_name'}
...

제 함수에는 기본값(default) 로직이 없습니다. AI가 그것을 지어낸 것입니다. 이를 30개의 테스트 케이스로 곱하면, 잘못된 확신을 주는 테스트 스위트(test suite)를 갖게 되는 셈입니다.

제가 시도했던 것들 (효과가 없었던 것들)

  1. 더 나은 프롬프트 (Better prompts) – "존재하는 것만 테스트하고, 동작을 추가하지 마"라고 요청했습니다. 여전히 환각(hallucinations)이 발생했습니다.
  2. 예시를 포함한 원샷 프롬프팅 (One-shot with examples) – 전체 수동 테스트를 예시로 제공했습니다. 더 나아졌지만, 여전히 엣지 케이스(edge cases)를 놓쳤습니다.
  3. 생각의 사슬 프롬프팅 (Chain-of-thought prompting) – "단계별로 생각하라"고 했습니다. 결과는 길고 쓸모없는 설명과 여전히 틀린 테스트였습니다.
  4. 파인튜닝 (Fine-tuning) – 일회성 프로젝트를 하기에는 비용이 너무 많이 듭니다. 게다가 여전히 정제된 학습 데이터가 필요합니다.

마침내 효과를 본 방법: 반복적이고 검증 가능한 접근 방식

핵심 통찰: LLM은 정답 여부를 자동으로 검증할 수 있는 제한된 환경에서 코드를 생성할 때 매우 뛰어납니다.

따라서 테스트를 직접 요청하는 대신, 다음과 같은 과정을 거쳤습니다:

  1. 함수 시그니처(Function signature)와 독스트링(Docstring) 추출 (타입 힌트(Type hints) 포함).
  2. AI에게 한 번에 하나의 함수만 제공 – 컨텍스트(Context)를 좁게 유지합니다.
  3. 특정 스키마(Schema)를 가진 테스트 스텁(Test stub) 생성 (함수가 주어지면 입력/출력 쌍을 포함한 테스트 케이스 목록을 생성).
  4. 생성된 테스트를 실제 함수에 실행하여 검증. 만약 테스트가 실패하면 환각(Hallucination)이 발생한 것으로 간주하고, 이를 로그에 기록한 뒤 폐기합니다.

이를 수행하는 스크립트의 간소화된 버전은 다음과 같습니다:

import ast
import inspect
import json
...

{func_source}

"""Return only valid JSON, no markdown."""
    response = client.chat.completions.create(
...

결정적으로, 저는 생성된 테스트 케이스를 실제 함수에 실행합니다. 만약 AI가 기본값(Default value)을 지어냈다면, 검증 단계에서 테스트가 실패하고 폐기됩니다. 남은 테스트들은 통과가 보장되며, 실제로 실제 동작을 테스트하게 됩니다.

트레이드오프 (Trade-offs)

  • 예외 케이스(Edge-case) 발견 불가 – AI는 보이는 것만 테스트합니다. 사용자가 생각하지 못한 기이한 입력값은 찾아내지 못합니다. 경계값(Boundary values), Null 값 등에 대해서는 여전히 인간의 분석이 필요합니다.
  • 복잡한 로직에 취약함 – 분기(Branch)가 많은 깊게 중첩된 함수의 경우, AI는 종종 해피 패스(Happy path)만을 생성합니다. 저의 검증 루프는 실패를 잡아낼 뿐, 누락된 커버리지(Coverage)를 잡아내지는 못합니다. 따라서 사후에 커버리지 보고서를 검토해야 합니다.
  • 정확한 타입 힌트(Type hints) 필요 – 타입 힌트가 없으면 AI가 인자(Argument) 타입을 추측하게 되며, 호환되지 않는 입력을 생성하는 환각(Hallucination)이 빈번하게 발생합니다.
  • 비용 – 함수 호출마다 몇 센트의 비용이 발생합니다. 대규모 프로젝트에서는 이 비용이 쌓이겠지만, 그래도 동일한 작업을 수행하는 제 급여보다는 저렴합니다.

이 방식을 사용하지 말아야 할 때

  • 코드베이스에 테스트가 전혀 없고 당장 어제라도 배포해야 하는 상황이라면, 이 워크플로우는 작업 속도를 늦출 것입니다. 그럴 때는 중요한 테스트만 직접 작성하세요.
  • 함수가 I/O(네트워크, 파일)를 다루는 경우 – AI는 모킹 (Mocking)을 가정하는 테스트를 생성할 것이나, 저는 이 부분을 아직 자동화하지 않았습니다.
  • 함수가 너무 큰 경우 (30줄 이상) – 먼저 함수를 분해하세요. AI는 덩어리가 커질수록 컨텍스트 (Context)를 놓치게 됩니다.

다음에 제가 다르게 시도해 볼 것들

  • 커버리지 분석 (Coverage analysis) 추가 – 생성된 테스트에 대해 coverage.py를 실행하여 누락된 브랜치 (Branch)를 보고합니다. 이를 AI에게 다시 전달하여 두 번째 패스 (Second pass)를 진행합니다.
  • inspect.getsource보다 더 신뢰성 있게 함수 시그니처 (Function signature)를 추출하기 위해 tree-sitter AST 파서를 사용합니다 (특히 클래스 메서드의 경우).
  • 검증 병렬화 (Parallelise validation) – 테스트를 하나씩 실행하는 것은 느립니다. 테스트를 배치 (Batch)로 묶고 서브프로세스 (Subprocess)를 사용하여 임시 pytest 스위트 (Suite)를 실행하겠습니다.

마법은 아니지만, 유용합니다

저는 이제 80%의 커버리지를 빠르게 확보해야 하는 모든 새로운 모듈에 이 방식을 사용합니다. AI가 지루한 매핑 테스트 (Mapping tests)를 작성하면, 저는 까다로운 엣지 케이스 (Edge cases)에 대해서만 고민하면 됩니다. 여전히 치팅 (Cheating)을 하는 기분이 들지만, 좋은 의미에서의 치팅입니다.

다른 분들도 LLM이 신뢰할 수 있는 테스트를 생성하도록 만드는 신뢰할 만한 패턴을 찾으셨나요? 여러분의 설정 (Setup)은 어떤 모습인가요?

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0