♻️ [Refactor/#40] 계산엔진점검#41
Open
jung32111 wants to merge 7 commits into
Hidden character warning
The head ref may contain hidden characters: "refactor/\uacc4\uc0b0\uc5d4\uc9c4\uc810\uac80"
Open
Conversation
이후 API 계약 정리 작업(단위 통일, 고갈 표현 통일 등)에서 의도치 않은 필드 값 변경을 즉시 감지하기 위한 baseline. TestClient 사용을 위해 httpx를 requirements에 추가.
data/active_income_stats.csv(집계 통계, 개인 단위 레코드 없음)가 .gitignore의 data/ 규칙에 걸려 레포에 없었다. fresh clone 시 /api/employees/simulate가 첫 요청에서 500을 반환하는 문제가 있어, 이 파일만 예외 처리해 커밋한다. 개인 단위 원본 데이터 3종은 계속 제외한다. 추가로 앱 기동(lifespan) 시점에 필수 데이터 파일 존재를 검증해, 파일이 없으면 요청 시점 500 대신 기동 자체가 명확한 에러 메시지와 함께 실패하도록 한다.
/api/retirement/* 5개 요청·응답의 모든 금액 필드가 만원과 원을 혼용하고 있었다(예: employees/simulate는 원, retirement/*는 만원인데 필드명은 동일하게 monthly_pension). 프론트가 두 API를 이어 쓰면 10,000배 오차가 조용히 발생하는 문제였다. 내부 계산 로직(app/services/retirement_service.py)과 상수는 그대로 만원 단위를 쓰고, app/schemas/money.py의 WonAmountInput/WonAmountOutput 타입으로 Pydantic 스키마 경계에서만 원 <-> 만원 변환을 수행하도록 했다. 변환 지점을 한 곳에 모아 서비스 코드는 전혀 건드리지 않았다. reduction_rules.threshold처럼 고정 상수와 비교되는 필드는 변환 여부에 따라 결과가 실제로 달라지므로(회귀 테스트로 검증: threshold와 정확히 같은 재취업 소득을 원 단위로 넣으면 감액이 0이어야 함), 이를 tests/test_money_unit_consistency.py에 회귀 테스트로 먼저 추가한 뒤 고쳤다. 골든 스냅샷은 동일한 실제 시나리오(예: "월 생활비 250만원")를 유지한 채 입력 리터럴에 10,000을 곱해 새 계약에 맞게 갱신했다.
두 가지 문제를 하나의 이슈로 묶어 처리: 1. timeline의 asset이 고갈 이후에도 매년 음수로 계속 하강했다(100세까지 최대 -15억원 수준). Recharts에 그대로 꽂으면 축 스케일이 깨져 프론트가 반드시 잘라내는 전처리를 해야 했다. 고갈 이후 asset을 0으로 클램프한다. 2. scenarios의 depletion_age가 무고갈일 때 MAX_AGE(100)를 sentinel로 써서, 실제로 100세에 고갈되는 경우와 구분이 불가능했다(diagnosis 등 다른 엔드포인트는 무고갈 시 null). ScenarioOutcome.depletion_age도 null로 통일하고, _select_best_scenario의 정렬 기준만 별도로 무고갈을 "가장 늦게 고갈"로 취급하도록 수정했다(API 응답 값 자체는 null 유지). 추가로 depletion_age is not None을 매번 계산하지 않아도 되도록 depleted: bool 필드를 모든 진단류 응답에 추가했다. timeline 각 시점에 고갈 여부 플래그를 추가하는 안은 검토했으나, depletion_age가 이미 응답에 있어 프론트가 point.age >= depletion_age로 바로 판단할 수 있으므로 중복 정보라 추가하지 않았다. 회귀 테스트(tests/test_depletion_representation.py)를 먼저 추가해 수정 전 실패를 확인한 뒤 구현했다.
TimelinePoint.income/expense/gap/cumulative_gap이 SimulationResult.monthly_gap 등 월 단위 필드와 이름이 겹쳐(둘 다 "gap"), 프론트가 연/월 어느 쪽인지 필드명만으로 구분할 수 없었다. annual_income/annual_expense/annual_gap/ cumulative_annual_gap으로 이름을 바꿔 연 단위임을 명시했다. 값 자체는 변경하지 않았다(골든 스냅샷으로 rename 전후 값이 동일함을 확인).
gender: str에 제약이 없어 "MALE"(대문자 오타) 같은 값이 get_target_age()의 if gender == "male": ... else: FEMALE 분기를 조용히 타고 여성 목표연령(88세)으로 처리됐다(HTTP 200, 프론트는 오류를 알 방법이 없음). Gender(str, Enum)로 "male"/"female" 외 값을 422로 거부하도록 했다. 전수 조사 결과 이 "검증 없는 문자열 + 매칭 실패 시 조용한 기본값" 패턴은 gender 하나뿐이었다(employees.py의 current_band/retire_band는 서버가 계산해 내보내는 출력 필드라 사용자 입력 검증 대상이 아님). 회귀 테스트(tests/test_gender_validation.py)를 먼저 추가해 대문자/오타/빈 문자열 입력이 현재 200으로 통과함을 확인한 뒤 고쳤다.
에러가 상황마다 세 가지 다른 모양으로 나가고 있었다: Pydantic 422는
{"detail": [...]} 배열(영문 메시지), 라우터의 HTTPException(400)은
{"detail": "문자열"}, model_validator의 ValueError는 422이지만 메시지 앞에
"Value error, "가 붙었다. 프론트가 error.detail을 그대로 쓸 수 없었다.
app/schemas/errors.py에 {code, message, details} 단일 스키마를 두고,
app/error_handlers.py의 RequestValidationError/HTTPException 핸들러가
모든 에러를 이 형태로 변환하도록 했다. code는 프론트 분기용
(VALIDATION_ERROR/INVALID_INPUT/INTERNAL_ERROR 등), details는 필드별
원인(Pydantic 에러 타입을 한국어로 번역), message는 그대로 사용자에게
보여줄 수 있는 한국어 문장이다. 라우터 코드는 건드리지 않았다(기존처럼
ValueError -> HTTPException(400)만 던지면 핸들러가 알아서 감싼다).
회귀 테스트(tests/test_error_format.py)를 먼저 추가해 세 가지 에러
경로(타입 에러/비즈니스 룰/model_validator) 전부와 5개 엔드포인트에서
현재 포맷이 제각각임을 확인한 뒤 고쳤다.
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🚀 관련 이슈
📝 작업 내용
dcde592)9c8a9d1)1cc3b67)depletion_agenull=무고갈로 sentinel 제거,depletedboolean 필드 추가 (3855ebe)gap→annual_gap) (8815486)8533b18)6bdf7ed)✔️ 체크 리스트
mainbranch에 실수로 PR 생성 금지)📸 스크린샷 (선택)
💬 리뷰 요구사항(선택)
➕ 추후 계획(선택)