11from __future__ import annotations
22
3- from datetime import datetime
3+ from datetime import datetime , timedelta
44from logging import getLogger
5+ from time import time
56from traceback import format_exception
67from typing import Any , Literal
78
1415logger = getLogger (__name__ )
1516
1617DEFAULT_TIMEOUT = 5
18+ # PortOne v1 access_token 의 공식 TTL 은 발행 시점 +30분 (developers.portone.io 명시).
19+ # 응답에 `expired_at` (unix epoch sec) 가 포함되며, 만료 직전 재발급 race 를 막기 위한 안전 마진.
20+ TOKEN_REFRESH_MARGIN = timedelta (seconds = 30 )
21+ # `expired_at` 가 응답에 누락된 비정상 케이스의 보수적 fallback (공식 TTL 30분보다 짧게).
22+ TOKEN_FALLBACK_TTL = timedelta (minutes = 5 )
1723RequestMethodType = Literal ["GET" , "OPTIONS" , "HEAD" , "POST" , "PUT" , "PATCH" , "DELETE" ]
1824
1925
@@ -29,6 +35,8 @@ class PortOneClient:
2935 def __init__ (self , timeout : TimeoutTypes = DEFAULT_TIMEOUT ) -> None :
3036 self ._timeout = timeout
3137 self ._client : Client | None = None
38+ self ._cached_token : str | None = None
39+ self ._cached_token_expires_at : float = 0.0
3240
3341 @property
3442 def client (self ) -> Client :
@@ -39,6 +47,10 @@ def client(self) -> Client:
3947
4048 @property
4149 def _access_token (self ) -> str :
50+ # 만료 직전 안전 마진까지는 캐시 재사용 (PortOne 토큰 TTL 30분).
51+ if self ._cached_token and time () < self ._cached_token_expires_at - TOKEN_REFRESH_MARGIN .total_seconds ():
52+ return self ._cached_token
53+
4254 response = self .client .post (
4355 url = "/users/getToken" , json = {"imp_key" : settings .PORTONE .imp_key , "imp_secret" : settings .PORTONE .imp_secret }
4456 )
@@ -47,15 +59,18 @@ def _access_token(self) -> str:
4759 resp_serializer = PortOneV1ResponseSerializer .from_response (response )
4860 resp_serializer .is_valid (raise_exception = True )
4961
50- if not (access_token := resp_serializer .validated_data ["response" ]["access_token" ]):
62+ resp = resp_serializer .validated_data ["response" ]
63+ if not (access_token := resp .get ("access_token" )):
5164 raise ValueError ("PortOne access_token 값이 존재하지 않습니다." )
5265
53- return access_token
54-
5566 except Exception as e :
5667 logger .error (format_exception (e ))
5768 raise PortOneException ("PortOne AccessToken 획득에 실패했습니다." ) from e
5869
70+ self ._cached_token = access_token
71+ self ._cached_token_expires_at = float (resp .get ("expired_at" ) or (time () + TOKEN_FALLBACK_TTL .total_seconds ()))
72+ return access_token
73+
5974 def _request (
6075 self ,
6176 method : RequestMethodType ,
0 commit comments