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에서도 쓸 수 있습니다.