주문이 수집되지 않는다는 제보였다. 외부 채널에서 주문은 났는데 우리 쪽에 안 들어오고 있었다. 기록을 보니 연동 상태가 오류이고 로그에 등록 중 오류가 남아 있는데, 그 상품은 우리 시스템에서 판매 중지 상태였고 중지도 정상 반영돼 있었다.
Table of contents
Open Table of contents
외부에 상품이 둘
외부 채널 관리자에서 찾아보니 같은 상품이 두 개 등록돼 있었고 상품번호가 달랐다. 우리 DB에는 나중에 만들어진 쪽 하나만 기록돼 있었다.
먼저 만들어진 쪽은 우리가 모른다. 중지도 안 되고 수정도 안 되며 거기서 난 주문은 번호가 안 맞아 매칭에 실패한다.
요청과 저장 사이의 창
등록 코드를 보니 등록 요청과 받은 번호를 저장하는 지점 사이에 간격이 있었다.
$response = $this->api->registApiGoodsInfo($params); // 등록 요청
// ... 응답 파싱 ...
$dp->channel_product_id = $response['goods_no']; // 번호 저장
$dp->save();
그 사이에서 응답이 유실되면 외부에는 상품이 만들어졌는데 우리는 번호를 못 받는다. 예외가 나고 작업이 실패로 기록되며 외부에만 존재하는 상품이 그 순간 생긴다.
재시도의 재진입
실패한 작업은 재시도 배치가 다시 돌리는데 재진입한 등록 함수가 아무것도 확인하지 않았다. 이미 번호가 있는지도 안 보고 외부에 실물이 있는지도 안 본다. 그냥 등록 요청을 또 보낸다.
그리고 그 채널은 같은 판매자 상품코드에 대해 중복을 막지 않고 새 번호를 발급해 또 만들어 준다. 재시도 한 번이 유령 하나가 되는 구조였다.
실제 데이터로 확인하니 작업의 시도 횟수가 3이었고 작업 생성 1초 뒤에 외부 상품 하나가 만들어졌으며 마지막 재시도에서 또 하나가 만들어졌다. 우리 DB에는 마지막 것만 저장돼 있었다.
유령의 성질
이것이 문제의 성질을 정한다. 유령은 우리 쪽에 흔적을 남기지 않는다. 로그에도 없고 테이블에도 없으며 우리 관점에서는 등록이 한 번 실패했다가 성공한 것으로만 보인다.
그래서 중지 작업은 저장된 번호에만 나가므로 유령은 계속 판매 중이고, 수정 작업도 마찬가지라 유령은 옛 정보로 남으며, 유령에서 난 주문은 수집되지 않는다. 중지했는데 팔린다는 증상이 여기서 나온다.
규모를 재려면 외부에서 세야 한다. 우리 DB를 아무리 뒤져도 유령의 개수는 안 나오고 채널 관리자에서 판매자 상품코드 기준 중복을 세는 것이 유일한 방법이다.
고칠 두 자리
진입부에 멱등 가드를 넣는다. 이미 번호가 저장돼 있으면 등록이 아니라 수정으로 넘긴다.
if ($dp->channel_product_id) {
return $this->updateProduct($product);
}
그리고 실패했을 때 외부와 대조한다. 등록 요청 후 예외가 나면 판매자 상품코드로 외부를 조회해 이미 존재하면 그 번호를 채워 넣고 성공 처리하며 존재하지 않을 때만 재등록한다.
두 번째가 핵심이다. 첫 번째만 넣으면 번호가 저장되기 전에 죽은 첫 실패는 여전히 막지 못하는데 그 지점이 정확히 유령이 생기는 창이다.
재시도 안전의 조건
같은 형태를 다른 채널에서도 찾아보니 등록 함수의 구조가 대개 같았다. 요청하고 파싱하고 저장하는 순서에서 요청과 저장 사이에 창이 있고 재시도가 그 창의 존재를 모르면 어디서든 같은 일이 난다. 채널이 중복을 막아 주면 운이 좋은 것이고 안 막아 주면 유령이 쌓인다.
이번 건이 알려 준 것은 재시도 가능한 작업이라는 말이 재시도해도 안전하다는 뜻이 아니라는 점이다. 외부에 무언가를 만드는 작업이면 재시도 전에 셋 중 하나가 있어야 한다. 우리가 아는 식별자로 존재를 확인할 수 있거나, 우리가 보낸 키로 외부를 조회할 수 있거나, 외부가 같은 키의 중복을 거부하는 것이다. 셋 다 없으면 재시도가 곧 중복 생성이고 그 중복은 우리 DB에 안 보인다.
정리
- 등록 요청과 번호 저장 사이의 창에서 응답이 유실되면 외부에만 있는 것이 생긴다
- 재시도가 존재 확인 없이 재등록하면 재시도 한 번이 유령 하나가 된다
- 외부가 중복을 안 막아 주면 새 번호를 계속 발급한다
- 유령은 우리 DB에 흔적이 없어 중지와 수정 대상에서 영구히 빠진다
- 규모는 외부에서 세야 나온다
- 진입부 멱등 가드와 실패 시 외부 대조 두 곳을 고친다
- 재시도 안전은 식별자나 조회 가능한 키나 외부의 멱등성 중 하나를 요구한다