ai-coding-mines목차GitHub

"중복된 X값" 에러가 값이 아니라 ID를 말하고 있었다 — 그리고 에러는 겹으로 나온다

쓰기 API

증상

옵션(변형)을 확장해 전체 PUT을 보내니 400이 떨어졌다.

중복된 옵션값

이라고 하니 속성값을 봤다. 허용목록, 표기, 중복 조합. 세 번 고쳤고 세 번 다 헛일이었다.

원인

프로토타입 하나를 deepcopy로 복제해 변형을 만들고 있었는데, 복제본이 원본의 식별자 필드까지 그대로 가져갔다. 결과적으로 모든 변형이 같은 ID를 달고 전송됐다. 플랫폼이 말한 "중복된 옵션값"은 속성값이 아니라 옵션 식별자를 가리킨 것이었다.

★★★ deepcopy는 "값"만 복제하지 않는다. 식별자도 값이다. 프로토타입 복제는 동일성을 생산하는 연산이라, 유일해야 할 필드가 조용히 따라온다.

복제 직후 ID 계열 필드를 지우는 것을 규약으로 둬라. "나중에 각자 채운다"는 방식은 하나만 빠뜨려도 그 건이 원본과 같은 ID로 나간다. 지우면 빠뜨린 것이 즉시 드러나고, 덮어쓰면 빠뜨린 것이 정상처럼 보인다.

★★ 이건 식별자가 잘못 설계돼서 생긴 문제가 아니다. 필드는 정확하고 유일했다. 우리가 사본을 만들면서 유일성을 깼다. 식별자 유일성이 깨지는 경로는 보통 절단(길이 제한이 서로 다른 키를 같게 만듦)과 부모 공유인데, 복제가 세 번째다.

★★★ 메시지의 명사가 원인을 가리키지 않는다

플랫폼이 쓰는 단어와 당신이 아는 단어가 같은 뜻이라는 보장이 없다. "값"이라고 쓰여 있으면 값을 파게 되는데, 그 단어는 그쪽 스키마의 어휘다. 같은 컬렉션의 다른 항목에서도 같은 형태가 나왔다 — 누락 메시지가 요구한 것은 그 필드가 아니라 그 필드가 켜는 검증이었다.

에러 문구는 위치 정보이지 원인 정보가 아니다. 그 근처를 보라는 뜻이지, 그것이 범인이라는 뜻이 아니다.

★★★ 로그를 앞 100자로 자르면 답이 화면에 안 나온다

메시지 전문에는 중복된 ID 목록이 그대로 들어 있었다. 응답 안에 답이 있었는데 로그 포맷이 [:110]에서 잘라내고 있었다.

110자는 "에러 메시지 한 줄"을 담기에 충분해 보인다. 그래서 아무도 의심하지 않는다. 하지만 요약은 앞에 오고 근거는 뒤에 온다 — 플랫폼이 위반 항목 목록을 붙여 보내면, 그 목록이 정확히 잘리는 위치다.

★★ 이 컬렉션에는 이미 "오류 문구의 모든 절을 용의선상에 둔다"가 적혀 있다. 그걸 적어두고도 다시 밟았다 — 그때는 혹은 뒷절을 안 읽은 것이었고, 이번엔 애초에 전문이 남지 않았다.

규칙을 적는 것과 로그 포맷을 고치는 것은 다른 작업이다.실패 응답은 자르지 않고 전문을 남겨라. 요약은 성공 응답에만 쓴다. 실패는 드물고, 드문 만큼 전문을 남길 여유가 있다.

★★★ 그 다음 겹 — 고치면 다음 에러가 나온다

ID를 비웠더니 이번엔 이렇게 나왔다.

판매중인 상품은 삭제할 수 없습니다

전체 PUT에서 보낸 목록이 곧 최종 상태다. 기존 항목을 빼고 새 목록을 보내는 것은 삭제 요청으로 읽힌다. 판매 중인 항목은 지울 수 없으므로 원본을 첫 항목으로 그대로 남기고 변형을 뒤에 추가하는 형태여야 했다.

메시지실제 원인
1중복된 옵션값복제가 식별자까지 가져감
2판매중인 상품은 삭제할 수 없습니다기존 항목은 유지해야 함
3중복된 옵션값 (다시)두 출처의 옵션축이 같은 축이었다

★★★ 세 번째 겹 — 같은 메시지가 다른 원인으로 다시 나온다

기존 상품의 속성축이 색상인데 소스 쪽 변형축도 색상이었다. 변형을 추가해도 옵션값이 원본과 같아져 또 중복이 된다. 1번을 고친 코드는 여기에 아무 소용이 없다.

★★★ 메시지가 같다고 원인이 같은 것이 아니다. 한 번 본 에러 문구는 "그때 그거"로 읽히기 때문에, 재발이 아니라 신종일 가능성을 놓친다.

★★ 서로 다른 두 출처의 축을 합칠 때, 축 이름이 겹치면 곱집합이 아니라 충돌이 된다. 직교한다고 가정하고 합치는 코드는 같은 축일 때 조용히 중복을 만든다. 합치기 전에 축 이름의 교집합을 확인하라.

★★ 그리고 어느 시점에는 포기해야 한다 — skip:축충돌

3회 실패하면 사유를 적고 다음 건으로 간다.

★★★ 기준은 재시도 횟수가 아니라 "재시도가 새 정보를 만드는가"다. 접근 경로를 바꾸는 재시도는 매번 새 정보를 낳지만, 구조적으로 불가능한 건은 30번을 해도 같은 답이다. 축이 겹친다는 사실은 재시도로 바뀌지 않는다.

한 건을 붙들면 배치가 멈춰 작업 전체를 포기하게 된다. 개별 건을 놓지 못하는 것이 오히려 완주를 막는다.

사유를 적은 skip은 손실이 아니라 보류다. 그냥 건너뛰면 나중에 재처리 대상인지조차 알 수 없다. 스킵 사유는 실패 사유와 같은 자리에 같은 형식으로 남긴다.

★★ 첫 에러가 사라졌다고 고쳐진 게 아니다. 다음 겹이 나오는 것이 정상 경과다. 겹이 안 나오면 그때 재조회로 확인한다 — 이 문서의 첫 문장이 그것이다.

검증 방법

한 번에 한 겹만, 가설 하나만 바꿔 재시도한다. 세 곳을 같이 고치면 어느 것이 겹을 벗겼는지 모른다. 그리고 전송 직전 페이로드에서 식별자 필드의 유일성을 어서션으로 건다len({x.id for x in items}) == len(items). 이 한 줄이면 세 번의 헛수고가 없었다.