장기 액세스 토큰 자동 갱신하는 법 — 60일 뒤 조용히 멈추는 걸 막는 systemd 타이머
- 공유 링크 만들기
- X
- 이메일
- 기타 앱

API 연동으로 뭔가를 자동 발행하고 있다면, 지금 당장 확인해볼 게 하나 있어요. 그 액세스 토큰을 갱신하는 코드가 실제로 있는지입니다.
제 서버는 없었습니다. 토큰 만료일을 파일에 적어두기만 하고 갱신은 아무도 하지 않는 상태로 돌고 있었어요. 그대로 뒀으면 60일째 되는 날 오류 하나 없이 발행만 조용히 멈췄을 겁니다.
이 글에서 그 구멍을 어떻게 찾았고, systemd 타이머 몇 줄로 어떻게 막았는지 설정 파일까지 그대로 정리했어요.
📌 3줄 결론
- 만료일을 "기록"하는 코드와 "갱신"하는 코드는 다릅니다. 전자만 있는 경우가 흔합니다
- 갱신은 만료 당일이 아니라 10일 전부터 매일 시도해야 합니다. 한 번 실패해도 아홉 번이 남습니다
- 앱 안 루프가 아니라 별도 타이머로 두면, 문제가 생겼을 때 그것만 즉시 끌 수 있습니다
확인일 2026-08-21 · 대상: 제가 직접 운영하는 리눅스 서버 1대
갱신 코드가 0줄이었다
Threads API의 장기 액세스 토큰은 유효기간이 60일입니다. 만료 전에 갱신 요청을 보내면 다시 60일이 연장돼요. 공식 문서에 그렇게 적혀 있고, 저도 알고 있었습니다.
문제는 알고 있는 것과 코드에 있는 것이 다르다는 거였어요. 코드·크론·systemd 유닛을 전부 뒤졌는데 갱신을 시도하는 곳이 한 군데도 없었습니다. 토큰을 처음 받을 때 만료일을 계산해서 파일에 적어두는 코드는 있었어요. 그게 있으니까 갱신도 되고 있다고 착각했던 겁니다.
이런 구멍이 위험한 이유는 실패 방식 때문입니다. 서버가 죽지도 않고, 500 오류가 나지도 않고, 로그에 빨간 줄이 찍히지도 않아요. 그냥 발행 요청이 거절되고, 큐에는 계속 쌓이고, 화면에는 아무 이상이 없습니다. 실제로 이 서버는 다른 설정 문제로 발행이 2주간 멈춰 있던 적이 있는데, 그때도 화면에는 성공으로 보였어요.
터지는 고장은 금방 알아챕니다. 조용해지는 고장이 오래 갑니다.
systemd 타이머 네 줄이 하는 일
갱신 스크립트를 만들고 이렇게 걸었습니다.
[Timer]
OnCalendar=daily
RandomizedDelaySec=1h
Persistent=true
세 줄이지만 각각 다른 사고를 막습니다.
OnCalendar=daily는 매일 한 번 돌립니다. 만료 당일에 한 번이 아니라 매일이라는 게 핵심이에요. 뒤에서 설명할 10일 창과 합쳐지면 기회가 열 번 생깁니다.
RandomizedDelaySec=1h는 실행 시각을 최대 한 시간 흩어놓습니다. 정각에 여러 작업이 몰려 서로를 방해하는 걸 막아요.
Persistent=true는 서버가 꺼져 있어서 실행을 건너뛰었다면 다음 부팅 직후에 한 번 따라잡습니다. 토큰 만료는 서버 가동 시간과 무관한 시계라서 이게 필요합니다. 서버를 사흘 껐다 켜도 만료일은 사흘 앞으로 다가와 있으니까요.
서비스 유닛에는 한 줄을 더 넣었습니다.
[Service]
Type=oneshot
SuccessExitStatus=0 1
SuccessExitStatus=0 1은 스크립트가 1로 끝나도 실패로 취급하지 않는다는 뜻이에요. 갱신에 실패해도 다음 날 타이머가 다시 시도하면 되니까, 한 번의 실패로 유닛 전체가 실패 상태에 빠지지 않게 했습니다.
언제 갱신하고 언제 건너뛰나
그렇다고 조건 없이 날마다 갱신 요청을 보내면 안 됩니다. 판정 규칙을 이렇게 뒀어요.
- 만료까지 10일 이내일 때만 갱신한다
- 발급된 지 25시간이 안 지났으면 건너뛴다
- 이미 만료됐거나 만료일을 못 읽으면 건너뛰고 알린다
두 번째 규칙에 사연이 있습니다. 공식 제약상 발급 직후 24시간 안에는 갱신이 거부돼요. 그래서 24시간이 아니라 25시간으로 한 시간 여유를 뒀습니다. 경계값에 딱 맞추면 시계 오차나 실행 지연 때문에 매번 아슬아슬해집니다.
실제로 2026년 8월 21일에 판정만 다시 확인해보니 이렇게 나왔어요.
[건너뜀] @arion_0722 — 아직 여유 51일
아직 10일 갱신 창에 들어오지 않았기 때문에 규칙대로 건너뛴 겁니다. 아무 일도 안 한 게 아니라, 안 해야 할 때 안 한 것이 확인된 거예요.
그리고 갱신에 실패해도 기존 토큰은 절대 지우지 않습니다. 실패한 갱신 응답으로 멀쩡한 토큰을 덮어쓰는 게 가장 흔한 사고라서요.
직접 확인하는 법
여러분 서버에도 같은 구멍이 있는지 몇 분이면 확인할 수 있어요.
먼저 갱신을 시도하는 코드가 있기는 한지 봅니다. 저장소 전체에서 갱신 관련 표현을 찾아보세요.
grep -rn "refresh_token\|refresh_access_token" --include="*.py" .
주기 실행에 걸려 있는지도 따로 봐야 합니다. 코드가 있어도 아무도 안 부르면 없는 것과 같아요.
systemctl list-timers --all | grep -i refresh
crontab -l
타이머가 있다면 마지막 실행 시각을 확인하세요. LAST 칸이 비어 있으면 한 번도 안 돈 겁니다.
systemctl list-timers 타이머이름 --all
설치 직후 첫 점검 때는 LAST가 비어 있었습니다. 하지만 2026년 8월 21일 재점검에서는 LAST가 2026-08-21 00:15:57 UTC로 채워져, 타이머가 실제 실행된 기록까지 확인했습니다. 만들어둔 것과 돌아본 것은 다릅니다.
오늘 제가 저지른 오판
이 글을 쓰려고 스크립트를 점검하다가 이런 오류를 만났습니다.
ModuleNotFoundError: No module named 'httpx'
"고쳐놓은 갱신 스크립트가 사실은 죽어 있었다"는 결론으로 갈 뻔했어요. 그런데 서비스 유닛을 열어보니 실행 경로가 시스템 파이썬이 아니라 가상환경 파이썬이었습니다.
ExecStart=/opt/ai-world/.venv/bin/python /opt/ai-world/backend/threads_refresh.py
그쪽으로 실행하니 아무 문제 없이 돌았습니다. 제가 손으로 python3을 쳐서 테스트한 게 틀렸던 거예요.
검증 도구를 잘못 쓰면 멀쩡한 것을 고장으로 봅니다. 점검할 때는 반드시 서비스가 실제로 쓰는 실행 경로를 그대로 따라가세요. 유닛 파일의 ExecStart가 정답지입니다.
자주 묻는 질문 (FAQ)
Q. 앱 안에 갱신 루프를 넣으면 안 되나요?
됩니다. 다만 저는 일부러 분리했어요. 이 서버에는 잘못 만든 감시 스크립트가 35일간 서비스를 반복해서 죽인 이력이 있어서, 문제가 생기면 그것만 즉시 끌 수 있는 형태가 필요했습니다. 별도 유닛이면 systemctl disable --now 한 줄로 끝납니다.
Q. cron으로 하면 안 되나요?
됩니다. 다만 서버가 꺼져 있던 시간을 따라잡는 기능이 기본으로 없어요. systemd 타이머의 Persistent=true가 그 역할을 합니다.
Q. 갱신 주기를 왜 10일로 잡았나요?
하루에 한 번 시도하므로 10일이면 열 번의 기회가 됩니다. 네트워크 문제나 일시적인 API 오류로 며칠 실패해도 여유가 남아요. 반대로 너무 길게 잡으면 불필요한 요청이 늘어납니다.
Q. 실행해보기 전에 판정만 확인할 수 있나요?
그렇게 만드는 걸 권합니다. 저는 --dry-run 옵션을 넣어서, 실제 요청을 보내지 않고 "무엇을 왜 하기로 했는지"만 출력하게 했어요. 자동으로 도는 것일수록 사람이 미리 들여다볼 수 있어야 합니다.
Q. 지금 완전히 안전한가요?
아직 완전히 안전하다고 말할 수는 없습니다. 타이머가 매일 실행되는 기록과 D-51 건너뜀 판정까지는 확인했습니다. 하지만 실제 토큰 갱신 성공은 만료 10일 전 갱신 창에 들어가야 검증됩니다. 지금 확인된 것은 "예약 실행과 사전 판정이 정상"까지입니다.
실행 체크리스트
- 토큰 만료일을 기록만 하는지, 갱신도 하는지 코드로 확인한다
- 갱신 코드가 주기 실행에 실제로 걸려 있는지
systemctl list-timers로 본다 LAST칸이 비어 있지 않은지, 즉 한 번이라도 돌았는지 확인한다- 갱신 창을 만료 당일이 아니라 여러 날 전부터로 잡는다
- 발급 직후 제한 시간에 여유를 둔다
- 갱신 실패 시 기존 토큰을 덮어쓰지 않도록 만든다
- 점검할 때 서비스 유닛의
ExecStart경로를 그대로 따라간다
참고한 공식 문서
- Threads 장기 토큰 발급과 갱신 조건: Threads long-lived tokens
- 타이머 설정 —
OnCalendar,Persistent,RandomizedDelaySec: systemd.timer 매뉴얼 - 시간 표기법: systemd.time 매뉴얼
Type=oneshot,SuccessExitStatus=: systemd.service 매뉴얼
다음 글 예고
다음은 서버에 무료로 HTTPS를 붙이는 이야기입니다. 인증서는 자동 갱신되는데 환경변수가 반영되지 않아 한참 헤맸던 함정이 하나 있었어요. 이것도 오류 없이 조용히 어긋나는 종류였습니다.
참고 자료
출처 확인일: 2026-08-21
댓글