외부에 자료를 보내는 연동이 있었다. 며칠치가 상대 쪽에 없다는 연락을 받았는데 우리 로그에는 아무 오류가 없었다.
Table of contents
Open Table of contents
증상 — 오류가 없던 로그
오류가 없는데 자료가 안 갔다면 실패를 기록하지 않은 것이다. 그래서 sendToPartner 를 부르는 자리를 열어 실패했을 때 무엇을 하는지부터 봤다.
grep 으로 그 기간의 로그를 다시 훑어도 경고 한 줄이 없었다. 아무것도 없는 것과 아무 일도 없었던 것을 로그만 보고는 가를 수 없었다.
받은 연락에는 빠진 날짜가 적혀 있어서 그 날짜의 주문은 우리 쪽에 남아 있었다. orders 에는 정상으로 들어갔고 밖으로 나가는 부분만 빠진 상태였다.
원인 — 예외를 삼키던 catch
전송 코드는 이렇게 돼 있었다.
try {
$this->sendToPartner($data);
} catch (Exception $e) {
// 전송 실패해도 주문 처리는 계속되어야 함
}
주석에 적힌 의도 자체는 맞다. 외부 전송이 실패했다고 주문 처리를 막으면 안 된다.
문제는 catch 안에 그 주석 말고 아무것도 없다는 점이었다. 실패했다는 사실이 $e 와 함께 그 자리에서 사라진다.
흐름을 계속하는 것과 아무 일도 안 하는 것은 다른 이야기다. 이 둘이 한 덩어리로 붙어 있어서 앞을 택하면 뒤가 딸려 오고 있었다.
조치 — 흐름과 기록과 재처리를 나눴다
정해야 할 것이 하나가 아니라 셋이었다. 흐름을 막을 것인지와 기록을 남길 것인지와 나중에 다시 보낼 것인지다.
이번 경우 sendToPartner 에 대한 답은 차례로 아니오와 예와 예였다. 원래 코드는 첫 번째만 답하고 나머지 둘을 답하지 않은 상태였다.
셋을 따로 적어 넣었다.
try {
$this->sendToPartner($data);
} catch (Exception $e) {
Log::error('partner send failed', [
'order_id' => $data['id'],
'exception' => $e,
]);
PendingTransfer::create([
'order_id' => $data['id'],
'payload' => $data,
'attempts' => 0,
]);
}
흐름은 그대로 계속되고 Log::error 가 남고 PendingTransfer 에 재시도 대상이 쌓인다.
배치가 주기적으로 PendingTransfer 를 꺼내 다시 보내고 attempts 를 올린다. 몇 번 실패하면 상태를 바꾸고 사람에게 알린다.
재시도가 안전한지 확인했다
다시 보내는 처리를 넣기 전에 확인할 것이 하나 있었다. 같은 자료를 두 번 보내면 상대 쪽에서 어떻게 되는가였다.
물어보니 주문번호로 중복을 거른다고 해서 두 번째는 무시된다고 했다. 그 답을 문서에 적어 두었는데 이런 것은 물어봐야 알고 물어본 사람이 떠나면 다시 물어야 한다.
중복을 안 걸렀으면 우리 쪽에서 전송 여부를 order_id 별로 기록하고 그것을 확인한 뒤에 보내야 했다. 재시도를 넣을 수 있느냐는 상대 쪽 동작이 정하는 것이었다.
설정 — 등급 나누기와 성공 건수
기록을 넣으면서 어떤 것을 어느 등급으로 남길지도 정했다.
| 상황 | 레벨 | 이유 |
|---|---|---|
| 한 번 실패, 재시도 예정 | warning | 흔하고 스스로 회복된다 |
| 재시도 소진 | error | 사람이 봐야 한다 |
| 응답이 예상과 다름 | error | 계약이 바뀌었을 수 있다 |
전부 error 로 하면 진짜 문제가 그 안에 묻힌다. 하루에 수십 건 나는 일시적 실패와 재시도가 다 끝난 실패는 성격이 다르다.
그리고 실패만 남기면 아무것도 안 나갔을 때를 알 수 없다. 배치가 아예 안 돌면 실패 로그도 안 생겨서 조용한 것이 정상인지 고장인지 갈리지 않는다.
그래서 성공 건수도 주기적으로 남겼다.
[transfer] 배치 완료: 성공 1,240 / 실패 3 / 대기 12
0건이어도 이 줄은 나간다. 이 줄이 안 보이면 배치가 안 돈 것이다.
우리가 아니라 상대가 알려줬다
빠진 건수를 세어 보니 연락받은 며칠치가 전부가 아니었다. 오래된 것도 섞여 있었고 그때는 상대 쪽에서 알아채지 못했을 뿐이었다.
이 사건에서 가장 걸린 것은 삼킨 예외 자체가 아니었다. 우리 시스템의 실패를 우리가 아니라 상대가 먼저 알려줬다는 쪽이었다.
그 구조면 상대가 보는 것만 결국 알게 된다. 상대가 안 보는 연동에서 같은 catch 를 만나면 아무도 모르는 채로 남는다.
정리
- 빈
catch는 흐름을 계속하려는 의도는 맞다 - 그러면서 기록과 후속 처리를 함께 빠뜨린다
- 흐름을 계속하는 것과 아무 일도 안 하는 것은 다르다
- 흐름을 막나와 기록하나와 나중에 처리하나 셋을 나눠 정한다
Log::error와PendingTransfer를 따로 넣는다- 재시도 전에 두 번 보내도 되는지 확인하고 그 답을 문서에 적는다
- 전부
error로 남기면 진짜 문제가 묻힌다 - 성공 건수도 남겨야 아무것도 안 나간 것을 안다
- 외부가 먼저 알려주는 구조면 외부가 안 보는 것은 영영 모른다