Macros, Metaprogramming

이현재·2023년 12월 9일
2

매크로는 뭘까

컴퓨터 프로그래밍의 세계에서 매크로는 반복적인 작업을 자동화하는 단축키와 같습니다. 컴퓨터에게 새로운 단어를 가르치면, 사용자가 그 단어를 사용할 때마다 컴퓨터가 사용자가 가르친 문장이나 단락으로 대체하는 것과 비슷합니다. 기술적인 용어로 매크로는 패턴 매칭 시스템을 통해 코드를 생성하는 방법입니다. 같은 코드를 반복해서 작성하는 대신, 매크로를 작성하면 컴퓨터가 자동으로 작업을 수행합니다.

Rust의 매크로

Rust는 매크로를 광범위하게 사용합니다. 매크로는 코드 재사용과 메타프로그래밍을 위한 강력한 도구 역할을 합니다. Rust에서 매크로는 함수와 상당히 다릅니다. 함수는 값을 인수로 받아 새로운 값을 생성하는 반면, 매크로는 코드를 입력으로 받아 더 많은 코드를 출력으로 생성합니다. 이러한 코드 생성 기능 덕분에 매크로는 상용구 코드를 줄이고 Rust 내에서 도메인별 언어를 생성하는 데 매우 적합합니다.

Rust의 매크로 예제

Rust의 간단한 매크로를 살펴보겠습니다. Rust에서 가장 일반적으로 사용되는 매크로 중 하나는 println! 매크로입니다. 이 매크로는 끝에 개행이 있는 텍스트를 콘솔에 인쇄하고 자리 표시자를 사용하여 문자열의 서식을 지정할 수 있습니다.

println! 의 기본 사용법은 다음과 같습니다:

println!("Hello, world!");

이제 매크로를 직접 정의해 보겠습니다. 콘솔에 인사말을 출력하는 say_hello! 라는 매크로를 만들겠습니다.

macro_rules! say_hello {
    () => {
        println!("Hello, good to see you!");
    };
}

fn main() {
    // 여기서 매크로를 사용합니다.
    say_hello!();
}

say_hello!()를 사용하면 Rust는 컴파일 시 매크로 정의 내부의 코드로 확장합니다.

println!("Hello, good to see you!");

매우 기본적인 예시이지만 매크로를 사용하여 보다 표현력이 풍부하고 유지 관리가 쉬운 코드를 작성하는 방법을 보여줍니다.

이제 매크로가 무엇이고 Rust에서 매크로가 어떻게 사용되는지 기본적으로 이해했으니 다음 섹션에서 좀 더 복잡한 메타프로그래밍의 세계에 대해 알아보겠습니다.

메타 프로그래밍은 또 뭐지

메타프로그래밍은 작성한 코드가 다른 코드를 조작하고 생성할 수 있는 기술입니다. 이는 프로그램에 작업을 수행할 뿐만 아니라 해당 작업에 대한 지침을 생성하고 수정할 수 있는 두뇌를 부여하는 것과 같습니다.

메타프로그래밍은 본질적으로 다른 프로그램(또는 자신)을 데이터로 취급할 수 있는 프로그램에 관한 것입니다. 즉, 메타프로그래밍은 다른 프로그램을 읽고, 생성하고, 분석하거나 변형할 수 있으며, 실행 중에도 스스로를 수정할 수 있습니다.

Rust의 메타프로그래밍

Rust는 다음과 같은 메타프로그래밍을 가능하게 하는 몇 가지 기능을 제공합니다:

  • 매크로
  • 다양한 데이터 유형에서 작동할 수 있는 코드의 특성 및 제네릭
  • 컴파일 타임 함수 실행 (const 함수)
  • 새로운 속성을 정의하거나 특성에 따라 코드를 파생할 수 있는 속성과 유사한 절차적 매크로

이러한 도구를 사용하면 Rust 프로그래머는 높은 성능과 표현력을 갖춘 코드를 작성할 수 있습니다.

Rust의 메타프로그래밍 예제

Rust 메타프로그래밍 기능의 핵심 기능은 절차적 매크로입니다. Procedural macro는 일부 Rust 코드를 입력으로 받아 연산한 후 일부 코드를 출력으로 생성합니다. 이는 앞서 살펴본 선언적 매크로보다 더 강력하고 유연합니다.

다음은 구조체에 함수를 추가하는 절차적 매크로의 간단한 예제입니다:

use proc_macro::TokenStream;

// Procedural 매크로 정의입니다.
#[proc_macro_attribute]
pub fn add_hello(attr: TokenStream, item: TokenStream) -> TokenStream {
    let attr = attr.to_string();
    let mut item = item.to_string();
    
	// 구조체에 'hello'라는 새 메서드를 추가합니다.
    let hello_fn = format!("
        fn hello(&self) {{
            println!(\"Hello, {}!\");
        }}
    ", attr);
    
    // 새 메서드를 구조체에 삽입합니다.
    item.push_str(&hello_fn);
    
    item.parse().unwrap()
}

// 구조체에서 매크로를 사용합니다.
#[add_hello("world")]
struct Greeter;

fn main() {
    let greeter = Greeter;
    greeter.hello();
}

이 예제에서 add_hello 프로시저 매크로는 Greeter 구조체에 새로운 메서드 hello를 추가합니다. Greeter가 메인에서 인스턴스화되면 이제 콘솔에 인사말을 출력하기 위해 호출할 수 있는 hello 메서드가 생깁니다.

매크로와 메타프로그래밍의 개념을 소개했으니 이제 마지막 섹션에서 두 개념이 서로 어떻게 연관되는지 살펴보겠습니다.

매크로와 메타프로그래밍의 관계

메타프로그래밍을 위한 Gateway로서의 매크로

매크로는 Rust에서 메타프로그래밍의 기본 요소입니다. 매크로는 코드를 작성하여 다른 코드를 생성하는 방법을 예시하며 메타프로그래밍 원칙을 명확하게 보여줍니다. Rust에서 매크로는 메타프로그래밍 기능이 필요할 때 개발자가 가장 먼저 사용하는 도구입니다.

상호 보완적인 도구

매크로와 메타프로그래밍은 Rust 프로그래머의 툴킷에서 상호 보완적인 도구입니다. 매크로는 패턴 일치 및 대체를 통해 코드 생성을 처리하는 반면, 제네릭 및 특성과 같은 다른 메타프로그래밍 기능은 추상적이고 다양한 데이터 유형에 적용할 수 있는 코드를 작성하는 방법을 제공합니다. 프로시저 매크로는 한 단계 더 나아가 복잡한 코드를 변환하고 Rust 내에서 완전히 새로운 구문 또는 도메인별 언어를 생성할 수 있습니다.

복잡한 애플리케이션에서의 시너지 효과

복잡한 애플리케이션에서는 매크로와 메타프로그래밍 간의 시너지 효과가 분명하게 드러납니다. 매크로는 반복적인 코드 패턴을 생성하여 상용구를 줄일 수 있으며, 메타프로그래밍 기술은 코드가 효율적이고 유형 안전하며 다양한 컨텍스트에 맞게 조정될 수 있도록 보장합니다. 이러한 조합은 메타프로그래밍 추상화를 통해 한 곳의 변경 사항이 코드 전체에 전파될 수 있는 매우 유지 관리가 용이하고 유연한 코드베이스로 이어질 수 있습니다.

Example: 매크로와 제네릭

Rust에서 매크로와 메타프로그래밍이 모두 함께 작동하는 시나리오를 고려해 보겠습니다. 파생 매크로는 데이터 유형에 대한 특성 구현을 자동으로 생성할 수 있는 절차적 매크로의 일종입니다. 제네릭과 함께 사용하면 개발자는 특정 특성을 구현하는 모든 데이터 유형에서 작동하는 코드를 작성할 수 있으므로 상용구를 더욱 줄일 수 있습니다.

use serde::{Serialize, Deserialize};

// 이 파생 매크로는 구조체를 직렬화 및 역직렬화하는 코드를 생성합니다.
#[derive(Serialize, Deserialize)]
struct User {
    id: u32,
    name: String,
    age: u8,
}

fn serialize_to_json<T: Serialize>(item: &T) -> String {
    serde_json::to_string(item).unwrap()
}

fn main() {
    let user = User {
        id: 1,
        name: "Alice".to_string(),
        age: 30,
    };

    // 이제 serialize_to_json 함수는 제네릭 덕분에 Serialize를 구현하는 모든 데이터 유형에서 작동할 수 있습니다.
    let user_json = serialize_to_json(&user);
    println!("Serialized User: {}", user_json);
}

이 예제에서는 파생 매크로를 사용하여 사용자 구조체에 대해 SerializeDeserialize 특성이 자동으로 구현됩니다. 그런 다음 serialize_to_json 함수는 제네릭을 사용하여 각 유형에 대한 사용자 정의 직렬화 코드를 작성할 필요 없이 Serialize 특성을 구현하는 모든 유형을 직렬화합니다.

이제 끝

매크로와 메타프로그래밍은 올바르게 사용하면 코드의 표현력과 효율성을 크게 향상시킬 수 있는 Rust의 강력한 기능입니다. 매크로를 사용하면 프로그래머는 간결하고 재사용 가능한 코드를 작성할 수 있으며 일반적으로 상위 언어에서 제공하는 수준의 역동성을 제공하는 동시에 Rust의 성능과 안전성을 보장받을 수 있습니다.


참고
The Rust Programming Language Book.

medium: metaprogramming-declarative-macros-with-rust

understanding-implementing-rust-metaprogramming

that-s-so-rusty-metaprogramming

metaprogramming-with-macros

profile
코드 보는걸 좋아합니다. 궁금한게 많습니다.

0개의 댓글