결론 (Conclusion)

PyO3가 Rust의 라이프타임(lifetime)을 사용하는 구조체를 직접 Python에 노출하지 못하는 것은 처음에는 한계처럼 보일 수 있습니다. 하지만 Rust 표준 라이브러리와 PyO3는 이러한 한계를 극복할 강력한 도구들을 제공합니다. std::mem::takestd::mem::replace는 변경 가능한 참조(mutable reference)와 소유된 값(owned value)을 능숙하게 다룰 수 있게 해주며, ArcMutex는 공유되는 변경 가능한 데이터를 Python에 노출하는 데 매우 유용합니다. 특히 PyO3의 MutexExt는 Python과 함께 뮤텍스를 사용할 때 데드락을 방지하는 필수적인 도구입니다.


주요 내용 요약

이 문서는 Django의 템플릿 언어를 Rust로 재구현하는 프로젝트에서 Rust와 Python 간에 변경 가능한(mutable) 데이터를 공유하면서 마주친 기술적 문제와 그 해결 과정을 단계별로 설명합니다.

  • 배경: Django 템플릿 언어는 context라는 객체를 사용하여 템플릿에 동적 데이터를 제공합니다. 프로젝트의 Rust 구현체에서는 이 context를 Rust 구조체로 정의했으며, 템플릿 태그를 렌더링할 때 변경 가능한 참조(&mut Context)로 전달해야 합니다.

  • 초기 문제: Rust 코드의 변경 가능한 참조(&mut Context)를 커스텀 태그 실행을 위해 Python 함수로 전달해야 합니다. 하지만 Python은 Rust의 라이프타임을 이해하지 못하며, Rust-Python 연동 라이브러리인 PyO3는 소유권이 있는 값(owned value)을 요구하기 때문에 참조를 직접 전달하면 컴파일 에러가 발생합니다.

  • 해결 과정:

    1. 소유권 문제 해결: std::mem::take를 사용하여 &mut Context에서 소유권을 잠시 가져와 Python에 전달 가능한 소유된 Context 객체를 생성합니다. Python 코드 실행 후에는 std::mem::replace를 사용하여 처리된 Context를 다시 원래의 참조 위치로 돌려놓으려 시도합니다.
    2. 'Moved Value' 에러 해결: 하지만 이 과정에서 Context 객체가 Python 함수로 이동(move)된 후 다시 사용하려 할 때 "use of moved value" 컴파일 에러가 발생합니다. 이 문제를 해결하기 위해 Arc(Atomic Reference Count)를 도입하여 Context를 감쌉니다. 이를 통해 소유권을 옮기지 않고도 Python에 복제된 참조(clone)를 전달할 수 있습니다.
    3. Python의 참조 유지 시 처리: Python이 Context에 대한 참조를 계속 유지할 경우 Arc::try_unwrap을 통한 소유권 회수가 실패할 수 있습니다. 이 경우, Context 내부 데이터를 깊은 복사(deep clone)하는 clone_ref와 같은 fallback 메소드를 구현하여 데이터를 복제합니다.
    4. Python에서의 데이터 변경 허용: 최종적으로 Python 코드가 Context를 읽기만 하는 것이 아니라 변경도 할 수 있도록 Mutex를 도입합니다. Arc<Mutex<Context>> 구조를 사용하여 여러 스레드에서 안전하게 데이터에 접근하고 수정할 수 있도록 보장합니다. 이때 Python 인터프리터와의 데드락을 방지하기 위해 PyO3가 제공하는 MutexExtlock_py_attached 메소드를 사용합니다.