
문제상황
프로젝트를 운영하던 중, 로그에 주기적으로 아래와 같은 에러가 발생했습니다.
[ERROR] token_manager: Thread error during token refresh
처음에는 일시적인 네트워크 문제나 인증 서버의 응답 지연이라고 생각했지만, 해당 로그가 계속 같은 주기로 쌓이고 있었고, 특정 기능을 호출하지 않아도 지속적으로 에러가 발생했습니다.
결국 코드 레벨에서 로그를 추적한 결과, 해당 오류는 제가 작성한 코드가 아니라 사용 중이던 SDK 내부의 토큰 관리 스레드(token_manager)에서 발생하고 있음을 확인했습니다. 이 객체는 내부적으로 인증 토큰을 자동으로 갱신하는 token_manager를 가지고 있었고, 토큰 만료 시마다 백그라운드 스레드를 통해 지수 백오프(Exponential Backoff) 로 재시도하는 구조였습니다.
원인분석
문제의 원인은 의외로 단순했는데, SDK에서 해당 객체는 Context Manager 패턴을 사용하도록 설계되었지만, 객체의 생명주기를 Context Manager 규약에 맞게 관리하지 않았기 때문이었습니다. 해당 SDK의 객체 코드를 살펴보니 내부적으로 아래와 같은 구조를 가지고 있었습니다.
def __exit__(self, exc_type, exc_val, exc_tb):
self._shutdown() # 내부 스레드 종료
즉, 객체가 with 구문에서 사용될 때, __exit__() 메서드를 통해 _shutdown()이 자동 호출되며, 백그라운드 스레드를 정리하게 됩니다. 하지만 저는 이 객체를 재사용하기 위해, 한 세션 클래스의 멤버 변수로 저장해두고 아래와 같이 관리하고 있었습니다.
self.obj = SomeSDKObject(...)
...
# 문제가 생기면
self.obj = None
self.obj = SomeSDKObject(...)
즉, 단순히 None을 할당해버림으로써 참조는 끊겼지만, 객체의 __exit__()이 호출되지 않아 스레드가 종료되지 않은 채 남아있게 됩니다. 파이썬의 가비지 컬렉터는 내부 스레드를 강제로 종료시키지 않기 때문에, 결국 프로그램이 종료될 때까지, 해당 스레드가 계속 살아서 불필요한 요청을 보내고 있던 것입니다.
해결방안
1. 리소스 수동 관리
이 문제를 해결하기 위해 먼저 수동으로 __exit__() 함수를 호출하도록 코드를 개선했습니다. 객체를 재사용하기 위해서는 with 구문 외부에서도 해당 객체를 유지해야했고, 그러기 위해서는 context manager에서 관리되는 것과 같이 수동으로 객체의 생명주기를 관리해야 했습니다.
if self.obj:
self.obj.__exit__(None, None, None)
self.obj = SomeSDKObject()
self.obj.__enter__()
2. with 구문을 활용한 올바른 자원 관리
하지만 수동으로 관리하는 것은 근본적인 해결방법이 아니라고 생각했고, 또한 SDK가 업데이트된다면 어떤 문제가 발생할지 모르기 때문에 결국 Context Manager를 올바르게 사용하는 방식으로 리펙토링을 진행했습니다.
기존에는 요청마다 SDK 객체에서 함수를 호출하는 방식으로 구현했었는데,
Context Manager를 사용하기 위해 내부 스레드를 생성하고 1초에 1번씩 함수를 호출하여 내부 캐싱하는 방식으로 변경했습니다.
while True:
try:
with SomeSDKObject() as obj:
while True:
result = obj.do_something()
self.result = result # 내부에서 상태를 캐싱
time.sleep(1) # 주기적으로 작업 수행
# 블록을 벗어나면 자동으로 _shutdown() 호출
except Exception as e:
# 오류 발생 시 로깅 및 재시도
print(f"Error occurred: {e}")
결론
- Context Manager는 단순한 문법적 편의가 아니라, 자원 생명주기를 보장하는 규약입니다.
- 내부적으로 스레드나 소켓, 세션을 관리하는 객체는 항상 with 구문으로 사용하거나
명시적으로 종료 메서드를 호출해야합니다. - Context Manager를 무시하면, 스레드가 남거나 세션이 끊기지 않는 미묘한 버그로 이어질 수 있습니다.
참고자료
Python Docs - Context Manager Types
Python Docs - contextlib (with 문 컨텍스트를 위한 유틸리티)
'python_fastapi' 카테고리의 다른 글
| [FastAPI] 요청 단위 로깅 구현하기 - Middleware, ContextVar, Decorator (0) | 2025.10.14 |
|---|