Codex use case
검증 모듈 마이그레이션
모듈 하나를 업그레이드하고 외부에서 보이는 동작이 이전과 같은지 검증하세요.
Codex에 마이그레이션 범위를 명시한 문서와 계약 테스트 픽스처, 미리 설정한 클라우드 환경을 제공하세요. Codex가 검증 모듈 하나를 업그레이드한 뒤 해당 범위의 변경 사항, 기존 동작과의 일치 여부를 보여 주는 검증 자료, 남은 차이점, 롤백 절차를 정리합니다.
Best for
- 레거시 검증 컴포넌트를 마이그레이션하는 엔지니어링 팀
- 계약이 명확하고 테스트를 반복 실행할 수 있는 마이그레이션
- 다른 모듈의 동작을 유지하면서 소규모 변경을 적용하고 검토받아야 하는 팀
Contents
검증 모듈 마이그레이션
모듈 하나를 업그레이드하고 외부에서 보이는 동작이 이전과 같은지 검증하세요.
Codex에 마이그레이션 범위를 명시한 문서와 계약 테스트 픽스처, 미리 설정한 클라우드 환경을 제공하세요. Codex가 검증 모듈 하나를 업그레이드한 뒤 해당 범위의 변경 사항, 기존 동작과의 일치 여부를 보여 주는 검증 자료, 남은 차이점, 롤백 절차를 정리합니다.
Codex에 마이그레이션 범위를 명시한 문서와 계약 테스트 픽스처, 미리 설정한 클라우드 환경을 제공하세요. Codex가 검증 모듈 하나를 업그레이드한 뒤 해당 범위의 변경 사항, 기존 동작과의 일치 여부를 보여 주는 검증 자료, 남은 차이점, 롤백 절차를 정리합니다.
Related links
Best for
- 레거시 검증 컴포넌트를 마이그레이션하는 엔지니어링 팀
- 계약이 명확하고 테스트를 반복 실행할 수 있는 마이그레이션
- 다른 모듈의 동작을 유지하면서 소규모 변경을 적용하고 검토받아야 하는 팀
Starter prompt
언제 사용하면 좋을까요?
마이그레이션 범위가 명확하고 변경 전후의 동작을 비교하는 테스트가 있을 때 이 워크플로우를 사용하세요. Codex에 마이그레이션 작업 개요, 대상 모듈과 이를 호출하는 코드, 미리 고정한 계약 테스트 픽스처, 준비된 클라우드 환경을 제공하세요. 범위를 한정해 한 가지 변경을 수행하고 검증 자료와 롤백 방법을 함께 제시하도록 요청하세요.
이 예제에서는 주문 서비스의 유효성 검사 모델 하나를 Pydantic v1에서 v2로 마이그레이션합니다. 예제에는 기존 구현, 호환성을 확보하는 준비 단계, 최종 모듈이 포함되어 있습니다. 테스트는 실제 FastAPI 라우터와 오류 핸들러, 응답 직렬화, 인접한 레거시 라우트, 공개 OpenAPI 문서를 검증합니다. 데이터베이스 마이그레이션과 배포는 범위에 포함되지 않습니다.
클라우드 환경 준비하기
-
작업 위치 > 클라우드에서 게시된 환경을 선택하세요.
-
새 환경을 만들려면 환경 만들기를 선택하고 GitHub 리포지터리를 고른 다음, Codex에 런타임과 종속성, 격리된 테스트 서비스를 준비하도록 요청하세요.
-
설정 보고서를 검토하고 실패한 검사 항목의 문제를 해결한 다음, 새 작업을 시작하기 전에 게시를 선택하세요.
- 클라우드 환경 가이드를 따르세요.
- 리포지터리 지침은 AGENTS.md에 작성하세요.
종속성이나 서비스 설정이 바뀌면 재사용할 환경을 수정하고 테스트한 다음 다시 게시를 선택하세요.
업데이트한 환경은 새 작업에서 검증하세요. 리포지터리를 새로 고치면 종속성 캐시는 유지되고 설치 또는 시작 명령어는 다시 실행되지 않습니다. 기존 작업은 각자의 상태를 유지합니다.
필요한 네트워크 접속 대상과 자격 증명은 별도로 설정하세요.
각 단계에서 환경과 설정을 식별할 수 있는 참조 정보, 소스 커밋, Python 패치 버전, 프레임워크 및 종속성 버전, 패키지 관리자 버전, 잠금 파일 해시를 기록하세요. 새 작업의 결과를 사용하기 전에 해당 작업 안에서 실제 버전이 기록과 일치하는지 확인하세요.
예제 실행하기
전체 마이그레이션 예제를 다운로드하세요. 여기에는 독립적으로 실행할 수 있는 uv 프로젝트 세 개와 각 프로젝트의 종속성 버전이 확정된 uv.lock 파일, 애플리케이션 구현 코드, 오류와 스키마 처리를 위한 공용 어댑터, 고정된 HTTP 픽스처, 계약 테스트 러너가 들어 있습니다.
Python 3.11, 3.12 또는 3.13을 사용하세요. uv를 설치한 다음 아래 명령어를 실행하세요.
unzip sample.zip
cd migrate-a-validation-module
uv run --project support/target --python python3 --frozen python run.py --compare-stages --output evidence
비교를 실행하면 각 단계에 별도의 .venv가 생성되고 uv sync --frozen으로 종속성이 설치됩니다. 모든 단계에서 러너를 실행한 Python 인터프리터를 사용하며, HTTP 및 스키마 픽스처도 모든 단계에서 동일합니다. 종속성 버전은 각 잠금 파일에 지정된 대로 유지됩니다.
레거시 Pydantic v1 호환 브리지가 Python 3.14 이상을 지원하지 않으므로 이 버전들은 대상에서 제외됩니다. 실행에 성공하면 마지막에 다음과 같이 출력됩니다.
PASS: legacy, prepared, target HTTP/schema/serialization contracts match
Native model schemas differ; the existing public schema adapter is preserved.
evidence/legacy.json, prepared.json, target.json, summary.json을 확인하세요. 이 파일에는 실제 응답, 런타임 버전, 소스·픽스처·잠금 파일의 해시, 실행한 명령어, 종료 코드, 원본 로그 경로가 기록됩니다.
러너는 FastAPI의 TestClient를 사용해 동일한 프로세스 안에서 예제 애플리케이션의 HTTP 엔드포인트를 테스트합니다. 선택한 클라우드 작업 안에서 러너를 실행하고 해당 작업의 검증 자료를 보관하세요.
python3 run.py를 --compare-stages 없이 실행하는 방식은 Pydantic v2가 설치된 환경에서 마이그레이션 대상 모델만 확인할 때 사용하세요. 이 단독 실행 모드에서는 유효성 검사와 직렬화에 관한 일곱 가지 케이스를 확인합니다. HTTP 검사나 단계 간 결과 비교는 수행하지 않습니다. 마이그레이션을 검증하려면 위의 전체 비교 명령어를 사용하세요.
전체 비교에서는 단계마다 HTTP 케이스 23개, 공개 OpenAPI 문서, 응답 직렬화를 검사합니다. 로컬에서는 Python 3.11, 3.12, 3.13에서 모두 통과했습니다. 다운로드한 예제의 README.md에 테스트한 버전과 실행 이력이 기록되어 있습니다. 클라우드 환경에서도 현재 버전의 비교 테스트를 실행해 해당 환경의 결과를 기록하세요.
단계별 종속성 분리하기
| 단계 | Python / FastAPI / Pydantic |
마이그레이션 범위 |
|---|---|---|
| 레거시 | 3.11–3.13 / 0.119.0 / 1.10.22 |
기존 모델은 pydantic에서 임포트합니다. |
| 준비 단계 기준선 | 3.11–3.13 / 0.119.0 / 2.12.3 |
두 모델 모두 pydantic.v1을 사용합니다. 패키지에 실제로 포함된 v1 버전을 기록하세요. |
| 목표 단계 | 3.11–3.13 / 0.119.0 / 2.12.3 |
OrderInput만 v2로 전환합니다. 인접 라우트는 별도의 v1 모델을 계속 사용합니다. |
준비 단계와 목표 단계의 종속성 잠금 파일은 동일합니다. 모듈을 마이그레이션할 때는 종속성 환경을 그대로 두고 모델과 해당 모델을 직접 직렬화하는 메서드만 변경합니다.
준비 단계의 변경 사항은 별도로 검토합니다. 이 단계에서는 남아 있는 v1 라우트 어댑터도 수정해 응답 검증 전에 dict(by_alias=True)로 별칭을 키로 사용하는 딕셔너리를 반환하도록 합니다. v2 호스트에서 v1 모델을 직접 반환하면 공개 API의 별칭이 누락될 수 있습니다.
목표 단계의 주문 어댑터는 model_dump(by_alias=True)를 사용합니다. 인접한 v1 라우트는 준비 단계의 어댑터를 그대로 사용합니다.
FastAPI 마이그레이션 가이드에 따르면 v1과 v2 라우트를 함께 사용하는 임시 구성은 0.119.0부터 0.127.x까지만 지원됩니다. 0.128.0에서 이 지원이 제거되었습니다.
이 예제는 전환 과정을 보여 주기 위해 버전을 0.119.0으로 고정합니다. 더 최신 버전의 FastAPI를 사용하는 애플리케이션에서는 마이그레이션 범위 전체를 v2로 구성하세요. 임시 브리지에는 담당자를 지정하고 제거 조건을 명시하세요. v1 모델과 v2 모델을 서로 중첩하지 마세요. 이 브리지를 사용해 Python 3.14로 마이그레이션할 수는 없습니다.
계약 테스트 검토하기
미리 고정한 HTTP 테스트 케이스는 정상 요청, 메모를 생략하거나 명시적으로 null로 설정한 요청, ID가 누락되거나 유효하지 않은 요청, 알 수 없는 필드가 여러 개인 요청, 서로 다른 필드 오류가 함께 발생하는 요청, 유효하지 않은 메모를 다룹니다. 형식이 잘못된 JSON, 잘못된 콘텐츠 유형, 본문 누락, 별칭, 배열 및 문자열 본문, 인접한 레거시 라우트도 검사합니다. 오류 필드를 제거하거나 예상 타입을 바꾸지 않고 상태 코드, Content-Type 헤더, 파싱한 JSON 응답 전체를 비교합니다.
이 서비스는 레거시 단계부터 애플리케이션 자체에서 정의한 오류 응답 형식과 공개 OpenAPI 문서를 사용합니다. 오류 어댑터와 스키마 어댑터는 그대로 유지합니다. 목표 단계에서도 기존 오류 순서를 유지해 선언된 필드의 오류를 먼저 표시하고, 그다음 알 수 없는 키를 알파벳순으로 표시합니다.
테스트는 error.fields 배열 전체를 순서까지 비교합니다. 실제 응답을 따로 정렬하지 않습니다. 러너는 각 모델이 자체적으로 생성한 스키마를 별도로 기록하고 이 스키마들이 서로 다른지 확인합니다. 공개 스키마가 일치하더라도 종속성이 생성하는 스키마까지 동일한 것은 아닙니다.
이 서비스는 기존 입력 처리 방식도 유지합니다. [["order_id", "ord_123"]]처럼 키와 값의 쌍으로 이루어진 배열을 객체로 받아들이고, []와 빈 JSON 문자열은 모두 빈 객체로 변환해 ID 누락 오류를 반환합니다. 비어 있지 않은 문자열은 여전히 유효하지 않은 본문으로 처리합니다. 목표 모델도 dict(value)로 변환할 수 있는 입력을 계속 허용합니다. 입력을 객체로만 제한하려면 담당자가 계약 변경 여부를 결정해야 합니다. 테스트의 예상 결과만 고쳐서는 안 됩니다.
자체 리포지터리에서는 코드를 수정하기 전에 실제 기존 응답을 테스트의 기준값으로 고정하세요. 선택적 필드는 기본값을 명시해 유지하고, 검증의 엄격성과 알 수 없는 필드의 처리 방식도 보존하세요. 실제 오류 핸들러와 응답 직렬화를 테스트하세요.
가공하지 않은 유효성 검사 오류는 버전마다 다릅니다. Pydantic 마이그레이션 가이드에서는 별칭, 타입 강제 변환, 선택적 필드, 사용자 정의 유효성 검사 함수의 변경 사항도 설명합니다. v2에서는 이 함수 안에서 발생한 TypeError를 ValidationError로 변환하지 않습니다.
계약 테스트 러너 살펴보기
아래 코드는 다운로드한 예제의 run.py에서 발췌했습니다. ZIP 파일에 단계별 종속성 잠금 파일과 HTTP 픽스처가 포함되어 있으므로 압축을 푼 다음 전체 파일을 사용해 실행하세요.
세 단계 비교하기
class OrderInput(BaseModel):
internal_order_id: StrictStr = Field(alias="order_id")
customer_note: StrictStr | None = None
model_config = ConfigDict(extra="forbid", populate_by_name=False)
@model_validator(mode="before")
@classmethod
def preserve_legacy_mapping(cls, value):
# v1 tries dict(value), so pairs, [] and "" can become objects.
# Preserve successful conversions without changing failed-input errors.
if not isinstance(value, dict):
try:
return dict(value)
except (TypeError, ValueError):
return value
return value
def serialize_order(payload):
return OrderInput(**payload).model_dump(by_alias=True)
def preserve_legacy_error_order(fields):
# v1 reports declared-field errors first and unknown keys alphabetically.
# v2 keeps unknown keys in input order; preserve the existing HTTP contract.
known = [field for field in fields if field["code"] != "extra_forbidden"]
extra = sorted(
(field for field in fields if field["code"] == "extra_forbidden"),
key=lambda field: field["path"],
)
return known + extra
def check_target_model():
for payload, expected in (
({"order_id": "ord_123"}, {"order_id": "ord_123", "customer_note": None}),
(
{"order_id": "ord_123", "customer_note": "Leave at desk"},
{"order_id": "ord_123", "customer_note": "Leave at desk"},
),
(
{"order_id": "ord_123", "customer_note": None},
{"order_id": "ord_123", "customer_note": None},
),
):
assert serialize_order(payload) == expected
for payload, expected in (
({}, [("order_id", "missing")]),
({"order_id": 123}, [("order_id", "string_type")]),
(
{"order_id": "ord_123", "customer_note": 17},
[("customer_note", "string_type")],
),
(
{"order_id": "ord_123", "unexpected": True},
[("unexpected", "extra_forbidden")],
),
):
try:
OrderInput(**payload)
except ValidationError as error:
assert [
(item["loc"][0], item["type"]) for item in error.errors()
] == expected
else:
raise AssertionError(f"Accepted invalid order: {payload}")
print("PASS: target model validation and serialization (7 cases)")
print("Model-only checks; use --compare-stages for HTTP migration parity.")
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--output", type=Path, default=ROOT / "evidence")
parser.add_argument("--compare-stages", action="store_true")
args = parser.parse_args()
if not args.compare_stages:
check_target_model()
return
args.output.mkdir(parents=True, exist_ok=True)
reports = []
commands = []
for stage in ("legacy", "prepared", "target"):
project = Path("support") / stage
stage_commands = [
[
"uv",
"sync",
"--project",
str(project),
"--python",
sys.executable,
"--frozen",
],
[
"uv",
"run",
"--project",
str(project),
"--python",
sys.executable,
"--frozen",
"python",
"test/run_stage.py",
stage,
str(args.output / f"{stage}.json"),
],
]
for command in stage_commands:
completed = subprocess.run(
command, cwd=ROOT, capture_output=True, text=True
)
label = f"{stage}-{len(commands)}"
(args.output / f"{label}.log").write_text(
completed.stdout + completed.stderr
)
commands.append(
{
"command": command,
"exit_code": completed.returncode,
"log": f"{label}.log",
}
)
(args.output / "commands.json").write_text(
json.dumps(commands, indent=2, sort_keys=True) + "\n"
)
if completed.returncode:
print(completed.stdout + completed.stderr, file=sys.stderr)
raise SystemExit(completed.returncode)
print(completed.stdout, end="")
reports.append(json.loads((args.output / f"{stage}.json").read_text()))
assert all(report["python"] == platform.python_version() for report in reports)
for report in reports[1:]:
for key in (
"observations",
"public_openapi",
"direct_call",
"fixture_sha256",
"schema_fixture_sha256",
):
assert report[key] == reports[0][key], f"Parity failed: {key}"
assert reports[1]["lock_sha256"] == reports[2]["lock_sha256"]
native_schema_differs = (
reports[0]["native_model_schema"] != reports[2]["native_model_schema"]
)
assert native_schema_differs, "The example should expose native schema drift"
manifest = {
str(path.relative_to(ROOT)): digest(path)
for path in sorted(ROOT.rglob("*"))
if path.is_file()
and not path.resolve().is_relative_to(args.output.resolve())
and not any(
part in {".venv", "__pycache__", "evidence", ".ruff_cache"}
for part in path.relative_to(ROOT).parts
)
}
summary = {
"status": "pass",
"scope": "synthetic in-process FastAPI TestClient execution",
"stages": [report["stage"] for report in reports],
"http_cases_per_stage": reports[0]["http_cases"],
"native_model_schema_differs": native_schema_differs,
"public_http_and_schema_parity": True,
"commands": commands,
"source_sha256": manifest,
}
(args.output / "summary.json").write_text(
json.dumps(summary, indent=2, sort_keys=True) + "\n"
)
print("PASS: legacy, prepared, target HTTP/schema/serialization contracts match")
print(
"Native model schemas differ; the existing public schema adapter is preserved."
)리포지터리에서 워크플로우 실행하기
- 작업 개요를 작성하세요. 대상 모듈, 이를 직접 호출하는 코드, 담당자, 승인된 버전, 계약 테스트, 범위에서 제외할 코드, 롤백 절차를 명시하세요. 해당 범위를 마이그레이션하려면 종속성을 더 광범위하게 업그레이드해야 하거나 공개 API의 동작이 달라진다면 작업을 중단하세요.
- 비교 기준을 확립하세요. Codex에
AGENTS.md, 작업 개요, 공식 마이그레이션 지침, 잠금 파일, 호출 코드, 테스트를 검토하도록 요청하세요. 시작 커밋과 설치된 버전을 기록하세요. 기준 단계에서 실패한 검사가 있다면 동작이 일치한다고 판단하기 전에 실패 원인을 설명하거나 문제를 수정하세요. - 호환성 확보 작업은 별도로 진행하세요. 공유 종속성 때문에 대상 모듈의 변경이 다른 코드에 영향을 준다면 호환성 확보를 위한 변경부터 검토하세요. 기존 기준선과 준비 단계 기준선에서 동일한 픽스처로 테스트를 실행해 모두 통과하는지 확인하세요.
- 한 번에 한 범위만 마이그레이션하세요. 이 페이지의 시작 프롬프트를 사용하세요. 대상 모듈과 직접 연결된 어댑터를 수정하고, 관련 없는 모듈은 그대로 유지하세요. 남겨 둔 호환성 코드가 무엇인지 명시하세요.
- 동일한 테스트를 실행하세요. 예상 결과를 수정하지 않고 기준 단계와 최종 단계의 HTTP 및 스키마 픽스처를 비교하세요. 변경의 영향을 받는 통합 테스트, 린트 검사, 타입 검사를 실행하고 최종 소스와 종속성 상태를 식별할 수 있도록 출력을 저장하세요.
- 인계 내용을 검토하세요. 코드 차이, 실제 검증 자료, 해결하지 못한 차이점, 남아 있는 브리지, 롤백 절차를 확인하세요. 리포지터리의 도구와 권한으로 초안 PR을 만들 수 있을 때만 생성하세요. 병합, 배포, 다음 범위의 마이그레이션은 검토자에게 맡기세요.
검증한 내용 기록하기
각 기준에 대해 다음 형식으로 검증 자료를 기록하세요.
Criterion: [behavior and test]
Environment: [setup reference; actual runtime and dependency versions]
Before: [commit/diff ID; command; exit code; observed result; evidence path]
After: [final commit/diff ID; command; exit code; observed result; evidence path]
Status: [pass / fail / blocked / not run]
Limits: [untested behavior, missing prerequisite, or required owner decision]
임포트에 성공하거나 린트 검사를 통과하거나 스냅샷을 다시 생성한 것만으로는 API 동작이 일치한다고 판단할 수 없습니다. 의도한 차이는 별도로 기록하고 최종 소스가 변경되면 검사를 다시 실행하세요. 예제를 자체 애플리케이션에 맞게 적용할 때는 해당 애플리케이션의 호출 코드와 서비스도 계약 테스트에 포함하세요.
Related use cases
장애 조사 및 수정 사항 테스트
Codex Cloud에 민감한 정보를 제거한 장애 조사 자료와 정상 작동하는 테스트 환경을 제공하세요
Codex가 사용할 CLI 만들기
Codex에 어느 폴더에서나 실행할 수 있고 레포지토리 스크립트와 조합하거나 파일을 다운로드할 때 쓸 수 있는 CLI를 만들어 달라고 하세요
목표를 향해 작업하기
Codex가 여러 대화 턴에 걸쳐 작업을 이어가야 하고 종료 조건의 충족 여부를 확인할 수 있다면 `/goal`을 사용하세요.