통합과 배포 · 만료는 사고가 아니다 · 실습
야간 배치가 매번 몇 건씩 401 로 죽는다
목표
짧은 수명의 접근 토큰을 들고 남의 API 를 계속 부르는 클라이언트를 만든다. 만료 전에 미리 갱신하고, 시계 오차 여유를 두고, 401 을 한 번만 받고 갱신 후 재시도하며, 워커가 여럿이어도 갱신이 한 번만 일어나게 한다.
왜 중요한가
"야간 배치가 매번 두세 건씩 401 로 실패합니다" 의 원인은 거의 언제나 같다. 토큰을 한 번 받아 두고 401 이 오면 그때 새로 받는 구조는 만료 순간마다 반드시 한 번은 실패한다.
토큰이 짧게 사는 것은 결함이 아니라 설계다. 수명이 짧아야 새어 나갔을 때 쓸 수 있는 기간이 짧다. 그래서 고쳐야 하는 쪽은 서버가 아니라 우리 클라이언트다.
그리고 고칠 때 세 가지를 함께 봐야 한다. 시계는 정확히 같지 않으므로 여유가 필요하고, 401 이 토큰 문제가 아닐 수도 있으므로 재시도 횟수에 상한이 필요하고, 워커가 여럿이면 만료 순간에 갱신이 한꺼번에 몰리므로 갱신을 한 번으로 묶어야 한다.
채점기는 여러분의 문장을 믿지 않는다. 인증 서버를 채점기가 고른 포트에 직접 띄우고 발급 횟수와 자원 호출 횟수를 서버 쪽에서 세어, 여러분의 클라이언트가 실제로 몇 번 갱신하고 몇 번 다시 걸었는지 대조한다.
단계
1. /root/token/authsrv.py 를 만들어 포트 8015 에 띄우고, 토큰 하나를 받아 /root/token/token.json 에 저장하세요.
2. /root/token/decode.py 를 만들어 토큰 안의 헤더와 페이로드를 읽어 /root/token/claims.json 에 적으세요.
3. /root/token/should_refresh.py 를 만들어 만료·미도래·임박·정상 네 갈래를 시계 오차 여유까지 감안해 가려내게 하세요.
4. /root/token/client.py 를 만들어 만료 전에 미리 갱신하며 여러 번 호출해도 401 이 한 건도 나지 않게 하세요.
5. client.py 가 401 을 받으면 갱신 후 딱 한 번만 다시 걸게 하세요.
6. client.py 가 워커 여럿이 동시에 시작해도 토큰 발급이 한 번만 일어나게 하세요.
7. /root/token/token_policy.json 에 정책 표를 적으세요.
8. /root/token/token_report.md 에 네 절로 보고하세요.
참고
- 인증 서버 실행 계약:
python3 /root/token/authsrv.py --port <포트> [--ttl <초>] [--always-401].POST /oauth/token은{"access_token": <JWT>, "token_type": "Bearer", "expires_in": <초>}를,GET /api/data는Authorization: Bearer <토큰>이 유효하면 200 을, 아니면 401{"error": "invalid_token", "reason": ...}을 냅니다.GET /stats는{"issued": n, "api_requests": n}입니다. JWT 는 HS256 이고 claim 은 iss·sub·iat·nbf·exp·jti 입니다. - 읽기 실행 계약:
python3 decode.py --in <토큰 응답 JSON> --out <claims JSON>은{"header": {...}, "payload": {...}, "ttl_s": exp - iat}를 냅니다. base64url 은 패딩을 떼고 다니므로 길이를 4의 배수로 맞춘 뒤 디코드합니다. 여기서 하는 일은 읽기지 검증이 아닙니다. - 판정기 실행 계약:
python3 should_refresh.py --exp <epoch> --now <epoch> --skew-s <초> [--nbf <epoch>]는{"valid": ..., "refresh": ..., "remaining_s": ..., "reason": "ok"|"near_expiry"|"expired"|"not_yet_valid"}를 냅니다. 판정 순서는 nbf 먼저, 그다음 만료, 그다음 임박입니다.remaining_s는exp - now이고 만료된 뒤에는 음수입니다. 임박(near_expiry)한 토큰은 아직 쓸 수 있습니다. - 클라이언트 실행 계약:
python3 client.py --base <URL> --calls <n> [--interval-ms <m>] [--skew-s <s>] [--workers <w>] [--cache <파일>] [--start-expired]는{"calls": n, "ok": k, "unauthorized": u, "refreshes": r, "retried_after_401": x}를 냅니다.--workers가 1보다 크면 호출을 동시에 하고,--cache는 토큰을 담아 두는 파일이며,--start-expired는 만료된 토큰을 캐시에 심고 시작합니다. - 갱신 폭주를 막는 자리는 캐시와 잠금입니다. 잠금을 잡은 뒤에 캐시를 한 번 더 확인하지 않으면, 잠금은 순서만 줄 세울 뿐 갱신 횟수는 줄지 않습니다.
- 정책 표 형식:
{"token_ttl_s": ..., "refresh_skew_s": ..., "max_retry_on_401": ..., "cache_path": ..., "stampede_guard": ..., "clock_sync": ..., "notes": ...}.refresh_skew_s는 1 이상이고token_ttl_s보다 작아야 하며,max_retry_on_401은 1 이하입니다. - 흔한 실수: 401 이 온 뒤에야 갱신하기, 재시도 횟수에 상한을 안 두기(권한 문제를 401 로 답하는 서버에서 고리가 생깁니다), 잠금만 걸고 두 번째 확인을 빠뜨리기, 토큰을 로그에 그대로 남기기.
- 서버는 백그라운드로 띄우고
/health가 200 이 될 때까지 기다린 뒤 다음으로 갑니다. 채점기는 여러분이 띄워 둔 프로세스를 보지 않고 스크립트를 직접 다시 띄웁니다.
단계 8개
- 짧게 사는 토큰 받아 보기
- 토큰 안을 열어 보기
- 언제 갈아야 하는가
- 만료 전에 미리 갈기
- 401 은 한 번만 받는다
- 만료 순간에 스무 개가 한꺼번에 갱신한다
- 정책 표로 못박기
- 토큰 점검 보고서