2. Rust와 Polars
약 7분 읽기
Polars Rust 완전 정복 매뉴얼
2. Rust와 Polars
2.1 Rust 언어 소개 (왜 Rust인가)
Rust는 2010년 Mozilla에서 Graydon Hoare가 시작한 시스템 프로그래밍 언어로, 2015년 1.0 버전이 발표된 이후 빠르게 성장하여 2024년 기준 Stack Overflow 개발자 설문에서 “가장 사랑받는 언어” 부문 1위를 연속으로 차지하고 있는 언어입니다.
Rust가 Polars의 구현 언어로 선택된 이유는 크게 세 가지입니다:
2.1.1 성능
Rust는 가비지 컬렉터(GC)가 없는 대신, 소유권(ownership) 이라는 메커니즘으로 메모리를 컴파일 타임에 관리합니다. 덕분에 C/C++와 필적하는 성능을 내면서도 런타임에 GC로 인한 일시 정지(stop-the-world)가 발생하지 않습니다. 데이터 처리처럼 대용량 메모리를 다루는 작업에서는 이것이 결정적인 장점이 됩니다.
2.1.2 메모리 안전성
Rust는 컴파일 타임에 메모리 안전성을 보장합니다. 널 포인터(null pointer) 역참조, 이중 해제(double free), 댕글링 포인터(dangling pointer) 등 흔한 메모리 버그가 컴파일 에러로 잡힙니다. 이는 데이터 분석 라이브러리처럼 복잡한 자료구조를 다루는 코드에서 특히 중요한 이점입니다.
2.1.3 데이터 경쟁 없는 병렬성
Rust의 “두려움 없는 동시성(fearless concurrency)“이라는 표어처럼, 컴파일러가 데이터 경쟁(data race)을 정적으로 방지해 줍니다. Polars가 다중 스레드로 안전하게 데이터를 처리할 수 있는 것은 이 특성 덕분입니다. 다른 언어였다면 수많은 락(lock)과 동기화 코드가 필요했을 작업을, Rust에서는 컴파일러의 도움으로 깔끔하게 병렬화할 수 있습니다.
2.1.4 생태계와 도구
Cargo라는 훌륭한 패키지 매니저와 빌드 시스템, rustfmt(코드 포맷터), clippy(린터), rust-analyzer(LSP) 등 모범적인 도구가 기본 제공됩니다. 또한 docs.rs를 통한 자동 문서화, crates.io를 통한 크레이트(패키지) 배포 등 생태계가 매우 정돈되어 있습니다.
2.2 Polars의 Rust 구현 특징
Polars가 Rust로 구현되면서 얻은 구체적인 이점을 살펴봅니다.
2.2.1 제네릭과 트레이트를 활용한 타입 시스템
Polars는 Rust의 강력한 제네릭(generics)과 트레이트(trait) 시스템을 활용하여, 다양한 데이터 타입에 대해 타입 안전하면서도 중복 코드가 없는 구현을 갖추고 있습니다. 예를 들어 ChunkedArray<T>는 T에 대해 제네릭이며, 정수/부동소수/문자열 등 각 타입에 대해 동일한 인터페이스를 제공하면서도 성능 저하가 없습니다.
2.2.2 Rayon을 활용한 데이터 병렬성
Polars는 Rust의 데이터 병렬성 라이브러리인 Rayon을 적극 활용합니다. par_iter() 같은 메서드를 사용하면, 단일 스레드 반복문을 거의 수정 없이 병렬 버전으로 바꿀 수 있습니다. Polars 내부의 거의 모든 컬럼 연산은 병렬화되어 있습니다.
2.2.3 매크로 기반 표현식 DSL
Polars는 Rust 매크로를 활용하여, SQL이나 Pandas 체인과 유사한 표현식 DSL을 구현했습니다. 예를 들어 col("age"), lit(30), col("a") + col("b") 같은 표현식은 사실 매크로와 연산자 오버로딩의 결과입니다. 이를 통해 Rust의 정적 타입 시스템 안에서도 유연한 쿼리 표현이 가능합니다.
2.2.4 불필요한 추상화 비용 제거
Rust는 “영 비용 추상화(zero-cost abstraction)” 철학을 따릅니다. 트레이트, 제네릭, 클로저 같은 고수준 추상화가 런타임 비용을 유발하지 않습니다. Polars는 이를 통해 추상적이고 깔끔한 API를 제공하면서도, 직접 손으로 최적화한 C 코드와 맞먹는 성능을 냅니다.
2.3 Python Polars vs Rust Polars
Polars는 동일한 Rust 핵심 엔진을 사용하면서도, Python과 Rust에서 각각 다른 API를 노출합니다. 두 환경의 차이를 이해하는 것이 중요합니다.
2.3.1 API 형태의 차이
Python Polars는 Python의 관례를 따릅니다:
import polars as pl
df = pl.read_csv("data.csv")
result = (
df.lazy()
.filter(pl.col("age") > 30)
.group_by("city")
.agg(pl.col("salary").mean())
.collect()
)
Rust Polars는 Rust의 관례를 따릅니다:
use polars::prelude::*;
let df = CsvReader::new("data.csv".into())
.finish()?;
let result = df
.lazy()
.filter(col("age").gt(lit(30)))
.group_by([col("city")])
.agg([col("salary").mean()])
.collect()?;
두 코드는 본질적으로 동일한 작업을 수행하며, 동일한 쿼리 최적화기와 실행 엔진을 사용합니다. 다만 Python 버전은 조금 더 간결하고 REPL 친화적이며, Rust 버전은 타입 어노테이션과 에러 처리(? 연산자)가 추가됩니다.
2.3.2 언제 Rust Polars를 써야 할까?
Rust에서 Polars를 직접 사용하는 것이 유리한 경우는 다음과 같습니다:
- 성능이 극도로 중요한 프로덕션 시스템: Python 런타임 오버헤드나 GIL(Global Interpreter Lock)을 피하고 싶을 때.
- Rust 애플리케이션에 데이터 분석 기능을 통합할 때: 웹 서버, CLI 도구, 임베디드 시스템 등에서 Polars를 라이브러리로 사용.
- 타입 안전성이 중요할 때: 컴파일 타임에 데이터 타입 오류를 잡고 싶을 때.
- 단일 바이너리 배포: Python 인터프리터 없이 실행 파일 하나로 배포하고 싶을 때.
- Polars 자체를 확장하거나 기여할 때: 커스텀 연산자, 플러그인, 새로운 파일 포맷 지원 등.
반면 Python Polars가 유리한 경우는:
- 빠른 프로토타이핑과 탐색적 분석: Jupyter 노트북과 같은 인터랙티브 환경.
- 데이터 과학 생태계와의 통합: scikit-learn, matplotlib, seaborn 등과 결합.
- 팀 대부분이 Python에 익숙한 경우.
이 책은 Rust Polars에 집중하지만, 개념의 대부분은 Python 버전에도 그대로 적용됩니다. 핵심 엔진이 동일하기 때문입니다.