Rust의 attribute 종류 및 의미

Rust에서의 attribute(속성)는 컴파일러에게 특정 코드에 대한 추가적인 정보를 제공하여, 코드의 컴파일 방식 또는 동작 방식을 제어하거나, 경고/에러 메시지를 제어하는 데 사용됩니다. derive, allow, warn, cfg, test 등 다양한 종류가 있습니다.

1. attribute의 적용 범위별 종류

Rust의 attribute는 크게 다음과 같은 종류로 나눌 수 있습니다:

종류형태 예시설명
Item attribute#[derive(Debug)]함수, 구조체 등 개별 항목에 적용
Crate attribute#![allow(dead_code)]크레이트 전체에 적용, 보통 파일 상단에 위치
Inner attribute#![cfg(test)]모듈 또는 크레이트 내부에 선언, 내부 항목에 영향을 줌
Outer attribute#[test]특정 항목(함수 등)에만 적용

2. 주요 attribute

가. 컴파일러 관련 attribute

Attribute설명예시
#[allow(…)]특정 경고를 무시#[allow(dead_code)]
#[warn(…)]경고를 표시 (기본값)#[warn(unused_variables)]
#[deny(…)]해당 사항이 있으면 컴파일 에러#[deny(missing_docs)]
#[forbid(…)]deny보다 강하게 재정의 불가#[forbid(unsafe_code)]

나. 파생(derive) 관련 attribute

Rust의 많은 기능은 trait을 자동으로 구현해주는 #[derive(…)]를 통해 사용합니다.

#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}

Debug, Clone, PartialEq 트레이트를 자동 구현합니다.

다. 조건부 컴파일 관련 attribute

Attribute설명예시
#[cfg(…)]조건에 따라 컴파일 여부 결정#[cfg(target_os = “windows”)]
#[cfg_attr(…)]조건이 참일 때에만 다른 attribute 적용#[cfg_attr(test, derive(Debug))]

[cfg_attr(test, derive(Debug))]는 테스트를 실행할 때만 Debug 트레이트를 자동 구현합니다.

라. 테스트/벤치마크 관련 attribute

Attribute설명예시
#[test]단위 테스트 함수임을 표시#[test]
fn test_add() { … }
#[bench]벤치마크 함수 표시 (불안정)#[bench]
fn bench_sort(…)
#[ignore]테스트 실행 시 무시됨#[test] #[ignore]
fn test_heavy() {}

마. 매크로 관련 attribute

Attribute설명예시
#[macro_use]외부 크레이트의 매크로를 현재 스코프로 불러옴 (Rust 2015 방식)#[macro_use]
extern crate log;
#[proc_macro]프로시저 매크로 정의프로시저 매크로 crate에서 사용됨

바. 기타 유용한 attribute

Attribute설명예시
#[inline], #[inline(always)]함수 인라인화 힌트#[inline(always)]
fn fast_fn() {}
#[repr(C)]구조체 메모리 레이아웃 C와 호환#[repr(C)]
struct MyStruct { … }
#[non_exhaustive]미래 확장을 위해 열거형 등의 exhaustive 검사 방지#[non_exhaustive]
enum MyEnum { … }
#[must_use]반환값을 사용하지 않으면 경고#[must_use]
fn compute() -> i32 { … }

※ 함수를 호출할 때 일반적으로는 함수 본문은 어딘가에 있고 호출하면 그 함수로 점프(jump)해서 실행한 뒤 다시 돌아옵니다. 하지만 인라인화는 이런 점프 없이 함수의 코드를 호출한 곳에 ‘복붙’하듯 직접 삽입하는 방식입니다.

사. #![…]과 #[…] 차이

구분형태설명
Inner attribute#![…]모듈, 크레이트 전체에 적용 (파일 상단에 위치)
Outer attribute#[…]특정 항목(함수, struct 등)에 적용

3. attribute 예제

가. #[derive(…)] — 트레이트 자동 구현

(1) 설명: 구조체(struct)나 열거형(enum)에 대해, 특정 트레이트의 기본 구현을 자동 생성합니다.

(2) 자주 사용되는 트레이트:

  • Debug : {:?} 포맷으로 출력 가능
  • Clone, Copy : 값 복사 가능
  • PartialEq, Eq : 값 비교 가능
  • PartialOrd, Ord : 정렬 가능
  • Hash : 해시 사용 가능

(3) 예제:

#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}

fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1.clone(); // Clone 사용
println!("{:?}", p2); // Debug 사용
println!("p1 == p2? {}", p1 == p2); // PartialEq 사용
}

나. #[allow(…)], #[warn(…)], #[deny(…)], #[forbid(…)]

(1) 설명: 컴파일 경고/에러를 제어합니다. => 위 2.의 가. 컴파일러 관련 attribute 설명 참고

(2) 주요 옵션:

  • dead_code : 사용되지 않는 코드
  • unused_variables : 사용되지 않는 변수
  • non_snake_case : snake_case 규칙 위반

(3) 예제:

#[allow(dead_code)]
fn unused() {
println!("사용되지 않지만 경고 없음");
}

#[deny(unused_variables)]
fn foo() {
let x = 5; // 컴파일 에러 발생: 변수 사용 안 함
}

다. #[test], #[ignore] — 테스트 함수 지정

(1) 설명: 테스트 프레임워크를 위한 attribute입니다.

(2) 예제:

#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}

#[test]
#[ignore]
fn heavy_test() {
// 실행하려면 `cargo test -- --ignored`
}
}

라. #[cfg(…)] — 조건부 컴파일

(1) 설명: 플랫폼, 기능 등에 따라 코드 포함 여부 결정

(2) 예제:

#[cfg(target_os = "windows")]
fn run_on_windows() {
println!("Windows 전용 코드");
}

#[cfg(not(target_os = "windows"))]
fn run_on_unix() {
println!("Unix 계열 전용 코드");
}

(3) Cargo.toml에서 feature와 연계:

[features]
my_feature = []
#[cfg(feature = "my_feature")]
fn my_func() {
println!("my_feature가 활성화됨");
}

“my_feature”라는 기능(feature)이 활성화됐을 때만 my_func() 함수의 정의 자체를 컴파일에 포함시키고, 아니면 아예 없는 코드처럼 무시해버립니다.

마. #[inline] — 인라인 최적화 힌트

(1) 설명: 컴파일러에게 해당 함수를 인라인하도록 유도

(2) 예제:

#[inline(always)]
fn fast_add(a: i32, b: i32) -> i32 {
a + b
}

fn main() {
let x = add(3, 4);
}
  • 일반 호출: main에서 add 함수로 점프하고, 결과를 받아와서 x에 저장
  • 인라인: 컴파일할 때 add(3, 4)를 그냥 3 + 4로 바꿔서 main 함수 안에 넣어버림

바. #[repr(…)] — 메모리 레이아웃 제어

(1) 설명: 구조체/열거형의 메모리 정렬 방법을 지정(repr은 representation(표현)의 약어)

(2) 종류:

  • C: C 언어와 동일한 레이아웃
  • packed: 패딩 없이 압축
  • transparent: 단일 필드 감싸기

(3) 예제:

#[repr(C)]
struct MyStruct {
a: u8,
b: u32,
}

사. #[non_exhaustive] — 미래 확장을 위한 열거형

(1) 설명: 나중에 항목이 더 생길 수 있으니, “지금 있는 것만 가지고 match를 완벽하게 쓰지 마세요“라는 의미입니다.

(2) 예제:

#[non_exhaustive]
pub enum Error {
Io,
Parse,
}

이 코드는 다음과 같은 의미를 가집니다.

Error enum은 지금은 Io와 Parse 두 가지 variant만 있지만, 앞으로 새로운 variant가 추가될 수 있으므로 다른 크레이트에서는 이 enum을 match할 때 지금 있는 것만으로 열거하지 못하게 제한하는 것입니다.

그래서 #[non_exhaustive]를 붙이면, 사용자는 반드시 _를 이용한 default arm을 추가해야만 컴파일이 가능하며, 이것은 다른 크레이트가 match를 지금 있는 것만으로 exhaustively(완전하게) 작성하면, 나중에 enum에 새로운 variant를 추가했을 때 컴파일이 깨질 수 있기 때문입니다.

아. #[must_use] — 반환값 사용하지 않으면 경고

(1) 설명: 실수로 무시하면 안 되는 반환값을 경고로 알려줌

(2) 예제:

#[must_use]
fn important_result() -> Result<(), String> {
Err("Error".into())
}

fn main() {
important_result(); // 경고 발생!
}

위 코드는 important_result 함수의 반환 값을 사용하지 않아 warning(경고)이 발생하므로 아래와 같은 식으로 수정해야 합니다.

fn main() {
    let result = important_result(); // OK
    if let Err(e) = result {
        println!("에러 발생: {}", e);
    }
}

자. #[macro_use], #[macro_export] — 매크로 관련

(1) 설명: 외부 crate의 매크로를 가져오거나 내보낼 때 사용

(2) 예제:

// 외부 매크로 사용
#[macro_use]
extern crate log;

// 매크로 정의 및 export
#[macro_export]
macro_rules! hello {
() => {
println!("Hello, macro!");
};
}
  • #[macro_use] : 외부 크레이트 log에서 정의된 매크로(log!, info!, warn! 등)들을 이 파일 안에서 직접 사용할 수 있도록 가져오라는 의미로서, 예전 Rust 스타일 (Rust 2018 이전)의 방식이며, 최신 Rust (2018 edition 이후)에서는 use log::info; 식으로 가져오는 게 일반적입니다.
  • #[macro_export] : 이 매크로를 다른 모듈/크레이트에서 사용할 수 있게 공개(export) 하겠다는 의미입니다. 따라서, 이 매크로를 crate_name::hello!() 식으로 다른 crate에서도 쓸 수 있습니다.

매크로(Macro)와 메타 프로그래밍

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는 절차적 매크로를 제공합니다. 절차적 매크로는 함수처럼 동작하며, 다음과 같이 세 가지 유형이 있습니다:

  1. Derive 매크로 (#[derive])
  2. Attribute 매크로 (#[route], #[test] 등)
  3. 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!로 간단한 코드 생성을 처리하고, 절차적 매크로로 복잡한 로직도 커버할 수 있습니다.

메타 프로그래밍은 러스트의 안전성과 추상화를 동시에 만족시키는 중요한 도구이므로, 초반에는 어렵더라도 반드시 익혀야 할 개념입니다.