POST /api/withdrawals 로 출금을 요청하면 202 수락 응답이 오고 실제 처리는 WithdrawWorker 가 한다. 화면에서 버튼을 누르면 수락 응답이 오는데 작업은 뒤에서 실패하고 있었다.
Table of contents
Open Table of contents
원인 — 조건이 두 곳에 나뉜 구조
요청을 받는 쪽의 검증은 잔고와 1일 한도 두 조건만 보고 있었다. WithdrawWorker 의 검증에는 withdraw_locked 와 코인의 wallet_status 가 둘 더 있었고 그것들이 거절의 실제 근거였다.
앞쪽이 통과시킨 것을 뒤쪽이 거절하는 사이에 수락 응답은 이미 나간 뒤다. 받는 조건과 처리하는 조건이 갈려 있으면 실패는 항상 뒤로 밀린다.
판단 기준 — 수락 응답이 뜻하는 것
RFC 7231 이 정한 대로 202 는 처리하려고 받았다는 뜻이지 됐다는 뜻이 아니고, 실제 처리 때 거절될 수도 있다고 명세 자체가 적고 있다. 그런데 화면은 대개 그 구분을 하지 않고 요청되었다는 문구를 띄운다.
회원은 그 문구를 보고 됐다고 판단하고 그 자리를 떠난다. 실패는 몇 분 뒤 ActiveMQ.DLQ 에 쌓이고 그 큐는 아무도 보지 않는다.
조치 — 버튼 노출 조건을 워커에 맞춘다
두 가지 방법이 있었다. 워커 실패를 회원에게 푸시로 알리는 것과, 처리할 수 없는 요청을 애초에 받지 않도록 버튼을 보여 주는 조건을 워커의 조건과 같게 하는 것이다. 뒤쪽을 골랐다. 이유는 알림은 실패한 뒤의 일이고 회원은 이미 떠난 뒤라서다. 대신 조회 응답이 무거워지고 프론트가 계산할 것이 느는 것을 감수했다.
앞쪽의 두 조건이 아니라 네 조건 전부가 노출 조건이 된다. 프론트가 그것을 계산하려면 정보가 응답에 있어야 하므로 GET /api/me/assets 응답에 canWithdraw 를 이루는 네 필드를 추가했다.
문장이 아니라 표현식으로
조건을 프론트에 넘길 때 설명 문장이 아니라 !locked && wallet == 'ACTIVE' && (balance >= amount) && (dailyUsed + amount <= dailyLimit) 표현식으로 적었다. 잠기지 않았거나 점검 중이 아닐 때라는 문장은 괄호를 어디에 묶느냐에 따라 뜻이 갈린다.
표현식으로 주면 옮겨 적는 것으로 끝나고 해석이 개입하지 않는다. 조건이 세 개를 넘어가면 문장으로 옮기는 순간 정확도가 떨어진다.
주의 — 동작에 따라 갈리는 조회 축
여기에 함정이 하나 더 있었는데 코인별 가능 동작을 담은 coin_capabilities 가 노드 심볼 기준으로만 채워져 있었다. 프론트가 들고 있는 것은 마켓 심볼이라 USDT 로 조회하면 노드 쪽 ETH 행이 안 잡혀 항상 빈 결과가 나온다.
입금과 출금 잠금은 마켓 심볼로 맞지만 노드 전송은 노드 심볼을 써야 했다. 같은 조회 함수인데 동작에 따라 다른 축을 쓰는 것은 문서에 없어서 코드를 봐야 알았고 응답에 node_symbol 을 새 필드로 넣었다.
같은 노드를 쓰는 토큰이 둘일 때 노출 판정과 실제 작업이 다른 행을 고를 수 있다는 문제는 남겼다. 고치려면 coin_capabilities 의 정합성부터 봐야 하므로 별도 사안으로 적어 두고 이번 범위에서 뺐다.
정리
- 받는 조건과 처리하는 조건이 갈리면 실패가 뒤로 밀린다
202는 받았다는 뜻이지 됐다는 뜻이 아니다- 화면이 그 구분을 안 하면 회원은 성공으로 본다
- 실패는 아무도 안 보는
ActiveMQ.DLQ에 쌓인다 - 근본 해결은 처리 못 할 요청을 받지 않는 것이다
- 노출 조건을
WithdrawWorker조건과 같게 맞춘다 - 조건은 문장이 아니라 표현식으로 전달한다
- 같은 조회가 동작에 따라 마켓 심볼과
node_symbol다른 축을 쓸 수 있다