전체 애플리케이션을 Rust로 다시 작성하는 대신, Discord의 Staff Engineer인 Lily Mara는 Python과 같은 동적 언어에서 리소스를 가장 많이 소모하는 함수들을 점진적으로 Rust로 옮기고 동일한 프로세스 안에서 두 구현을 연결할 것을 제안한다. Mara는 Discord 사용자에게 매일 수백억 건의 알림을 전송하는 분산 시스템을 구축한 경험과 자신의 저서 Refactoring to Rust를 바탕으로 InfoQ를 통해 공개된 세션에서 이 방법론을 소개했다.
핵심 아이디어는 Rust가 더 빠르다는 이유만으로 한 언어를 다른 언어로 교체하는 것이 아니라, 개선했을 때 가장 큰 효과를 내는 시스템의 좁은 부분을 식별하는 데 있다. 이 접근법을 사용하면 변경 범위를 줄이고, 새로운 동작을 기존 동작과 비교해 테스트하며, 결과의 일치를 보장하기 어려운 경우에는 원래 구현을 유지할 수 있다.
왜 전면적인 재작성으로 시작하지 않는가?
Mara는 오래된 시스템 전체를 더 새로운 언어로 재구축하고 싶은 열정은 이해할 수 있지만, 실무적으로 큰 위험이 따른다고 말한다. 재작성 프로젝트는 일정을 초과하거나 예상보다 복잡해질 수 있으며, 기존 시스템이 이미 해결한 오류를 다시 도입할 수도 있다. 또한 오래된 코드가 항상 그 연식 때문에 복잡한 것은 아니다. 실제 사용 과정에서 축적된 제약과 세부 사항, 그리고 새로운 프로젝트로 옮기기 어려운 조직의 지식을 반영할 수도 있다.
이 세션은 성능 문제를 프로그래밍 언어 하나로만 환원하는 것에 대해서도 경고한다. 병목의 원인은 코드 한 줄의 실행 비용이 아니라 데이터베이스 스키마, 쿼리와 캐시 패턴, 또는 서비스 설계에 있을 수 있다. 따라서 Rust를 활용한다고 해서 재작성만으로 아키텍처상의 병목이 자동으로 해결되는 것은 아니다.
FFI를 통한 리팩터링이란 무엇인가?
이 방법론은 Mara가 FFI refactoring이라고 부르는 방식, 즉 다른 언어로 함수 하나 또는 함수의 작은 부분을 다시 작성하고 언어 간 연동 인터페이스를 통해 기존 애플리케이션에 연결하는 방식을 사용한다. 제시된 예제에서는 Flask 애플리케이션이 HTTP 요청을 받고 JSON을 디코딩한 뒤, Rust로 작성된 통계 함수에 데이터를 전달한다. 이후 결과를 Python으로 돌려보내 응답을 전송한다.
연결에는 여러 시스템과 언어 사이에서 사실상 공통 언어로 작동하는 C 인터페이스가 사용된다. Python의 경우 PyO3 프로젝트는 Python에서 가져올 수 있는 모듈을 만드는 도구를 제공하며, Maturin을 사용해 이 모듈을 빌드할 수 있다. pyfunction 및 pyclass와 같은 프로그래밍 속성은 Rust의 함수와 데이터 구조를 Python에서 호출할 수 있는 인터페이스로 변환한다.
적절한 함수를 선택하고 효과를 측정하기
이 실험은 호출 빈도가 높거나 호출 비용이 큰 함수를 찾을 것을 권한다. 함수가 매번 실행될 때는 비교적 저렴하더라도 API 핸들러 앞에 있는 검증 로직처럼 모든 요청에서 실행될 수 있다. 반대로 드물게 실행되지만 매우 비싼 작업도 있을 수 있다. 기준은 특정 함수가 느려 보인다는 인상이 아니라 CPU 시간에 미치는 누적 영향이다.
통계 예제에서 Rust 구현은 Python을 통해 동일한 함수만 측정했을 때 100배보다 조금 빠른 결과를 보였다. 이 테스트에서도 애플리케이션은 Python 인터프리터를 통해 실행되었다. 그러나 Flask, JSON 직렬화와 역직렬화를 포함한 전체 HTTP 핸들러를 측정했을 때 개선 폭은 약 15%에 그쳤다. 원래 함수의 실행 시간은 약 86마이크로초였는데, 한 번만 보면 짧은 시간이지만 대규모로 반복되면 중요한 비용이 될 수 있다.
부분 측정과 전체 측정 사이의 이 차이는 이 세션의 가장 중요한 교훈 중 하나다. 함수의 속도 향상을 기록하는 것만으로는 충분하지 않으며, 필요에 따라 네트워크나 웹 프레임워크, 데이터 직렬화를 포함해 실제 사용 경로를 나타내는 전체 테스트를 수행해야 한다.
기능적 호환성, 테스트 및 제약
예제를 다시 구현한 결과 Python 통계 라이브러리와 Rust 라이브러리의 결과가 서로 달랐다. 한 라이브러리는 사분위수를 정확하게 계산한 반면, 다른 라이브러리는 대규모 데이터 집합에 적합한 추정치를 사용했으며, 소수 반올림에서도 미세한 차이가 나타났다. 따라서 라이브러리를 교체한다고 해서 동일한 동작이 자동으로 유지된다고 가정해서는 안 된다.
동일한 결과가 필수라면 다른 라이브러리를 찾거나, 원래 알고리즘을 Rust로 다시 구현하거나, 민감한 부분을 Python에 남겨 둘 수 있다. 함수 수준으로 이전하는 방식의 장점은 구성 요소를 별도 서비스로 분리해 네트워크 통신과 새로운 운영 비용을 추가하지 않고도 동일한 프로세스 안에서 두 언어를 조합할 수 있다는 점이다.
테스트에는 직접적인 Rust 테스트, Flask 핸들러를 대상으로 하는 기존 Python 테스트, 두 구현의 결과를 비교하는 테스트, 그리고 적절한 경우 속성 기반 무작위 테스트가 포함된다. 그러나 네이티브 코드를 추가하면 운영상의 복잡성이 발생한다. 개발 환경에 Rust 컴파일러가 필요하거나 운영체제와 아키텍처에 호환되는 바이너리가 필요하며, 배포가 더 복잡해지고 새로운 오류가 나타날 가능성도 있다.
실무적으로 이 방법론은 기존 시스템에서 선택한 부분을 비교적 낮은 위험으로 개선할 수 있는 경로를 제공하지만, 아키텍처 분석과 호환성 테스트, 전체 측정의 필요성을 없애지는 않는다. 실제 절감 효과는 고립된 함수 benchmark가 아니라 전체 서비스 경로에서 개선으로 나타날 때만 입증된다.
뉴스 출처
InfoQ - Architecture Articles
원문 보기 ↗