Watch at Ease세상을 읽고, 일상의 답을 찾다.

기술 · 실용 가이드

Python 3.12에서 cached_property가 두 번 실행되나요? 정확성에 필요할 때만 단일 실행을 추가하세요

Python 3.12는 cached_property의 기존 잠금을 제거했습니다. 중복 실행이 해로운지 확인한 뒤 인스턴스별 잠금으로 성공한 값만 게시하세요.

동시 접근과 멱등성에 따라 일반 cached_property와 인스턴스별 single-flight를 고르는 결정도.
직접 제작한 설명용 개념도: 중복 실행이 정확성이나 비용에 영향을 줄 때만 동기화합니다. Watch at Ease

캐시 구조가 일관된 것과 getter가 한 번만 실행되는 것은 다릅니다. Python 3.12 이후 같은 인스턴스의 첫 접근이 겹치면 값이 저장되기 전에 두 스레드가 모두 getter를 실행할 수 있습니다.

무조건 잠금을 넣지 마세요. 불변 필드의 순수하고 멱등인 계산은 중복돼도 주로 시간만 듭니다. 결제, 파일 생성, 외부 ID, 공유 상태 변경이나 객체 정체성이 걸리면 단일 실행을 별도 정확성 요구로 취급해야 합니다.

1. 설계를 바꾸기 전에 경합 재현

배리어로 두 스레드가 같은 인스턴스를 처음 읽게 합니다. getter 시작에서 횟수를 올리고 잠시 기다리면 Python 3.12 이상에서 최종 값이 같아도 두 번 실행될 수 있습니다.

Python 버전, 같은 인스턴스인지, 프레임워크가 다른 cached_property를 쓰는지 기록하세요. 서로 다른 두 인스턴스를 만드는 시험은 다른 문제입니다.

2. 비용보다 결과로 분류

불변 값의 순수 계산은 보통 멱등이라 중복은 시간 문제입니다. 비싼 계산을 합치는 것은 성능 선택입니다.

DB 삽입, 결제, 파일, 외부 요청, 카운터와 등록은 정확성 문제입니다. 가능하면 멱등 키나 트랜잭션도 쓰세요. 프로세스 내부 잠금은 분산 환경의 정확히 한 번을 보장하지 않습니다.

3. 인스턴스 잠금 안에서 값 저장

일반 property, 전용 센티널, 캐시 필드와 인스턴스 잠금을 사용합니다. 빠른 경로는 값을 반환하고, 느린 경로는 잠금 뒤 재확인하여 계산·저장 후 해제합니다.

cached_property getter 안만 잠그고 데코레이터가 잠금 밖에서 저장하게 두면 작은 경합 창이 남을 수 있습니다.

4. 실패와 무효화 의미 보존

계산이 성공한 뒤에만 대입합니다. 예외가 나면 센티널이 남아 다음 접근이 재시도합니다. 덜 초기화된 가변 객체를 먼저 공개하지 마세요.

무효화도 같은 잠금 안에서 하고 진행 중인 독자가 이전 값을 써도 되는지 정하세요. None이 유효한 결과면 센티널로 쓰면 안 됩니다.

5. Python 3.11 경합을 되살리지 않기

디스크립터나 클래스 잠금은 무관한 인스턴스를 직렬화해 제거된 설계 문제를 되살립니다. 접근마다 새 잠금을 만들면 상호 배제가 되지 않습니다.

인스턴스별 잠금 비용이 크다면 선계산, 불변화, 요청 병합 캐시 또는 부작용 재설계를 검토하세요.

6. 네 가지 시험으로 검수

같은 인스턴스의 두 동시 읽기는 한 번 계산, 두 인스턴스는 겹쳐 실행, 첫 실패 뒤 재시도, 잠금 안 무효화 뒤 새 값 하나만 게시되는지 확인합니다.

지원하는 모든 Python 버전에서 실행하고 과거 비공개 구현이 아니라 애플리케이션의 동시성 계약을 검증하세요.

센티널 확인, 잠금, 재확인, 계산, 해제 전 저장의 인스턴스별 흐름.
직접 제작한 설명용 개념도: 잠금 뒤 재확인과 잠금 내 저장이 중복 초기화를 막습니다. Watch at Ease