Table of contents
개요
Rust 언어의 매크로는 코드를 자동으로 생성하거나 변환하는 기능을 제공합니다.
매크로는 소스 코드를 컴파일하기 전에 처리되며, 매크로 규칙 매크로와 프로시저 매크로 두 가지 종류로 나뉩니다.
매크로 규칙 매크로는 패턴을 찾아내어 해당 코드를 대체하는 매크로이며, Rust에서는 "macro_rules!" 매크로를 사용하여 정의할 수 있습니다.
프로시저 매크로는 Rust 컴파일러의 일부로서, 컴파일 타임에 실행되며, 매크로가 적용된 코드를 변경하거나 확장합니다.
프로시저 매크로는 derive 매크로와 attribute 매크로 두 종류로 나뉘며, Rust 코드를 직접 조작할 수 있는 능력이 있습니다.
설명
Rust에서는 매크로를 두 가지 종류로 나눌 수 있습니다.
매크로 규칙 매크로는 특정 패턴을 찾아내어 해당 코드를 대체하는 매크로로, Rust에서는 "macro_rules!" 매크로를 사용하여 정의할 수 있습니다. 반면, 프로시저 매크로는 Rust 컴파일러의 일부로 실행되며, 매크로가 적용된 코드를 변경하거나 확장합니다. 또한, derive 매크로와 attribute 매크로 두 종류로 나뉘며, 각각 Rust에서 제공하는 trait를 구현하거나, 메타데이터를 추가할 수 있습니다. 프로시저 매크로는 Rust 코드를 직접 조작할 수 있는 능력이 있으므로, 더 복잡한 코드 변환 작업을 수행할 수 있습니다.
1. 매크로 규칙 매크로(Rule-based macros)
매크로 규칙 매크로는 특정한 규칙을 따르는 패턴을 찾아내어 해당 패턴에 일치하는 코드를 대체하는 매크로입니다. Rust에서는 이러한 매크로를 "macro_rules!"라는 매크로를 사용하여 정의할 수 있습니다.
예를 들어, 다음과 같이 매크로를 정의할 수 있습니다.
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
Rust
복사
위의 매크로는, "vec![1, 2, 3]" 코드를 작성하면, "vec![1, 2, 3]" 코드를 "let mut temp_vec = Vec::new(); temp_vec.push(1); temp_vec.push(2); temp_vec.push(3); temp_vec" 코드로 변경합니다.
let my_vec = vec![1, 2, 3]; // let my_vec = vec![1, 2, 3];
Rust
복사
2. 프로시저 매크로(Procedural macros)
프로시저 매크로는 Rust 컴파일러의 일부로서, 컴파일 타임에 실행되며, 매크로가 적용된 코드를 변경하거나 확장합니다. 이러한 매크로는 보통 사용자가 직접 정의하며, 주로 derive 매크로와 attribute 매크로 두 종류로 나뉩니다.
•
derive 매크로: Rust에서 제공하는 특정한 trait를 구현하기 위한 코드를 자동으로 생성하는 매크로입니다. 예를 들어, Rust에서 제공하는 Debug trait를 구현하기 위한 코드를 자동으로 생성하는 #[derive(Debug)] 매크로가 있습니다.
#[derive(Debug)]
struct MyStruct {
name: String,
age: i32,
}
Rust
복사
•
attribute 매크로: Rust 코드에 메타데이터를 추가할 수 있는 기능입니다. 예를 들어, #[test]라는 attribute를 추가하면 해당 함수가 테스트 함수임을 나타냅니다. 이러한 attribute를 붙이는 기능을 자동으로 처리하는 매크로를 attribute 매크로라고 합니다.
#[test]
fn test_my_function() {
assert_eq!(my_function(), 42);
}
Rust
복사
프로시저 매크로는 매크로 규칙 매크로와 달리 Rust 언어를 직접 조작할 수 있는 능력이 있으므로, 더 복잡한 코드 변환 작업을 수행할 수 있습니다. 하지만, 이러한 매크로를 작성하는 것은 상대적으로 어렵고, 안전성을 유지하는 것이 중요합니다.
사용성
매크로의 장점
1. 코드 중복 제거
매크로를 사용하면, 코드 중복을 제거할 수 있습니다. 예를 들어, 반복되는 코드를 매크로로 정의하여, 해당 코드를 간단하게 작성할 수 있습니다.
macro_rules! log_error {
( $( $arg:expr ),* ) => {
{
eprintln!("Error: {}", format!($( $arg ),*));
}
};
}
Rust
복사
위의 매크로를 사용하면, 다음과 같이 오류 로그를 쉽게 작성할 수 있습니다.
log_error!("Failed to read file: {}", file_path);
Rust
복사
2. 코드 가독성 향상
매크로를 사용하면, 코드 가독성을 향상시킬 수 있습니다. 예를 들어, 벡터를 생성하는 코드를 작성할 때, "vec![1, 2, 3]"와 같이 매크로를 사용하면, 코드가 간단해지고 가독성이 좋아집니다.
let my_vec = vec![1, 2, 3];
Rust
복사
3. 코드 재사용성 증가
매크로를 사용하면, 코드 재사용성을 증가시킬 수 있습니다. 예를 들어, 반복문을 매크로로 정의하여, 해당 코드를 재사용할 수 있습니다.
macro_rules! my_for {
($val:expr in $range:expr => $body:block) => {
for $val in $range {
$body
}
};
}
Rust
복사
위의 매크로를 사용하면, 다음과 같이 코드를 간단하게 작성할 수 있습니다.
my_for!(i in 0..10 => {
println!("{}", i);
});
Rust
복사
4. 코드 생성 자동화
매크로를 사용하면, 코드 생성을 자동화할 수 있습니다. 예를 들어, derive 매크로를 사용하여, Rust에서 제공하는 trait를 자동으로 구현하는 코드를 생성할 수 있습니다.
#[derive(Debug)]
struct MyStruct {
name: String,
age: i32,
}
Rust
복사
위의 코드에서는 "MyStruct" 구조체에 "Debug" trait를 구현하기 위한 코드를 생성하는 #[derive(Debug)] 매크로를 사용하였습니다. 이를 통해, 코드 작성 시간을 단축할 수 있습니다.
매크로를 잘못 사용했을 때 생기는 문제
1. 코드 가독성 저하
매크로를 사용하면, 코드 가독성이 향상될 수 있지만, 잘못 사용하면 가독성이 저하될 수 있습니다. 예를 들어, 다음과 같이 코드를 작성하면, 매크로 코드를 이해하기 어렵고, 가독성이 저하됩니다.
macro_rules! my_macro {
() => {
println!("Hello, World!");
};
}
my_macro!(); my_macro!(); my_macro!();
Rust
복사
2. 디버깅 어려움
매크로를 사용하면, 코드가 자동으로 생성되므로 디버깅이 어려울 수 있습니다. 예를 들어, 다음과 같이 매크로를 사용하면, 컴파일 타임에 생성된 코드를 디버깅해야 하므로, 오류를 찾기 어렵습니다.
macro_rules! my_macro {
($val:expr) => {
match $val {
1 => println!("One"),
2 => println!("Two"),
3 => println!("Three"),
_ => println!("Other"),
}
};
}
my_macro!(1);
Rust
복사
3. 안전성 문제
프로시저 매크로를 사용할 때, 안전성 문제가 발생할 수 있습니다. 매크로가 실행되는 동안, Rust 코드를 직접 조작할 수 있으므로, 매크로 코드를 잘못 작성하면, 메모리 누수나 버그가 발생할 수 있습니다. 따라서, 매크로 코드를 작성할 때는 안전성을 고려하여야 합니다.
use std::mem;
macro_rules! my_macro {
($val:expr) => {
let mut vec = vec![$val];
mem::forget(vec.as_mut_ptr());
};
}
my_macro!(42);
Rust
복사
위의 코드에서, 매크로가 실행될 때, 메모리 누수가 발생할 수 있습니다. 매크로는 "vec" 벡터의 포인터를 잃어버리므로, 해당 메모리가 해제되지 않습니다.
매크로 테스트 방법
Rust에서 매크로를 테스트하는 방법은 크게 두 가지가 있습니다. 하나는 "macro_rules!" 매크로를 사용하여 정의한 매크로를 테스트하는 것이고, 다른 하나는 derive 매크로나 attribute 매크로를 사용하여 정의한 매크로를 테스트하는 것입니다. 각각에 대해 자세히 설명하겠습니다.
1. "macro_rules!" 매크로를 테스트하는 방법
"macro_rules!" 매크로를 테스트할 때는, 다음과 같은 방법을 사용할 수 있습니다.
1.
테스트 코드 작성하기
"macro_rules!" 매크로를 테스트하기 위한 테스트 코드를 작성합니다. 테스트 코드는 매크로를 호출하고, 호출 결과를 확인하는 코드를 작성합니다.
#[test]
fn test_my_macro() {
assert_eq!(my_macro!(1, 2, 3), 6);
assert_eq!(my_macro!("a", "b", "c"), "abc");
}
Rust
복사
2.
매크로 호출하기
테스트 코드에서 매크로를 호출합니다. 매크로가 예상대로 동작하는지 확인하기 위해, 매크로 호출 결과를 출력해볼 수 있습니다.
macro_rules! my_macro {
( $( $val:expr ),* ) => {
{
let mut sum = 0;
$(
sum += $val;
)*
sum
}
};
}
fn main() {
println!("{}", my_macro!(1, 2, 3)); // 6
println!("{}", my_macro!("a", "b", "c")); // abc
}
Rust
복사
3.
테스트 실행하기
코드를 실행하여, 테스트를 실행합니다. 실행 결과, 테스트가 통과하면, 매크로가 예상대로 동작하는 것입니다.
2. Derive 매크로나 Attribute 매크로를 테스트하는 방법
Derive 매크로나 Attribute 매크로를 테스트할 때는, #[test] attribute를 사용하여, 테스트 함수를 작성합니다. 이때, #[test] attribute와 함께 #[derive()]나 #[my_attribute()]와 같이 사용하여, 매크로가 제대로 동작하는지 확인합니다.
#[derive(MyTrait)]
struct MyStruct {
name: String,
age: i32,
}
#[test]
fn test_my_macro() {
let my_struct = MyStruct {
name: "John".to_string(),
age: 42,
};
assert_eq!(my_struct.to_string(), "John (42)");
}
Rust
복사
위의 코드에서는 "MyTrait"를 구현하는 Derive 매크로를 테스트하였습니다. 테스트 코드에서는 "MyStruct" 인스턴스를 생성하고, "to_string()" 메소드가 제대로 동작하는지 확인하였습니다.
참조
Rust 공식 문서: Macros - https://doc.rust-lang.org/reference/macros.html
Rust by Example: Macros - https://doc.rust-lang.org/stable/rust-by-example/macros.html
The Little Book of Rust Macros - https://danielkeep.github.io/tlborm/book/index.html