24시간 뒤 자동 완료가 안 된다는 문의를 받고 원인을 추적했다. 옛 시스템의 스케줄러를 열어 그 명령이 걸린 줄을 찾았다.
Table of contents
Open Table of contents
증상 — 주석 처리된 스케줄
그 줄은 이렇게 돼 있었다.
// $schedule->command('order:cancel-auto-complete')->hourly();
order:cancel-auto-complete 가 주석 처리돼 있으니 기능을 껐구나 싶었다.
자동 완료 기능이 비활성화돼 있다고 그대로 답할 뻔했다. 증상과 그 // 한 줄이 딱 맞아떨어져 더 볼 이유가 없어 보였다.
그대로 답할 뻔했다
그런데 그 답에는 설명되지 않는 구석이 하나 남아 있었다.
왜 껐는지가 설명되지 않았고 껐으면 대체가 있거나 요구사항이 없어졌어야 하는데 둘 다 확인하지 않았다.
// 한 줄이 그 자리에서 답을 주는 것처럼 보였다. 그것이 왜 그렇게 됐는지는 아무것도 말해 주지 않았다.
껐다는 사실은 상태이고 왜 껐는지는 이유다. 상태만 보고 답하면 그 // 가 나온 맥락을 통째로 건너뛴다. 껐다는 것만 알고 이유를 모르면 되살려도 되는지조차 못 정한다.
원인 — 신 시스템에 있었다
이 시스템은 옛 schedule 과 새 @Scheduled 가 함께 도는 기간이었다.
@Scheduled(cron = "0 */5 * * * ?")
fun processOrderCancel() {
val claims = repository.findByTypeAndStatus("cancel", "ready")
claims.forEach { kafka.send(it) }
}
processOrderCancel 이 5분 주기로 같은 일을 하고 있었다.
kafka.send 만 하고 끝나는지도 봤다.
class CancelHandler {
fun handle(msg: CancelMessage) = when (msg.claimStatus) {
"ClaimReady" -> confirmCancel(msg) // 승인
"ClaimDone" -> completeCancel(msg) // 완료
"ClaimReject" -> rejectCancel(msg) // 거부
}
}
CancelHandler 가 claimStatus 별로 처리하고 있었다.
kafka.send 만 있고 handle 이 없으면 job 이 쌓이기만 하는데 양쪽이 다 있었다. 여기까지 보고 나니 그 // 가 폐지가 아니라 이관이라는 것이 분명해졌다.
주기도 달랐는데 hourly 쪽은 한 시간이고 @Scheduled 쪽은 5분이다. 옮기면서 주기까지 다시 정한 흔적으로 읽힌다. 그대로 옮긴 것이 아니라 옮기면서 값을 다시 정한 셈이다.
제약 — 확정하지 못한 부분
다만 이것을 확정할 근거가 약했다.
이관을 명시한 주석 없음
이관 커밋 메시지 확인 못 함
schedule->command 와 @Scheduled 가 둘 다 있다는 것으로 판단한 것이다.
그래서 기록에는 이관을 명시한 주석과 커밋을 확인하지 못했고 두 저장소의 코드 병존으로 판단했다고 적었다.
// 옆에 이관을 적은 한 줄만 있었어도 확정이었을 텐데 없었다. 확정이라고 쓸 근거가 없을 때는 그것을 판단이라고 적는 편이 맞았다.
다음 사람이 그 강도를 알고 읽어야 잘못된 확신이 안 생긴다. 판단을 확정처럼 적으면 그 뒤의 결정이 전부 그 위에 쌓인다.
대응 — 바뀐 조사 방향
이것으로 문의 조사의 방향이 바뀌었다.
[처음] 옛 스케줄러가 꺼져 있다 → 켜면 되나?
[실제] 신 시스템이 도는데 안 된다 → 거기를 봐야 한다
hourly 쪽을 켜면 @Scheduled 와 같은 대상을 처리해 이중 처리가 된다.
주석을 폐지로 읽고 되살리는 것이 그 상황에서 가장 나쁜 선택이었고 되살리기 전에 다른 쪽을 확인하는 것이 순서였다. 고치기 전에 지금 무엇이 실제로 도는지부터 보는 것이 먼저였다.
미처리 원인은 새 쪽의 수집과 소비 구간에서 찾아야 했고 방향이 바뀐 뒤에야 볼 곳이 정해졌다.
교훈 — 주석에 적혀 있어야 할 것
이 조사가 길어진 이유는 그 줄에 아무 이유도 없었기 때문이다.
// v2 의 OrderCancelProcessorJob 으로 이관 (2018-xx)
// $schedule->command('order:cancel-auto-complete')->hourly();
OrderCancelProcessorJob 을 적은 한 줄이면 됐고 그 한 줄이 없어서 두 저장소를 다 뒤졌다.
주석 처리한 코드에는 왜 껐는지와 옮긴 곳까지 적어야 하고 껐다는 사실만 남으면 다음 사람이 같은 조사를 다시 한다.
병존 구조에서는 한쪽만 봐서는 판단이 안 된다.
옛 것에 없다 → 없는 게 아니라 새 것에 있을 수 있다
새 것에 없다 → 아직 안 옮겨져 옛 것에 있을 수 있다
항상 양쪽 스케줄러를 보고 어느 쪽이 실제로 도는지도 확인해야 한다. 둘 다 돌고 있으면 그것대로 이중 처리이고 둘 다 꺼져 있으면 그때가 진짜 폐지다.
병존 기간에는 한쪽 저장소의 코드가 그 기능의 전부가 아니고 어디에 있느냐보다 어느 쪽이 도느냐가 답이었다.
정리
- 주석 처리가 기능 폐지를 뜻하지 않는다
- 증상과 코드가 맞아떨어져 보여도 왜 껐는지가 설명돼야 한다
- 병존 구조에서는 다른 쪽으로 이관됐을 수 있다
- 주석을 발견하면 다른 쪽의 스케줄러와 소비 쪽을 먼저 확인한다
- 넣는 쪽과 꺼내는 쪽이 다 있는지 본다
- 옛 것을 다시 켜면 이중 처리가 된다
- 되살리기 전에 다른 쪽을 확인한다
- 확정 근거가 없으면 병존으로 판단했다고 적는다
- 주석 처리한 코드에는 왜 껐는지와 어디로 갔는지를 적는다
- 한쪽만 보고 내린 결론은 한 번은 틀린다
- 둘 다 돌고 있으면 그것대로 이중 처리다