러스트 언어 입문 시리즈 - 9편: 매크로(Macro), 클로저(Closure), 함수형 패러다임 지원과 빌드 스크립트

지난 글에서는 러스트가 제공하는 동시성(Concurrency)과 병렬성(Parallelism) 지원을 살펴봤습니다. 이제 러스트가 제공하는 또 다른 강력한 기능들, 즉 매크로(Macro), 클로저(Closure), 그리고 함수형 패러다임을 지원하는 다양한 문법과 라이브러리, 마지막으로 프로젝트 빌드 과정에 개입할 수 있는 빌드 스크립트(Build Script) 개념을 살펴보며 러스트 생태계의 폭넓은 표현력을 확인해보겠습니다.

 

C++에 익숙한 분이라면 템플릿 메타프로그래밍, 람다 함수, CMake나 Meson을 통한 빌드 설정을 생각해볼 수 있습니다. 러스트는 이와 유사한 기능을 가지면서도 안전성과 명확한 문법, 통합된 패키지 관리 체계로 개발자 경험을 향상시킵니다.

매크로(Macro)

C++에서는 전처리기 매크로 #define을 통해 반복 코드를 줄이거나 조건부 컴파일을 하지만, 이는 텍스트 치환 방식으로 안전성이 낮고 디버깅이 어렵습니다. C++20 이후 constexpr 함수나 템플릿 메타프로그래밍을 통해 매크로 사용을 줄이는 경향이 있습니다.

러스트는 프로시저 매크로(Procedural Macro)선언 매크로(Declarative Macro)라는 안전하고 강력한 매크로 시스템을 제공합니다. 러스트 매크로는 토큰 트리(Token Tree) 단위로 작동하며, 컴파일러 파이프라인 내에서 안전하게 확장되어, 전처리기 기반의 단순 문자열 치환보다 훨씬 견고하고 파워풀합니다.

예: println! 매크로

fn main() {
    println!("Hello, {}!", "Rust");
}

println!은 매크로이며, 호출 시 문자열 포맷을 파싱하고 적절한 출력 코드를 생성합니다. 이는 C++의 std::cout와 비교할 때, 러스트 컴파일러가 매크로를 통해 형식 검사나 인자 개수 불일치 등을 컴파일 타임에 확인할 수 있다는 점에서 안전성이 높습니다.

자신만의 매크로를 정의할 수도 있습니다.

macro_rules! my_macro {
    ($val:expr) => {
        println!("매크로가 받은 값: {}", $val);
    };
}

fn main() {
    my_macro!(42);
}

macro_rules!를 이용한 선언 매크로는 패턴 매칭을 통해 인자를 파싱하고, 해당 인자를 활용한 코드를 생성합니다. 이는 C++ 매크로보다 훨씬 정교하며, 텍스트 치환이 아닌 토큰 단위 변환을 통해 안전하고 가독성 있는 메타프로그래밍을 가능케 합니다.

클로저(Closure)

C++11 이후 람다 함수를 통해 익명 함수와 캡처가 가능해졌습니다. 러스트는 이와 유사한 **클로저(Closure)**를 제공하며, 클로저는 외부 스코프 변수를 캡처할 수 있습니다. 단, 러스트의 소유권, 빌림 규칙을 그대로 따릅니다.

fn main() {
    let x = 10;
    let add_x = |y: i32| y + x; // 외부 변수 x를 캡처
    println!("{}", add_x(5)); // 출력: 15
}

클로저는 Fn, FnMut, FnOnce 세 가지 트레이트를 통해 어떤 식으로 변수를 캡처하는지 표현합니다. C++ 람다에서 [&] 혹은 [=]로 캡처 모드를 지정하는 것과 달리, 러스트는 컴파일러가 캡처 방식(불변 참조, 가변 참조, 소유권 이동)을 추론합니다. 필요하다면 명시적으로 캡처 방식이나 타입을 지정할 수도 있습니다.

 

이러한 캡처 규칙은 러스트의 메모리 안전성 보장과 결합되어, 멀티스레드 환경에서 클로저를 안전하게 공유하거나 이동할 때도 컴파일러가 문제점을 지적해줄 수 있습니다.

함수형 패러다임 지원: 이터레이터, map, filter, fold

이전 글에서 러스트의 이터레이터와 map, filter 메서드를 통해 함수형 프로그래밍 스타일을 지원하는 모습을 살짝 보여드렸습니다. 러스트는 C++20 이후 범위 기반 알고리즘(std::ranges)보다 초기부터 함수형 스타일에 익숙한 메서드 체인을 풍부하게 지원합니다.

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];

    let sum_of_even_squares: i32 = numbers.iter()
        .filter(|&x| x % 2 == 0)
        .map(|x| x * x)
        .sum();

    println!("짝수 제곱들의 합: {}", sum_of_even_squares);
}

여기서 filter, map, sum 등은 모두 이터레이터 트레이트와 연계된 기능입니다. C++에서는 람다와 std::transform, std::copy_if 등 알고리즘을 조합해야 하는 경우가 많지만, 러스트는 더 일관되고 가독성 높은 함수형 패턴을 제공합니다.

 

또한 fold를 사용하면 C++의 std::accumulate와 유사한 방식으로 임의의 축약 연산을 편리하게 정의할 수 있습니다.

let product = numbers.iter().fold(1, |acc, &x| acc * x);
println!("곱셈 결과: {}", product);

이러한 함수형 패턴은 병렬 라이브러리(Rayon)와 결합하면 쉽게 멀티코어 성능 향상을 꾀할 수도 있습니다.

빌드 스크립트(Build Script)와 환경 구성

C++ 프로젝트에서는 CMake나 Meson, Bazel 같은 빌드 도구를 사용하고, 빌드 과정에서 특정 스크립트를 실행하여 코드를 생성하거나 환경 변수를 조정하는 경우가 있습니다.

 

러스트에서는 Cargo가 빌드 시스템을 통합적으로 관리하며, 필요할 경우 **빌드 스크립트(build.rs)**를 통해 빌드 과정에 개입할 수 있습니다. build.rs는 Cargo가 빌드하기 전에 실행하는 러스트 스크립트 파일로, 특정 라이브러리를 컴파일러에 링크하거나, 특정 환경 변수를 설정하거나, 혹은 코드 생성 작업을 수행할 수 있습니다.

 

예를 들어, 환경 변수를 읽어 특정 코드 경로를 선택하거나, bindgen을 사용해 C 헤더에서 Rust FFI 바인딩 코드를 생성하는 일을 build.rs에서 수행할 수 있습니다. 이를 통해 C++에서 사용하는 복잡한 빌드 파이프라인을 단일 Cargo 프로젝트로 깔끔히 관리할 수 있습니다.

C++와의 비교 정리

  • 매크로: C++ 전처리기 매크로보다 안전하고 강력한 러스트 매크로. 프로시저 매크로를 통한 컴파일 타임 코드 변환 가능.
  • 클로저: C++ 람다와 유사하나, 러스트는 소유권 규칙에 따라 캡처 방식이 결정되고 안전성 보장. 명확한 트레이트(Fn, FnMut, FnOnce)로 기능 구분.
  • 함수형 패러다임: C++20 ranges와 유사한 기능을 러스트는 초기부터 지원. 이터레이터 기반 함수형 패턴이 일관되고 간단.
  • 빌드 스크립트: C++에서 CMake나 외부 스크립트를 사용하던 것을, 러스트는 Cargo의 build.rs로 통합. Rust 코드로 빌드 과정 커스터마이징 가능.

앞으로의 학습 방향

이번 글에서는 매크로, 클로저, 함수형 패턴, 빌드 스크립트를 통해 러스트가 어떤 식으로 생산성, 안전성, 확장성을 확보하는지 알아보았습니다. 앞으로는 러스트 생태계 전반(크레이트, 패키지, 문서화, 테스트, Benchmarking)과 WebAssembly, FFI(Foreign Function Interface) 등을 다루며 실전 활용법을 정리하는 방향으로 나아갈 수 있습니다.

이로써 러스트 입문 시리즈의 큰 줄기는 얼추 마무리됩니다. 여기까지 읽으셨다면, 러스트의 주요 특징을 C++ 관점에서 이해하셨을 것이며, 실제 프로젝트에 적용하는 과정에서 더 깊이 있는 학습과 실습을 이어나갈 수 있습니다.

유용한 링크와 리소스

 

반응형