사용자 지정 Derive 매크로로 코드 재사용성 잠금 해제
Grace Collins
Solutions Engineer · Leapcell

소개
성능, 메모리 안전성 및 동시성으로 유명한 Rust는 종종 개발자에게 상용구 코드를 작성하는 과제를 제시합니다. 이는 Debug
, Default
또는 직렬화 특성과 같이 많은 데이터 구조에 일반적인 특성을 구현할 때 특히 두드러집니다. Rust는 많은 표준 특성에 대해 내장된 derive
속성을 제공하지만, 사용자 지정 동작 또는 특성 조합이 필요한 수많은 시나리오가 있습니다. 모든 struct에 대해 수동으로 이러한 특성을 구현하는 것은 지루하고 오류가 발생하기 쉬우며 개발자 생산성을 크게 저하시킬 수 있습니다. 이곳이야말로 사용자 지정 derive 매크로의 진정한 힘이 빛을 발하는 곳입니다. 이들은 반복적인 코드 생성을 자동화하는 우아하고 강력한 솔루션을 제공하여 개발자가 특성 구현의 메커니즘이 아닌 애플리케이션의 고유한 논리에 집중할 수 있도록 합니다. 이 문서는 사용자 지정 derive 매크로를 작성하는 과정을 안내하고 이 강력한 기능을 활용하여 Rust 개발 워크플로를 간소화하는 방법을 보여줍니다.
사용자 지정 Derive 매크로의 빌딩 블록 이해
구현에 대해 자세히 알아보기 전에 사용자 지정 derive 매크로를 이해하는 데 기본이 되는 몇 가지 핵심 개념을 명확히 해보겠습니다.
절차적 매크로
사용자 지정 derive 매크로는 절차적 매크로의 특정 유형입니다. 선언적 매크로(.macro_rules!
를 사용하는)와 달리 절차적 매크로는 코드의 추상 구문 트리(AST)를 조작합니다. 이는 Rust 코드를 입력으로 받아 조작한 다음 새 Rust 코드를 출력한다는 것을 의미합니다. 이 AST 조작 기능 덕분에 derive 매크로는 struct 및 enum에 대한 코드를 "생성"할 수 있습니다.
syn
크레이트
syn
크레이트는 절차적 매크로 작성에 필수적인 도구입니다. Rust 구문의 강력한 파서를 제공하여 입력 토큰을 구조화된 AST 표현으로 쉽게 구문 분석할 수 있습니다. syn
을 사용하면 입력 struct의 필드, 이름, 속성 등을 검사할 수 있습니다.
quote
크레이트
syn
을 사용하여 입력 AST를 분석한 후에는 출력 Rust 코드를 생성하는 방법이 필요합니다. quote
크레이트는 거의 모든 API를 제공하여 구문 분석된 입력에서 Rust 코드를 매우 쉽게 구성할 수 있습니다. 이를 통해 매크로 내에서 직접 Rust와 유사한 구문을 작성하고 AST의 변수를 보간할 수 있습니다.
proc_macro
크레이트
이것은 Rust 자체에서 제공하는 기본 크레이트로, proc_macro
속성과 TokenStream
유형을 정의합니다. TokenStream
은 모든 절차적 매크로의 원시 입력 및 출력 유형입니다.
원칙과 구현
실제 예를 통해 원칙과 구현을 설명해 보겠습니다. MyTrait
이라는 사용자 지정 특성을 구현해야 하는 많은 struct가 있고, 이 특성은 단순히 struct의 이름을 반환하는 get_name
메서드를 가지고 있다고 가정해 보겠습니다.
// 라이브러리 또는 애플리케이션 크레이트에서 pub trait MyTrait { fn get_name(&self) -> String; } // 예제 struct struct User { id: u32, name: String, } struct Product { product_id: u32, product_name: String, price: f64, }
사용자 지정 derive 매크로 없이 User
와 Product
모두에 대해 MyTrait
을 수동으로 구현해야 하므로 반복적인 코드가 발생합니다.
impl MyTrait for User { fn get_name(&self) -> String { "User".to_string() // 또는 동적인 경우 self.name } } impl MyTrait for Product { fn get_name(&self) -> String { "Product".to_string() // 또는 self.product_name } }
이제 이 작업을 자동화하는 사용자 지정 derive 매크로 MyDerive
를 만들어 보겠습니다.
1단계: 프로젝트 설정
절차적 매크로를 위한 별도의 크레이트가 필요합니다. my_derive_macro
라고 부르겠습니다.
// my_derive_macro/Cargo.toml [package] name = "my_derive_macro" version = "0.1.0" edition = "2021" [lib] proc-macro = true [dependencies] syn = { version = "2.0", features = ["full"] } quote = "1.0" proc-macro2 = "1.0" # 디버깅을 위해 포함하는 것이 좋습니다
2단계: 절차적 매크로 구현
my_derive_macro/src/lib.rs
내부에서 매크로 로직을 작성합니다.
// my_derive_macro/src/lib.rs extern crate proc_macro; use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, Data, DeriveInput, Ident}; #[proc_macro_derive(MyDerive)] pub fn my_derive_macro_derive(input: TokenStream) -> TokenStream { // 1. 입력 TokenStream을 DeriveInput struct로 구문 분석 let input = parse_macro_input!(input as DeriveInput); // 2. struct/enum의 이름 추출 let name = &input.ident; // 3. `get_name`에 사용할 필드 이름 결정. // 단순화를 위해 'name' 또는 'product_name'이라는 필드가 항상 존재한다고 가정합니다. // 보다 강력한 매크로에서는 사용할 필드를 지정하기 위해 속성을 사용합니다. let field_to_use: Ident = match &input.data { Data::Struct(data_struct) => { let mut found_field = None; for field in &data_struct.fields { if field.ident.as_ref().map_or(false, |id| id == "name") { found_field = Some(quote! { self.name }); break; } else if field.ident.as_ref().map_or(false, |id| id == "product_name") { found_field = Some(quote! { self.product_name }); break; } } found_field.unwrap_or_else(|| { // 'name' 또는 'product_name'을 찾지 못한 경우 struct 이름으로 기본값 설정 let name_str = name.to_string(); quote! { #name_str.to_string() } }) }, _ => { // enum 또는 기타 유형의 경우 유형 이름을 반환할 수 있습니다. let name_str = name.to_string(); quote! { #name_str.to_string() } } }; // 4. quote!를 사용하여 MyTrait의 구현을 생성합니다. let expanded = quote! { impl MyTrait for #name { fn get_name(&self) -> String { #field_to_use.to_string() } } }; // 5. 생성된 코드를 다시 TokenStream으로 변환합니다. expanded.into() }
3단계: 사용자 지정 derive 매크로 사용
애플리케이션 크레이트(예: my_app
)에서는 my_derive_macro
를 종속성으로 추가해야 합니다.
// my_app/Cargo.toml [package] name = "my_app" version = "0.1.0" edition = "2021" [dependencies] my_derive_macro = { path = "../my_derive_macro" } # 경로 조정
그런 다음 struct에 derive 매크로를 적용합니다.
// my_app/src/main.rs use my_derive_macro::MyDerive; // trait 정의 (derive 매크로를 사용하는 곳에서 접근 가능해야 함) pub trait MyTrait { fn get_name(&self) -> String; } #[derive(MyDerive)] struct User { id: u32, name: String, email: String, } #[derive(MyDerive)] struct Product { product_id: u32, product_name: String, price: f64, } #[derive(MyDerive)] struct Company { // 이 struct에는 'name' 또는 'product_name' 필드가 없습니다. tax_id: String, employees: u32, } fn main() { let user = User { id: 1, name: "Alice".to_string(), email: "alice@example.com".to_string(), }; let product = Product { product_id: 101, product_name: "Widget".to_string(), price: 9.99, }; let company = Company { tax_id: "XYZ123".to_string(), employees: 50, }; println!("User name: {}", user.get_name()); // 출력: User name: Alice println!("Product name: {}", product.get_name()); // 출력: Product name: Widget println!("Company name: {}", company.get_name()); // 출력: Company name: Company }
이 예제에서 #[derive(MyDerive)]
속성은 절차적 매크로를 트리거합니다. 그런 다음 매크로는 User
및 Product
struct를 검사하고, name
또는 product_name
필드를 식별하고, 각 struct에 대해 impl MyTrait
블록을 생성합니다. Company
의 경우 특정 필드가 없으므로 struct의 유형 이름 사용으로 기본 설정됩니다. 이는 상용구 코드를 크게 줄이고 코드베이스 전체에 일관성을 보장합니다.
애플리케이션 시나리오
사용자 지정 derive 매크로는 매우 강력하며 광범위한 시나리오에서 사용됩니다.
- 직렬화/역직렬화: 복잡한 데이터 구조에 대한 사용자 지정 직렬 변환기/역직렬 변환기 구현 (예:
serde
가 기본적으로 제공하는 것 이상). - 데이터베이스 ORM: 데이터베이스 테이블에 대한 struct 매핑, 스키마 정의, CRUD 작업 및 기본 키 처리를 포함한 상용구 코드 생성.
- 구성 파싱: 구성 struct에 대한 getter를 자동으로 생성하며, 기본값 또는 유효성 검사 로직을 포함할 수 있습니다.
- 빌더 패턴: 복잡한 객체 구성을 위한 빌더 struct 및 메서드 생성 (
derive_builder
크레이트가 좋은 예입니다). - 테스팅: struct 정의를 기반으로 테스트 케이스 또는 모의 객체 생성.
- 도메인별 언어(DSL): 많은 유형에 일관되게 적용해야 하는 애플리케이션 도메인에 특화된 사용자 지정 특성 구현.
결론
Rust의 사용자 지정 derive 매크로는 개발자가 반복적인 코드 생성을 처리하는 방식을 변화시키는 강력한 기능입니다. syn
을 구문 분석에 사용하고 quote
를 코드 생성에 활용하면 상용구 코드를 크게 줄이고 코드 일관성을 개선하며 개발자 생산성을 향상시키는 매크로를 만들 수 있습니다. 이 기법을 익히면 매우 표현력이 풍부하고 유지 관리 가능한 Rust 애플리케이션을 구축할 수 있습니다. 사용자 지정 derive 매크로를 활용한다는 것은 더 많은 Rust 코드를 작성하고 상용구 코드는 더 적게 작성하는 것을 의미합니다.