Java의 MapStruct Rust 구현
Olivia Novak
Dev Intern · Leapcell

Java 생태계에는 Bean 간의 변환을 매우 편리하게 만들어주는 MapStruct라는 Bean 변환 도구가 있습니다. 그 원리는 컴파일 시간에 변환 메서드를 생성하는 것입니다. Rust 매크로도 컴파일 시간에 코드 생성을 지원하므로 속성 매크로를 사용하여 간단한 버전의 MapStruct를 구현하기로 결정했습니다.
Rust 매크로 기본 사항
Rust의 매크로는 크게 두 가지 범주로 나뉩니다. 선언적 매크로(macro_rules!
)와 세 가지 유형의 절차적 매크로입니다.
- Derive Macros:
Debug
트레이트와 같이 대상 구조체 또는 열거형에 대한 특정 코드를 파생시키는 데 일반적으로 사용됩니다. - Attribute-like Macros: 대상에 사용자 정의 속성을 추가하는 데 사용됩니다.
- Function-like Macros: 함수 호출과 유사하게 보입니다.
구현 원리 분석
Rust에서 Bean 간에 변환하려면 From
트레이트를 구현하고 from
메서드 내에서 변환 논리를 정의하면 됩니다.
pub struct Person { name: String, age: u32, } pub struct PersonDto { name: String, age: u32, } impl From<Person> for PersonDto { fn from(item: Person) -> PersonDto { PersonDto { name: item.name, age: item.age, } } } fn main() { let person = Person { name: "Alice".to_string(), age: 30, }; let dto: PersonDto = person.into(); // 자동 생성된 From 구현을 사용하여 변환 println!("dto: name:{}, age:{}", dto.name, dto.age); }
따라서 Rust에서 매크로를 사용하여 이를 구현하려면 매크로가 자동으로 From
메서드를 생성하여 자동 변환을 활성화해야 합니다.
사용 편의성을 위해 Diesel의 구문인 #[diesel(table_name = blog_users)]
에서 영감을 얻었습니다. 우리의 매크로는 구조체 위에 #[auto_map(target = "PersonDto")]
를 추가하기만 하면 사용할 수 있으므로 매우 깔끔하고 우아합니다.
#[auto_map(target = "PersonDto")] pub struct Person { name: String, age: u32, }
코드 구현
매크로 사용법은 #[auto_map(target = "PersonDto")]
이므로 매크로의 워크플로는 거의 고정되어 있습니다. Person
및 PersonDto
를 예로 들면 프로세스는 다음과 같습니다.
- 매크로에서
"target"
매개변수를 추출합니다. - 입력 구조체(
Person
)를 파싱합니다. - 입력 구조체에서 필드 이름과 유형을 추출합니다.
- 대상 유형을 파싱합니다.
- 원래 구조체를 다시 생성하고
From
메서드를 구현합니다.
1단계: 프로젝트 생성 및 종속성 추가
cargo new rust_mapstruct --lib cd rust_mapstruct
매크로 코드 생성에는 Rust의 AST 파싱이 필요하므로 quote
및 syn
의 두 가지 주요 라이브러리가 필요합니다. 또한 매크로를 만들고 있으므로 proc-macro = true
를 지정해야 합니다.
전체 종속성:
[lib] proc-macro = true [dependencies] proc-macro2 = "1.0" quote = "1.0" syn = { version = "1.0.17", features = ["full"] }
2단계: lib.rs
핵심 코드 수정
1. 핵심 함수 정의
#[proc_macro_attribute] pub fn auto_map(args: TokenStream, input: TokenStream) -> TokenStream { }
2. "target" 매개변수 추출 및 파싱
이를 확장하여 여러 매개변수를 지원할 수 있지만 MapStruct와 유사한 도구에는 하나만 필요하므로 target
문자열과 직접 일치시킵니다. 나중에 더 많은 매개변수를 추가하기 위해 이를 확장할 수 있습니다.
let args = parse_macro_input!(args as AttributeArgs); // "target" 매개변수 추출 및 파싱 let target_type = args .iter() .find_map(|arg| { if let NestedMeta::Meta(Meta::NameValue(m)) = arg { if m.path.is_ident("target") { if let Lit::Str(lit) = &m.lit { return Some(lit.value()); } } } None }) .expect("auto_map requires a 'target' argument");
3. 입력 구조체(Person
) 파싱
// 입력 구조체 파싱 let input = parse_macro_input!(input as DeriveInput); let struct_name = input.ident; let struct_data = match input.data { Data::Struct(data) => data, _ => panic!("auto_map only supports structs"), };
4. Person
에서 필드 이름 및 유형 추출
let (field_names, field_mappings): (Vec<_>, Vec<_>) = struct_data.fields.iter().map(|f| { let field_name = f.ident.as_ref().unwrap(); let field_type = &f.ty; (field_name.clone(), quote! { #field_name: #field_type }) }).unzip();
5. 대상 유형(PersonDto
) 파싱
syn::parse_str
은 문자열을 Rust 유형으로 변환할 수 있습니다.
// 대상 유형 파싱 let target_type_tokens = syn::parse_str::<syn::Type>(&target_type).unwrap();
6. 원래 구조체 및 From
구현 생성
quote
내부의 코드는 간단한 템플릿 엔진 역할을 합니다. 이전에 웹 페이지에 대한 템플릿을 작성한 적이 있다면 친숙하게 느껴질 것입니다. 첫 번째 부분은 원래 Person
구조체를 다시 생성하고 두 번째 부분은 From
메서드를 생성합니다. 파싱된 매개변수를 템플릿에 연결하기만 하면 됩니다.
// 원래 구조체 및 변환 구현 다시 생성 let expanded = quote! { // 참고: 원래 구조체 `Person`을 생성합니다. pub struct #struct_name { #( #field_mappings, )* } impl From<#struct_name> for #target_type_tokens { fn from(item: #struct_name) -> #target_type_tokens { #target_type_tokens { #( #field_names: item.#field_names, )* } } } }; expanded.into()
3단계: 프로젝트에서 매크로 테스트
먼저 cargo build
로 매크로 프로젝트를 컴파일합니다. 그런 다음 새 테스트 프로젝트를 만듭니다.
cargo new test-mapstruct cd test-mapstruct
Cargo.toml
종속성 수정
[dependencies] rust_mapstruct = { path = "../rust_mapstruct" }
main.rs
에서 간단한 테스트 작성
use rust_mapstruct::auto_map; #[auto_map(target = "PersonDto")] pub struct Person { name: String, age: u32, } pub struct PersonDto { name: String, age: u32, } fn main() { let person = Person { name: "Alice".to_string(), age: 30, }; let dto: PersonDto = person.into(); // 자동 생성된 From 구현을 사용하여 변환 println!("dto: name:{}, age:{}", dto.name, dto.age); }
코드를 실행하고 결과 확인
test-mapstruct
프로젝트에서 cargo build
, cargo run
을 실행하고 결과를 확인하세요!
❯ cargo build Compiling test-mapstruct v0.1.0 (/home/maocg/study/test-mapstruct) Finished dev [unoptimized + debuginfo] target(s) in 0.26s test-mapstruct on master ❯ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/test-mapstruct` dto: name:Alice, age:30
Rust 프로젝트 호스팅을 위한 최고의 선택, Leapcell입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
다국어 지원
- Node.js, Python, Go 또는 Rust로 개발하세요.
무료로 무제한 프로젝트 배포
- 사용량에 대해서만 지불하세요. 요청이나 요금이 없습니다.
탁월한 비용 효율성
- 유휴 요금 없이 사용량에 따라 지불합니다.
- 예: $25로 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
간소화된 개발자 경험
- 손쉬운 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
간편한 확장성 및 고성능
- 고도의 동시성을 쉽게 처리할 수 있는 자동 확장.
- 제로 운영 오버헤드 — 구축에만 집중하세요.
설명서에서 자세히 알아보세요!
X에서 팔로우하세요: @LeapcellHQ