Rust는 정적 타입 언어이면서도 매우 강력한 매크로 시스템을 제공합니다. 매크로는 코드를 작성하는 코드를 작성할 수 있게 해 주며, 반복되는 코드의 중복을 줄이고, 컴파일 타임에 코드를 생성하여 성능 저하 없이 유연성을 확보할 수 있습니다.
Rust의 매크로는 크게 두 가지로 나뉩니다:
- 매크로 by 예시 (macro_rules!)
- 절차적 매크로 (Procedural Macros)
이번 시간에는 두 매크로의 차이점과 사용법, 그리고 메타 프로그래밍의 개념에 대해 알아보겠습니다.
1. macro_rules! 기본 매크로
Rust에서 가장 널리 사용되는 매크로는 macro_rules!로 작성하는 선언형 매크로(declarative macro)입니다.
가. 예시
macro_rules! say_hello {
() => {
println!("Hello, macro!");
};
}
fn main() {
say_hello!(); // Hello, macro!
}
이 매크로는 함수처럼 보이지만, 실제로는 코드를 치환하는 역할을 합니다. 괄호 안에 아무 인자도 없을때, println! 코드를 삽입하는 구조입니다.
- macro_rules!는 Rust에서 매크로를 정의하는 키워드입니다. 함수와 비슷해 보이지만, 컴파일 타임에 코드 조각을 만들어주는 메타 프로그래밍 도구입니다.
- say_hello라는 이름의 매크로를 정의합니다.
()는 이 매크로가 인자를 받지 않음을 의미합니다.
=> { … }는 매크로가 어떤 코드로 확장될지를 정의합니다.
여기서는 println!(“Hello, macro!”);라는 코드를 삽입합니다.
즉, 이 매크로는 호출되면 println!을 실행하는 코드로 치환됩니다. - say_hello!();는 매크로를 호출하는 문법입니다.
함수 호출과 다르게 ! 기호가 사용됩니다. - 결과적으로 Hello, macro!가 출력됩니다.
나. 매크로는 함수보다 더 유연하다.
- 함수는 특정 타입에 대해 정의되어야 하지만,
- 매크로는 타입과 관계없이 패턴으로 처리할 수 있습니다.
macro_rules! create_function {
($name:ident) => {
fn $name() {
println!("함수 {} 호출됨", stringify!($name));
}
};
}
create_function!(foo);
create_function!(bar);
fn main() {
foo(); // 함수 foo 호출됨
bar(); // 함수 bar 호출됨
}
위의 매크로는 여러 개의 함수를 자동으로 생성하는 예시입니다.
(1) 매크로 정의
macro_rules! create_function {
($name:ident) => {
fn $name() {
println!("함수 {} 호출됨", stringify!($name));
}
};
}
- macro_rules! create_function: create_function이라는 이름의 매크로를 정의합니다.
- ($name:ident) => { … }: 이 매크로는 식별자 하나를 입력 인자로 받아 내부에서 함수 하나를 생성합니다.
ident: 식별자(identifier) 타입을 의미합니다. 변수명, 함수명 같은 이름을 받을 때 사용합니다.니다.- ident: 식별자(identifier) 타입을 의미합니다. 변수명, 함수명 같은 이름을 받을 때 사용합니다.
- fn $name() { … }: 입력받은식별자 $name을 함수 이름으로 사용하여 새 함수를 만듭니다.
- stringify!($name): 식별자 $name을 문자열로바꿔줍니다.
- 예: stringify!(foo) → “foo”
(2) 매크로 호출
create_function!(foo);
create_function!(bar);
- 이 부분은 위에서 정의한 매크로를 호출하는 부분입니다.
- create_function!(foo)는 foo를 인수로 받아 아래와 같은 함수를 만들어냅니다:
fn foo() {
println!("함수 foo 호출됨");
}
- create_function!(bar)도 마찬가지로 bar를 인수로 받아 아래와 같은 함수를 만들어냅니다:
fn bar() {
println!("함수 bar 호출됨");
}
즉, 이 두줄 덕분에 foo와 bar라는 이름의 함수가 자동으로 생성됩니다.
(3) main 함수 – 함수 호출
fn main() {
foo(); // 함수 foo 호출됨
bar(); // 함수 bar 호출됨
}
- 앞서 매크로를통해 생성된 foo()와 bar() 함수를 호출합니다.
- 각각의 함수는 다음과 같은 출력을 합니다:
함수 foo 호출됨
함수 bar 호출됨
2. 메타 프로그래밍(Metaprogramming)이란?
Rust에서 매크로를 사용하는 이유는 곧 메타 프로그래밍을 위한 것입니다. 메타 프로그래밍이란 프로그램이 프로그램 코드를 다루거나 생성하는 것을 말합니다.
Rust에서는 컴파일 타임에 코드를 생성하여
- 코드 반복 제거
- 에러 감소
- 성능 저하 없이 추상화 제공
이라는 장점을 누릴 수 있습니다.
3. 절차적 매크로(Procedural Macro)
macro_rules!는 구조가 제한적이므로 복잡한 로직을 처리하기엔 어렵습니다. 이를 해결하기 위해 Rust는 절차적 매크로를 제공합니다. 절차적 매크로는 함수처럼 동작하며, 다음과 같이 세 가지 유형이 있습니다:
- Derive 매크로 (#[derive])
- Attribute 매크로 (#[route], #[test] 등)
- Function-like 매크로 (my_macro!(…) 형태)
가. Derive 매크로
Rust는 많은 표준 트레잇을 #[derive(…)]를 통해 자동으로 구현할 수 있습니다.
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
#[DERIVE(…)]
이 부분은 파생 구현(derive macro)라고 부릅니다. Rust에서는 구조체에 대해 자주 사용하는 trait들을 자동으로 구현할 수 있도록 해줍니다.
* Debug
- 구조체를 {:?} 형식으로 출력할 수 있게 해줍니다.
- 예: println!(“{:?}”, p); // 출력: Point { x: 3, y: 5 }
*Clone
- 구조체를 복사(clone) 할 수 있게 해줍니다.
- let p2 = p.clone(); 같이 사용 가능하며, 깊은 복사가 이루어집니다.
- let p2 = p; 이라고 하면 Copy가 아니기 때문에 p는 이동(move)되고, 사용 불가가 되므로 clone을 사용하는 것입니다.
* PartialEq
- 두 구조체가 같은지 비교할 수 있게 해줍니다 (==, != 사용 가능).
- 예:
let a = Point { x: 1, y: 2 };
let b = Point { x: 1, y: 2 };
assert_eq!(a, b); // true
나. Attribute 매크로
Attribute 매크로는 함수, 구조체 등 앞에 붙여 동작을 변경합니다.
#[route(GET, "/")]
fn index() {
// 라우트 처리
}
🔸 #[route(GET, “/”)]
Rocket 프레임워크에서 제공하며, 해당 함수가 HTTP 요청을 어떻게 처리할지를 지정합니다.
- GET → HTTP 메서드 GET, POST, PUT, DELETE 등 중 하나입니다.
- “/” → 경로. 이 경우 루트 경로(예: http://localhost:8000/)입니다.
🔸 fn index()
이것은 실제 요청을 처리할 함수입니다.
보통 이 함수는 -> &’static str이나 -> Html<String> 같은 반환값을 갖습니다.
index는 함수 이름으로, 자유롭게 바꿀 수 있습니다 (home, root 등).
이 함수는 라우팅된 요청이 들어왔을 때 호출됩니다.
위 코드는 다음 의미를 갖습니다:
- 클라이언트가 GET / 요청을 보냈을 때,
- index() 함수를 실행해서 그 요청을 처리한다.
다. Function-like 매크로
마치 함수처럼 사용하는 매크로입니다.
my_macro!(input);
일반적으로 다음처럼 정의합니다.
#[proc_macro]
pub fn my_macro(input: TokenStream) -> TokenStream {
// input을 파싱하고 새로운 코드 생성
}
- pub fn my_macro(input: TokenStream) -> TokenStream
my_macro는 매크로 이름입니다. 실제 사용 시에는 my_macro!(…) 형태로 사용됩니다.
입력: input: TokenStream
출력: TokenStream - 즉, 입력으로 받은 코드를 읽고, 새로운 코드로 변환하는 함수입니다.
4. 매크로의 장단점
가. 장점
- 코드 중복 제거
- 성능 저하 없이 추상화 가능
- 정적 분석 기반으로 안전성 확보
나. 단점
- 디버깅이 어려움
- 복잡한 로직일수록 가독성 저하
- IDE의 지원이 제한적일 수 있음
5. 매크로 관련 도구
- syn: Rust 코드를 파싱하는 라이브러리
- quote: 코드 생성을 위한 DSL(Domain Specific Languages)
- proc-macro2: proc_macro를 확장한 안정적인 인터페이스
[dependencies]
syn = "2"
quote = "1"
proc-macro2 = "1"
6. 마무리
Rust의 매크로 시스템은 다른 언어의 템플릿이나 메타 프로그래밍 기능보다 훨씬 강력하고 안전하게 설계되어 있습니다. macro_rules!로 간단한 코드 생성을 처리하고, 절차적 매크로로 복잡한 로직도 커버할 수 있습니다.
메타 프로그래밍은 러스트의 안전성과 추상화를 동시에 만족시키는 중요한 도구이므로, 초반에는 어렵더라도 반드시 익혀야 할 개념입니다.