LLM 에이전트 OAuth 토큰 만료와 재인증 전략: 자동화된 워크플로우 안정성 확보
코딩아빠입니다. 이번 글은 제가 LLM 에이전트를 개발하면서 겪었던 OAuth 토큰 만료 문제와 그 해결 과정을 경험노트처럼 정리한 것입니다. 세상 모든 아빠 개발자들이 저처럼 미리미리 못 챙겨서 닥쳐야 부랴부랴 검색하는 일은 없기를 바라면서 말이죠. 처음에는 간단하게 Access Token만 갱신하면 될 줄 알았는데, Refresh Token마저 조용히 만료되거나 무효화되는 바람에 에이전트가 멈춰버리는 곤란한 상황을 여러 번 겪었습니다. 결국, 단순히 토큰을 갱신하는 것을 넘어, Refresh Token 자체의 무효화까지 대비하는 견고한 재인증 전략이 필수라는 것을 깨달았네요. 이 글은 한국어 원문을 정리한 것으로, 코드와 설정값은 그대로 재현 가능합니다.
이 글에서 다루는 것
- OAuth Access Token과 Refresh Token의 수명 주기 및 만료 원리
- Refresh Token마저 무효화될 때 발생하는 문제점과 그에 대한 대비책
- 자동화된 워크플로우에서 OAuth 토큰을 안전하게 저장하고 관리하는 방법
- Refresh Token 갱신 실패 시, 사용자에게 자동으로 재인증을 요청하는 흐름 구축
- 구현된 토큰 관리 시스템의 유효성을 검증하는 실질적인 테스트 방법
이런 분께 — LLM 에이전트나 자동화된 워크플로우를 구축하며 외부 서비스 연동에 OAuth를 사용하는 개발자 · 난이도는 중급 정도
LLM 에이전트, 조용히 멈춰버린 이유: OAuth 토큰 만료
저희 집에서 열심히 돌아가던 LLM 에이전트가 어느 날 갑자기 Blogger API 연동을 멈춰버렸습니다. 포스팅 스케줄을 까먹을 뻔해서 만들었는데, 에이전트마저 제 주인을 닮아 깜빡하는 건가 싶어 당황했더군요. 처음에는 네트워크 문제인가 싶어 이리저리 확인해봤지만, 에러 로그를 자세히 보니 OAuth 토큰 만료 관련 메시지가 눈에 띄었습니다. Access Token이야 수명이 짧으니 주기적으로 갱신해야 한다는 건 알고 있었죠. 그런데 문제는 그게 아니었습니다. 단순히 Access Token이 만료된 것이 아니라, Access Token을 재발급하는 데 사용되는 Refresh Token마저 작동하지 않는 상황이었어요.
자동화된 시스템에서 이런 일이 생기면 정말 골치 아픕니다. 사람이 직접 개입해서 다시 인증을 해줘야만 에이전트가 다시 움직이기 시작하거든요. 한두 번이야 괜찮지만, 이런 일이 반복되다 보니 워크플로우의 안정성이 심각하게 떨어지는 것을 체감했습니다. 이 문제를 해결하지 않고서는 장기적으로 자동화 시스템을 운영하기 어렵겠다는 생각이 들었죠. 결국, 이참에 OAuth 토큰 관리 전략을 좀 더 견고하게 다듬어야겠다고 마음먹게 되었습니다.
특히 LLM 에이전트처럼 백그라운드에서 오랜 시간 동작하는 서비스에서는 이런 '조용한 중단'이 더 치명적입니다. 에러가 바로 눈에 띄지 않고, 한참 뒤에야 문제가 발생했다는 것을 알게 되는 경우가 많거든요. 그때마다 수동으로 재인증 과정을 밟는 것은 비효율적일 뿐만 아니라, 개발자의 소중한 시간까지 잡아먹는 일이었습니다. 그래서 Refresh Token의 만료나 무효화까지 고려한 자동화된 재인증 전략이 반드시 필요하다고 판단하게 되었네요.
Access Token은 단명, Refresh Token도 영원하진 않다
OAuth 토큰은 보안상의 이유로 유효 기간이 정해져 있습니다. Access Token은 말 그대로 특정 리소스에 접근할 수 있는 권한을 부여하는 토큰인데, 보통 몇 분에서 몇 시간 정도로 수명이 매우 짧습니다. 만약 Access Token이 무제한으로 유효하다면, 탈취되었을 때 보안상 큰 문제가 발생할 수 있기 때문이죠. 그래서 Access Token이 만료되면 Refresh Token을 이용해 새로운 Access Token을 발급받아야 합니다. 여기까지는 많은 분이 아시는 내용일 겁니다.
문제는 Refresh Token도 영원히 유효한 것이 아니라는 점입니다. Refresh Token은 Access Token보다는 훨씬 길게 유지되지만, 그래도 영구적이지는 않습니다. 사용자 권한을 철회했거나, 특정 기간 동안 사용하지 않았을 때, 또는 서비스 제공자의 정책 변경 등으로 Refresh Token이 무효화될 수 있습니다. 심지어 Google 같은 경우 Refresh Token 개수에 제한을 두기도 합니다. 예를 들어, 너무 많은 Refresh Token을 발급받으면 오래된 토큰부터 자동으로 폐기되는 정책이 있을 수 있습니다. 이런 상황에서는 단순히 Refresh Token으로 갱신을 시도해도 실패하게 됩니다.
저도 처음에는 Refresh Token은 한 번 받으면 끝이라고 안일하게 생각했습니다. 하지만 실제 운영 환경에서는 이런 예상치 못한 Refresh Token 무효화 시나리오가 종종 발생하더군요. 이럴 때는 어쩔 수 없이 사용자에게 다시 인증을 요청하고, 새로운 Refresh Token을 발급받아야 합니다. 이 과정을 어떻게 하면 자동화 시스템에 녹여낼 수 있을지가 핵심 과제였습니다.
단순 Refresh Token 갱신으로 충분할 줄 알았는데...
처음 문제를 인지했을 때, 저는 Access Token이 만료되면 Refresh Token으로 갱신하는 로직만 구현하면 충분할 거라고 생각했습니다. 파이썬에서는 google-auth-oauthlib 라이브러리를 사용하면 이 과정이 아주 간단하게 처리됩니다. 아래 코드처럼 credentials.refresh() 메서드만 호출하면 되거든요.
def refresh_oauth_token(refresh_token):
# Google OAuth 예시
flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
'client_secret.json',
scopes=['https://www.googleapis.com/auth/blogger'],
redirect_uri='YOUR_REDIRECT_URI')
credentials = flow.credentials
credentials.refresh_token = refresh_token
credentials.refresh(google.auth.transport.requests.Request())
return credentials
이 코드는 주어진 Refresh Token을 이용해 새로운 Access Token을 발급받는 기능을 수행합니다. 처음 몇 번은 이 방식으로 잘 작동하는 것을 확인하고, '아, 이제 문제없겠구나!' 하고 안심했더군요. 하지만 앞서 언급했듯이, Refresh Token 자체의 유효 기간이 만료되거나 어떤 이유로든 무효화되는 상황이 발생하자, 이 refresh() 메서드 호출 자체가 실패하는 문제가 다시 불거졌습니다. 이 경우에는 google.auth.exceptions.RefreshError 같은 예외가 발생하며 시스템이 멈춰버렸습니다.
단순히 Access Token 갱신에만 초점을 맞추다 보니, Refresh Token 자체의 생명 주기를 간과한 것이죠. 결국, 이 예외를 처리하고 다음 스텝으로 넘어갈 수 있는 더 견고한 로직이 필요하다는 것을 깨닫게 되었습니다. 단순히 오류를 뱉어내고 멈추는 것이 아니라, 오류 발생 시 자동으로 복구 시나리오를 가동해야만 진정한 자동화 시스템이라고 할 수 있겠더군요.
Refresh Token 무효화 시나리오, 자동 재인증 흐름 만들기
Refresh Token 갱신마저 실패하는 상황에 대비하기 위해, 저는 예외 처리 로직을 강화하고 자동 재인증 흐름을 구축했습니다. 핵심은 google.auth.exceptions.RefreshError를 잡아내는 것입니다. 이 예외가 발생하면 현재 가지고 있는 Refresh Token은 더 이상 유효하지 않다는 뜻이므로, 해당 토큰 정보를 데이터베이스에서 즉시 폐기해야 합니다. 그리고 사용자에게 새로운 인증을 요청해야 하죠.
try:
# API 호출 시도
response = service.api_call()
except google.auth.exceptions.RefreshError as e:
# Refresh Token 만료 또는 무효화
print(f"Refresh Token 오류 발생: {e}. 토큰을 폐기하고 재인증이 필요합니다.")
delete_invalid_token_from_db(user_id)
auth_url = generate_new_authorization_url(scopes)
send_reauthentication_request(user_id, auth_url)
위 코드를 보면, RefreshError 발생 시 delete_invalid_token_from_db 함수를 호출해 무효화된 토큰을 저장소에서 지워버립니다. 그리고 generate_new_authorization_url 함수를 통해 새로운 인증 URL을 생성합니다. 이 URL은 사용자가 다시 Google에 로그인하여 권한을 부여하도록 유도하는 역할을 합니다. 마지막으로 send_reauthentication_request 함수를 호출하여 사용자에게 이 재인증 URL을 어떤 방식으로든(이메일, 알림 등) 전달하게 됩니다. 이 과정을 통해 수동 개입을 최소화하고, 시스템이 스스로 복구 과정을 시작할 수 있도록 만들었습니다.
이런 흐름을 통해 에이전트의 작동 중단 시간을 최소화하고, 개발자가 일일이 토큰 문제를 해결하러 가는 수고를 덜 수 있게 됩니다. 중요한 것은 사용자에게 '왜' 재인증이 필요한지, '어떻게' 해야 하는지 명확하게 안내하는 것입니다. 그래야 사용자가 혼란 없이 재인증 절차를 완료하고, 에이전트가 다시 정상 작동할 수 있습니다.
.ba-pc{display:none}.ba-mo{display:block}@media (min-width:768px){.ba-pc{display:block}.ba-mo{display:none}}
토큰은 물론, 재인증을 위한 정보까지 꼼꼼히 저장하기
자동화된 재인증 흐름을 구현하려면, 단순히 Access Token과 Refresh Token만 저장해서는 안 됩니다. 토큰을 갱신하거나 재인증 URL을 생성할 때 필요한 부가 정보들도 함께 저장해야 합니다. 예를 들어, 어떤 scope로 인증을 받았는지, 어떤 client_id와 client_secret을 사용했는지 같은 정보들 말이죠. 저는 이런 정보들을 데이터베이스 테이블에 함께 저장하는 방식으로 관리했습니다. 아래는 제가 사용한 oauth_tokens 테이블의 스키마 예시입니다.
CREATE TABLE oauth_tokens (
user_id VARCHAR(255) PRIMARY KEY,
access_token TEXT NOT NULL,
refresh_token TEXT NOT NULL,
token_uri TEXT NOT NULL,
client_id TEXT NOT NULL,
client_secret TEXT NOT NULL,
scopes TEXT NOT NULL,
expires_at DATETIME NOT NULL
);
여기서 user_id는 어떤 사용자의 토큰인지 식별하기 위한 값이고, access_token과 refresh_token은 당연히 필수입니다. token_uri, client_id, client_secret은 Google OAuth 흐름에서 토큰을 갱신하거나 재인증 URL을 만들 때 필요한 정보들이고요. 특히 scopes 필드는 아주 중요한데요, 사용자가 처음 인증할 때 어떤 권한(예: Blogger API 쓰기 권한)을 부여했는지 기록해두어야 나중에 재인증을 요청할 때 동일한 권한을 다시 요청할 수 있습니다. 만약 이 정보를 잃어버리면, 재인증 시 필요한 권한을 정확히 요청하지 못해서 또 다른 문제가 발생할 수 있습니다.
expires_at 필드는 Access Token의 만료 시간을 기록하는 용도입니다. 이 정보를 이용하면 Access Token이 만료되기 전에 미리 갱신을 시도하는 선제적인 전략을 세울 수도 있습니다. 이렇게 필요한 모든 정보를 한곳에 모아두면, 토큰 관리 로직이 훨씬 간결해지고 안정적으로 운영될 수 있습니다.
정말 잘 작동할까? 만료 및 무효화 시나리오 검증
아무리 좋은 로직을 만들었어도 실제로 잘 작동하는지 확인하는 과정은 필수입니다. 저는 두 가지 주요 시나리오를 테스트했습니다. 첫 번째는 Access Token 만료 후 Refresh Token으로 정상적으로 갱신되는지 확인하는 것이었습니다. Access Token의 유효 기간을 일부러 짧게 설정하거나, Access Token을 수동으로 삭제한 뒤 다음 API 호출 시 자동으로 새 Access Token이 발급되는지 로그를 통해 확인했습니다. 이때, API 호출 전에 credentials.valid 속성을 체크하여 False라면 credentials.refresh()가 호출되는지 살펴보면 됩니다.
두 번째이자 더 중요한 테스트는 Refresh Token이 무효화되었을 때 시스템이 제대로 대응하는지 확인하는 것이었습니다. 이를 위해 저는 Google 계정 설정에 들어가서 직접 해당 애플리케이션의 권한을 철회하거나, 데이터베이스에 저장된 Refresh Token을 강제로 잘못된 값으로 변경했습니다. 그런 다음 API 호출을 시도했을 때, 시스템이 google.auth.exceptions.RefreshError를 정확히 잡아내는지 확인했습니다. 로그에 'Refresh Token 오류 발생' 메시지가 출력되고, 데이터베이스에서 해당 토큰 정보가 삭제되는지, 그리고 사용자에게 재인증 URL이 포함된 알림이 제대로 발송되는지 면밀히 지켜봤습니다.
재인증 URL을 통해 다시 인증 과정을 거친 후, LLM 에이전트가 정상적으로 Blogger API와 연동되어 포스팅을 발행하는 것까지 확인해야 비로소 안심할 수 있었습니다. 이 과정을 통해 제가 구축한 토큰 관리 시스템이 단순한 Access Token 갱신을 넘어, Refresh Token의 무효화라는 예외 상황까지 견고하게 처리한다는 것을 입증할 수 있었네요.
마무리
LLM 에이전트나 자동화된 시스템을 운영하면서 OAuth 토큰 관리는 단순히 Access Token을 갱신하는 것을 넘어, Refresh Token의 만료나 무효화까지 고려해야 하는 복잡한 문제입니다. 이번에 겪은 경험을 통해 예외 상황까지 대비하는 견고한 재인증 전략이 워크플로우의 안정성과 운영 효율성을 크게 높여준다는 것을 깨달았습니다. 결국, '설마' 하는 마음으로 간과했던 부분이 시스템 전체를 멈추게 할 수 있다는 것을 다시 한번 배우게 되었네요. 다음번에는 미리미리 잘 챙겨서 이런 시행착오를 줄여나가야겠습니다. If the result differs, check the version first — that is the usual cause.

Top comments (0)