한 채널의 연동 해제 대상이 수십만 건이었고 신 경로에는 외부 호출 없이 즉시 정리하는 forceUnlink() 강제 수단이 있었다. 그런데 대상을 version으로 GROUP BY 해 세니 절반 넘게 구형이나 미분류여서 십수만 건은 그 수단을 못 쓰는 상태였다.
Table of contents
Open Table of contents
배경 — 구 경로의 두 단계
구 경로 코드를 직접 읽었다. unlink()는 event()만 발행하고 실제 작업은 다른 실행기에서 두 단계로 일어났다.
첫 단계에서 approval_type을 바꿔 작업 생성을 차단하고 관련 작업에 deleted_at을 찍은 뒤 두 번째 단계에서 연동 레코드에 deleted_at을 찍는다. 신 경로와 비교하니 복제본 생성 여부와 상품 상태를 건드리는지가 달라서 후처리 범위 자체가 달랐다.
검증 — 이름에 적힌 호출 조건
실행기 함수 이름에 AfterStopSucceeded라는 접미가 붙어 있었다. 무슨 이벤트 뒤인지를 찾아 호출 지점을 봤다.
stop 요청을 마켓에 보내고 그 결과가 성공일 때만 이 실행기를 부르고 있었다. 함수 이름의 접미가 호출 조건을 알려 주고 있었고 그것을 읽은 것이 이 조사의 전환점이었다.
원인 — 정리하려는 대상이라서 못 하는 것
우리 대상이 무엇이었는지가 여기서 문제가 됐다. 마켓에 이미 존재하지 않는 상품들이었고 그래서 정리하려는 것이었다.
stop 요청을 보내면 마켓이 그런 상품이 없다고 404로 답해 실패하고 그러면 후처리 자체가 안 돈다. 정리하려는 대상이라서 정리가 안 되는 순환이었다.
대안 — 우회 수단과 그 제약
신 경로에는 마켓 호출 없이 바로 정리하는 forceUnlink()가 있는데 구 경로에는 그것이 없었다. 이벤트와 전송과 성공과 후처리로 이어지는 흐름밖에 없다.
두 가지 방법이 있었는데 구 경로에도 forceUnlink()를 새로 만드는 것과 job_id로 직접 처리하는 기존 통합 헬퍼를 우회로 쓰는 것이다. 뒤쪽을 골랐다. 이유는 구 경로에 새 코드를 넣으면 배포와 검증이 이번 정리 일정을 넘어서다. 대신 제약을 감수했는데 작업이 이미 존재하는 대상만 처리할 수 있고 별도 권한이 필요하다. 각 단계가 연동 레코드 단건을 받으므로 여러 채널이 한꺼번에 영향받지는 않아서 범위는 안전하다는 것도 함께 확인했다.
교훈 — 경로 간 비대칭
이 조사의 순서가 중요했다. 대상을 버전별로 나눠 세고 구 경로 코드를 직접 읽은 뒤 실행 가능성을 판단하고 우회 수단을 찾는 순서였다.
코드를 읽지 않고 시작했으면 십수만 건에 처리를 돌리고 왜 안 되는지를 나중에 찾았을 것이다. 신 경로에 수단이 있으니 구 경로에도 비슷한 것이 있으리라는 가정이 틀린 것이었다. 새 경로를 만들 때 필요한 것을 추가하고 옛 경로는 그대로 두므로 둘의 기능은 대칭이 아니고 매번 그 경로에 그 기능이 있는지를 확인해야 한다.
정리
- 후처리가 외부 전송 성공에 의존하면 외부에 없는 대상은 정리가 안 된다
- 정리하려는 대상이라서 정리가 안 되는 순환이 생긴다
- 함수 이름의 접미가 호출 조건을 알려 주기도 한다
- 구 경로와 신 경로의
forceUnlink()같은 기능은 대칭이 아니다 - 저쪽에 있으니 이쪽에도 있겠거니 하고 가정하지 않는다
- 우회 수단을 찾으면 그 제약도 같이 적는다
- 대상을
version별로 나눠 센 것이 조사의 출발점이었다 - 코드를 먼저 읽지 않으면 실행 불가를 나중에 안다