Skip to content

[refactor] 추출 실패 code 전수 매핑 + reason 을 운영 액션 기준으로 재분류 #936

Description

@m-a-king

extractor 가 422 로 돌려주는 확정 실패 code 13종 중 core 가 의미를 아는 건 2종뿐이다. 나머지는 전부 else 폴백으로 permanent_error 한 바구니에 들어간다.

// RemoteExtractionContract.kt:99
return when (code) {
    CODE_NOT_PRODUCT_PAGE -> ProductSnapshotException.notProductPage()
    CODE_UNTRUSTWORTHY_VALUE -> ProductSnapshotException.untrustworthyValue()
    else -> ProductExtractorException.permanentFailure()
}

30일 실측이 이 바구니가 이름대로 쓰이지 않고 있음을 보여준다 (Loki):

code 건수 현재 reason
NOT_PRODUCT_PAGE 41 not_product
UNTRUSTWORTHY_VALUE 39 not_product
NO_EXTRACTABLE_CONTENT 3 permanent_error
EMPTY_SHELL 1 permanent_error
나머지 9종 0 -

미매핑으로 permanent_error 에 떨어진 4건이 전부 "우리가 이 페이지를 못 읽었다" 계열이고, 이름이 뜻하는 진짜 외부·내부 오류 code 9종은 30일간 한 건도 없었다. permanent_error 는 자기 이름대로 쓰이는 게 아니라 매핑 누락분을 받는 통이 되어 있다.

게다가 EMPTY_SHELL 은 성격이 바뀐 code 다. extractor 에서 permanent=true, escalatable=true("정적 fetch 로는 같은 셸이지만 실제 브라우저면 뚫린다")인데, 정직함 전환(2026-08-12)으로 헤드리스가 기본 차단되면서 화이트리스트 밖 도메인은 이 code 가 그대로 422 로 표면화된다. 의미는 "상품 아님"도 "외부 오류"도 아닌 "이 도메인은 지금 우리 구성으로 못 읽는다"는 커버리지 신호이며 앞으로 늘어날 code 인데, 지금은 permanent_error 에 묻혀 보이지 않는다.

무엇을

선행: infra 의 contracts/extraction-error-codes.yaml 신설 이슈가 머지된 뒤 착수한다.

전이 판정은 손대지 않는다. 422 를 확정 실패로 보고 즉시 FAILED 하는 것은 모든 code 에 대해 옳다. 고칠 대상은 "그 실패를 무엇이라 부르고 어떻게 세는가" 한 층뿐이다.

  • ProductSnapshotException.noExtractableContent() 팩토리 추가 + ProductSnapshotErrorCode 엔트리 (번호 append-only). 계약 문서가 이미 지목한 동등물이다.
  • RemoteExtractionContract.translate 를 전수 매핑으로 확장 — 카탈로그의 모든 permanent code 를 명시 분기로 처리하고, else 는 "모르는 code" 방어로만 남긴다.
  • ItemParsingMetrics reason 재편 — reason 을 "이 숫자가 늘면 누가 무엇을 하는가"로 나눈다.
reason 늘면 할 일 code
not_product 사용자가 상품 아닌 걸 넣음 없음 (정상) NOT_PRODUCT_PAGE, INVALID_URL
unreadable 우리 구성으로 못 읽음 도메인 허가 후보 EMPTY_SHELL, NO_EXTRACTABLE_CONTENT
blocked 대상이 우리를 막음 UNSUPPORTED 정책 후보 FETCH_CLIENT_ERROR, PERMANENT_UPSTREAM
extract_quality 추출은 됐는데 값을 못 믿음 모델·프롬프트·검증 규칙 UNTRUSTWORTHY_VALUE, LLM_INVALID_RESPONSE, IMAGE_UNSUPPORTED
internal_error 우리 버그·방어 발동 코드 조사 BLOCKED_HOST, TOO_MANY_REDIRECTS, MALFORMED_REDIRECT

permanent_error 는 제거한다. recover 계열(retry_exhausted·no_source·deadline)과 ready_rejected·none 은 그대로 둔다 — 파이프라인 내부 사정이라 축이 다르고 이미 액션과 잘 대응한다.

  • ci.yml 에 infra 체크아웃 스텝 + 메타 테스트 — 카탈로그의 모든 permanent code 가 translate 에 나타나고, 각 code 가 카탈로그 bucket 과 같은 reason 으로 귀결되는지 검사한다.
  • 기존 단위 테스트 갱신translate 분기 망라.

결정 근거

UNTRUSTWORTHY_VALUEnot_product 에서 뗀다. 39건으로 두 번째로 많은데 NOT_PRODUCT_PAGE(41건)와 한 통에 있어 "상품 아님" 지표를 사실상 두 배로 부풀린다. 성격도 다르다. 대신 대시보드 히스토리 연속성이 그 지점에서 끊긴다.

카디널리티는 감당된다. Prometheus counter 는 increment 될 때만 시계열을 만드는데, blocked·internal_error 계열 code 는 30일간 0건이라 시계열이 생성되지 않는다. 실질 증가는 두 종이고 permanent_error 가 사라지므로 환경 축 2개를 곱해도 순증 약 +2. Metrics 프리티어 한도 상황에서도 안전하다.

범위 밖

  • 사용자 문구 분화 — CLAUDE.md 규약이 "detail 은 전부 고정 사용자 문구, 구분은 로그·메트릭"이므로 현재의 단일 문구가 규약대로다.
  • extraction_platform_policies(DB)와의 통합 — 축이 다르다. DB 정책은 요청 도메인 라우팅이고 운영자가 백오피스로 바꾼다. 카탈로그는 응답 해석이고 배포로 양쪽이 함께 움직인다. unreadable 추이가 정책 추가의 근거가 되는 입력·출력 관계다.

후속

대시보드·알림의 reason 축 갱신(permanent_error 참조 제거, unreadable 추이 패널 신설)은 이 이슈 배포 이후 별건으로 한다.

Metadata

Metadata

Assignees

Labels

refactor구조 개선, 외부 동작 불변

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions