mypy를 속이고 런타임에서 오류를 일으키는 AI 타입 힌트 (Type Hints)
요약
AI 코딩 어시스턴트가 생성한 타입 힌트가 구문론적으로는 올바르지만 실제 코드 동작과 일치하지 않아 런타임 오류를 유발하는 문제를 다룹니다. mypy와 같은 타입 검사기가 잡아내지 못하는 AI 생성 타입 힌트의 의미론적 결함과 주요 패턴을 분석합니다.
핵심 포인트
- AI는 타입 검사 통과를 위해 구문론적으로만 올바른 타입 힌트를 생성할 수 있음
- mypy는 주석의 일관성은 검증하지만 실제 코드 동작의 의도는 검증하지 못함
- AI가 생성하는 주요 오류 패턴: 잘못된 Optional 사용, Dict[str, Any], 비동기 함수 반환 타입 오류, 잘못된 cast() 호출
- 타입 검사 결과(초록불)를 실제 동작의 완전한 증거로 신뢰해서는 안 됨
mypy는 당신의 코드가 깨끗하다고 말합니다. 당신의 CI 파이프라인도 이에 동의합니다. 그러다 누군가 str로 주석(annotation)을 달아놓은 곳에 None을 전달하면, 타입 검사기(type checker)의 경고 없이 런타임(runtime)에서 AttributeError가 발생합니다.
이것은 AI가 생성한 타입 주석(type annotations)이 가진 구조적인 문제입니다. 주석은 구문론적으로(syntactically) 올바릅니다. 즉, mypy가 이를 파싱하고 호출 지점(call sites)과 대조하여 검증하며 아무런 문제를 보고하지 않습니다. 문제는 주석이 주장하는 바와 코드가 실제로 수행하는 동작 사이의 간극에 존재하는 의미론적(semantic) 문제입니다. AI 코딩 어시스턴트(AI coding assistants)는 타입 검사 통과라는 초록불이 시사하는 것만큼 그 간극을 자주 메워주지 못합니다.
AI가 구조적으로는 유효하지만 의미론적으로는 틀린 타입 힌트를 생성하는 이유
AI 코딩 어시스턴트는 mypy의 구조적 검증기(structural validator)를 만족하는 타입 힌트를 생성합니다. 즉, 주석은 구문론적으로 올바르고 타입 검사를 통과하지만, 코드의 실제 동작과 일치하지 않는 의미론적 의미를 담고 있습니다. 이는 mypy가 주석의 일관성을 검증할 뿐, 주석 작성자의 의도가 올바른지는 검증하지 않기 때문입니다. BrassCoders는 12개의 스캐너 중 하나로 Pyre/Pysa를 실행하여, 단순히 주석의 일관성을 확인하는 것을 넘어 타입 주석이 달린 경계 전반에 걸쳐 데이터 흐름을 추적하는 오염 분석(taint analysis)을 추가합니다.
근본적인 원인은 AI 어시스턴트가 주석 패턴을 학습하는 방식에 있습니다. 수백만 개의 Python 파일로 학습된 어시스턴트는 수천 개의 문맥에서 Optional[str]이 사용되는 것을 보았습니다. 어시스턴트는 주석을 함수의 시그니처(signature) 형태에 맞춰 패턴 매칭할 뿐, 모든 호출자의 동작에 맞추지는 않습니다. 코드베이스의 나머지 부분이 None 체크 없이 반환 값을 역참조(dereference)할 때, 어시스턴트는 해당 호출 그래프(call graph)를 추적하지 않습니다. mypy는 문서화된 설계에 따라 주석이 구조적으로 일관되는지는 검증하지만, 동작적으로 올바른지는 검증하지 않습니다.
이러한 차이는 대규모의 타입 지정 코드베이스(typed codebases)에서 매우 중요합니다. 주석이 거짓을 말할 때, mypy의 초록불은 리뷰어들이 타입 검사 결과를 동작의 증거로 신뢰하도록 훈련시킵니다. 하지만 그것은 사실이 아닙니다.
mypy를 속이는 네 가지 타입 힌트 패턴
BrassCoder의 Pyre/Pysa 스캐너는 타입 어노테이션 (type-annotated)이 된 코드 전반에 걸쳐 오염 분석 (taint analysis)을 수행하며 타입 안전하지 않은 데이터 흐름을 표시할 수 있습니다. 하지만 AI가 생성하는 패턴 중 mypy가 아무런 불만 없이 수용하는 네 가지 패턴은 다음과 같습니다: 하위 코드에서 None으로 처리하지 않는 필드에 적용된 Optional[str], mypy가 강제할 수 있는 구조를 무너뜨리는 Dict[str, Any], 비동기 함수 (async functions)의 잘못된 반환 타입 어노테이션 (return type annotations), 그리고 타입을 변환하지 않으면서 타입에 대해 거짓말을 하는 cast() 호출입니다.
Optional[str]이 가장 흔합니다. AI 어시스턴트는 이론적으로 존재하지 않을 수 있는 필드를 보고 이를 Optional[str]로 어노테이션합니다. 이는 고립된 상태에서는 기술적으로 정확합니다. 문제는 모든 하위 호출자가 이를 직접 역참조(dereference)한다는 점입니다: user.name.upper(). mypy는 호출자가 None 분기를 처리하는지 여부가 아니라, 호출자 자신의 어노테이션을 기준으로 검사합니다. AttributeError가 기다리고 있습니다.
Dict[str, Any]는 타입 안전성을 완전히 무너뜨리는 어노테이션입니다. Python의 typing module은 정확히 이 경우를 위해 mypy가 강제할 수 있는 알려진 키 형태를 가진 딕셔너리인 TypedDict를 제공합니다. AI 어시스턴트는 타입 검사를 절대 실패하지 않기 때문에 기본값으로 Dict[str, Any]를 사용합니다. 하지만 이는 어떠한 강제성도 제공하지 않습니다.
비동기 함수에서의 반환 타입 불일치는 더 미묘합니다. -> str로 어노테이션된 비동기 함수가 코루틴 객체 (coroutine object)를 반환하더라도 컴파일 및 타입 검사를 통과합니다. 런타임에 str을 await 하는 호출 코드는 문자열이 아닌 예외 (exception)를 받게 됩니다. mypy는 해당 함수가 타입 검사가 이루어지는 컨텍스트에서 명시적으로 호출될 때만 이 불일치를 잡아냅니다. 어노테이션이 없는 호출자들은 이를 그대로 통과시킵니다.
cast()는 가장 직접적인 거짓말입니다. 이는 런타임에 값을 수정하지 않고 mypy에게 "나를 믿으세요, 이 값은 타입 T입니다"라고 말하는 것과 같습니다. cast()는 런타임에서 순수한 no-op (아무 작업도 하지 않음)입니다. AI 어시스턴트는 검사기가 해결할 수 없는 타입 에러를 침묵시키기 위해 이를 사용합니다. 근본적인 불일치는 그대로 남아 있습니다.
어노테이션이 거짓말을 할 때: 런타임 vs 정적 분석 (Static Analysis)
mypy의 타입 체커 (type checker)는 분석 시점의 어노테이션 (annotations)을 기반으로 작동하며, 코드를 직접 실행하지는 않습니다. 어떤 함수가 str을 반환한다고 명시된 어노테이션이 있다면, mypy가 추적하지 못한 코드 경로에서 해당 함수가 None을 반환하더라도 mypy는 이를 신뢰합니다. BrassCoders의 Pysa 스캐너 (scanner)는 이러한 흐름을 절차 간 (interprocedurally)으로 추적합니다. 즉, 함수 호출을 통해 None을 따라가며, 이것이 충돌을 일으키거나 잘못된 동작을 유발하는 싱크 (sink)에 도달할 때까지 추적합니다.
범위의 차이는 상당합니다. mypy는 지역적 타입 추론 (local type inference)을 수행합니다. 즉, 각 함수의 본문이 선언된 함수의 시그니처 (signature)와 일치하는지, 그리고 호출 지점 (call sites)이 시그니처와 일치하는지를 확인합니다. mypy는 런타임 (runtime)에 호출 그래프 (call graph)를 통해 실제로 어떤 값들이 흐르는지에 대한 전역적 도달 가능성 분석 (global reachability analysis)을 실행하지 않습니다. 함수가 -> str로 어노테이션되어 있지만 None을 반환하는 코드 경로가 있는 경우, mypy는 그 None 반환이 자신이 분석할 수 있는 분기 (branch) 내에 있을 때만 불일치를 표시합니다. Any 반환 타입을 가진 서드파티 API 호출을 통해 전달되는 None은 감지되지 않은 채 통과됩니다.
Pysa의 오염 분석 (taint analysis)은 소스 (sources) — 사용자 입력, 외부 API 응답, 데이터베이스 읽기 — 에서 시작하여 호출 그래프를 통해 해당 값들을 절차 간 (interprocedurally)으로 추적합니다. 단순히 어노테이션을 확인하는 것에 그치지 않고, 무엇이 도달 가능한지를 추적합니다. 오염된 None이 str로 표시된 타입 어노테이션 경계를 넘어서는 순간, 그것은 통과 신호가 아니라 탐지 결과 (finding)가 됩니다.
BrassCoders가 타입이 지정된 Python 코드에서 탐지하는 항목
BrassCoders는 12개의 스캐너 중 하나로 Pyre/Pysa를 실행합니다. Pysa는 타입이 지정된 Python 코드에 대해 절차 간 오염 분석 (interprocedural taint analysis)을 수행하며, 오염 (taint)이 타입 경계를 넘나드는 데이터 흐름을 탐지합니다. 또한 cast() 사용 패턴과 보안에 민감한 작업에 인접한 어노테이션되지 않은 코드 경로를 탐지합니다.
BrassCoders를 mypy와 함께 실행하면 동일한 코드베이스에 대해 두 가지 서로 다른 분석을 수행할 수 있습니다. mypy는 타입이 일치하지 않는 호출 지점(call sites)에서 어노테이션 불일치를 찾아냅니다. Pysa는 도달 가능성(reachability) 문제, 즉 어노테이션 내용과 관계없이 위험한 값이 민감한 싱크(sinks)에 도달하는 코드 경로를 찾아냅니다. 이 두 도구는 중복되는 것이 아니라 상호 보완적입니다.
실시간 어노테이션 검증을 위해 에디터에서는 Pyright를 사용하고, CI 파이프라인에는 mypy의 --strict 옵션과 함께 BrassCoders를 추가하세요. macOS, Linux, Windows (WSL2)에서는 다음과 같이 설치할 수 있습니다:
pip install brasscoders
brasscoders scan .
OSS 코어는 외부 네트워크 호출 없이 로컬에서 실행됩니다. Apache 2.0 라이선스이며, 계정이 필요하지 않습니다. Pyre/Pysa가 12개의 스캐너 중 하나로 포함되어 있어, 모든 스캔 시 오염 분석 (taint analysis)이 자동으로 실행됩니다.
BrassCoders Paid는 OSS 코어 위에 임베딩 기반의 노이즈 감소 기능을 추가하여 개발자당 월 $12에 제공됩니다. 이는 스캔 결과물이 수동으로 분류하기 어려운 수준으로 많아질 때 유용합니다. 여기서 설명한 타입 힌트 (Type Hints) 패턴을 잡아내는 데에는 Pysa가 포함된 OSS 코어만으로도 충분합니다.
벤치마크 결과: BrassCoders는 발표된 코퍼스에 포함된 12개의 AI 생성 버그 중 11개를 잡아냈습니다. Bandit은 이 중 6개만을 잡아냈습니다. 타입 힌트 오류는 Bandit이 전혀 탐지하지 못하는 범주입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기