기술 · 실용 가이드
Python ContextVar가 스레드 풀에서 사라질 때: 전파와 변경값 반환 확인
Task, to_thread, 스레드 풀과 프로세스의 경계를 구분하고, 제출할 때마다 컨텍스트를 복사해 변경값을 명시적으로 반환하세요.

ContextVar가 전달되는 방식은 실행이 넘어가는 동시성 경계에 따라 다릅니다. asyncio Task는 작업의 컨텍스트를 유지하고 asyncio.to_thread는 현재 Context를 작업 스레드로 복사합니다. ThreadPoolExecutor에 직접 제출하거나 loop.run_in_executor를 호출할 때는 프레임워크가 처리해 주지 않는 한 명시적인 복사가 필요합니다.
작업 스레드에 컨텍스트를 전달하는 것과 그 안의 변경을 호출자에게 되돌리는 것은 별개입니다. 제출할 때마다 새 복사본을 만들고 그 안에서 한 번의 호출을 실행하세요. 변경값은 일반 결과로 반환하고, 프로세스 간 전달은 따로 처리하며, 동시 요청 두 개로 격리를 검증하세요.
1. 먼저 실행 경계를 확인하세요
호출이 asyncio Task 안에 머무르는지, asyncio.to_thread나 loop.run_in_executor를 사용하는지, ThreadPoolExecutor에 직접 제출되는지, 별도 프로세스로 넘어가는지 기록하세요. 함수명이나 프레임워크 이름만 보고 동작을 추측하지 마세요.
경계 양쪽에서 민감한 정보가 없는 요청 식별자를 기록하세요. 값이 없으면 전파 실패, 이전 요청의 값이 보이면 작업 스레드의 상태 누출을 뜻합니다. 명시적으로 복사한 뒤에만 기대한 값이 보인다면 그 래퍼가 전파를 담당하는 것입니다.
2. 스레드에 제출할 때마다 복사하세요
스레드에 직접 제출할 때는 요청을 보내는 쪽에서 ctx=contextvars.copy_context()를 실행하고 ctx.run과 대상 함수, 인자를 제출하세요. 오래 사용하는 풀을 만들 때 한 번만 복사하지 말고, 제출할 때마다 새 복사본을 만드세요.
두 작업 스레드가 같은 Context에 동시에 진입하게 하지 마세요. 이미 진입한 Context에 다시 들어가면 RuntimeError가 발생합니다. 호출마다 새로 복사하면 풀을 시작할 때의 상태가 아니라 해당 요청 시점의 상태를 담을 수 있습니다.
3. 전파와 변경값 반환을 분리하세요
asyncio.to_thread는 작업 스레드 방향의 전파를 제공합니다. 프레임워크의 보조 함수도 run_in_executor를 비슷하게 감쌀 수 있습니다. 하지만 작업 스레드에서 ContextVar에 대입한 값이 기다리는 Task로 자동 반영되지는 않습니다.
작업 스레드가 새로운 테넌트, 추적 스팬 또는 인가 결과를 계산했다면 일반 데이터로 반환하세요. 호출자가 자신의 ContextVar를 설정하거나 복원할지 결정해야 합니다. 임시 변경이라면 set()이 반환한 token을 저장하고 try/finally에서 reset()으로 복원하세요.
4. 스레드와 프로세스를 구분하세요
CPython 논의에서 run_in_executor에 통일된 자동 전파 규칙을 적용하지 않은 이유 중 하나는 ProcessPoolExecutor도 사용할 수 있지만 Context는 pickle로 직렬화할 수 없다는 점입니다. 스레드 전용 래퍼를 프로세스 경계에 적용하지 마세요.
프로세스에는 작고 직렬화 가능한 요청 정보를 명시적으로 전달하고, 작업 프로세스에서 검증한 뒤 결과를 반환하세요. 자격 증명과 큰 가변 객체는 어디서나 읽는 ContextVar 상태로 취급하지 말고 프로세스 API의 보안 및 소유권 규칙을 따르세요.
5. 실제 호출 경로에서 격리를 검증하세요
실제 운영에 쓰는 보조 함수를 통해 식별자가 A와 B인 호출을 겹쳐 실행하고, 동기화 장벽으로 두 작업 스레드가 동시에 활성화되게 하세요. A는 A만, B는 B만 보고, 래퍼가 없는 대조 호출은 문서에 명시된 기본값을 얻는지 확인하세요.
각 작업 스레드에서 서로 다른 지역 값을 설정해 반환하게 하세요. await 또는 Future.result() 이후에도 호출자가 직접 변경하거나 복원하기 전까지 원래 식별자를 유지하는지 확인하세요. 오류 및 종료 경로에서도 반복해 풀의 스레드에 이전 요청 값이 남지 않도록 하세요.
