Rust에서 복잡한 데이터 구조를 위한 사용자 정의 직렬화 마스터하기
Lukas Schneider
DevOps Engineer · Leapcell

소개
네트워크 애플리케이션 및 데이터 영속성의 세계에서 구조화된 데이터를 전송 또는 저장을 위한 형식으로 변환하고 다시 원래 형태로 재구성하는 능력은 매우 중요합니다. 직렬화 및 역직렬화라고 하는 이 프로세스는 현대 소프트웨어 개발의 초석입니다. Rust의 serde 프레임워크는 대부분의 일반적인 데이터 유형에 대해 강력하고 종종 자동화된 추론을 제공하지만, 우리의 데이터 모델이 간단한 구조체 및 열거형에서 벗어나는 불가피한 시나리오가 있습니다. 직렬화 중 사용자 정의 유효성 검사 로직, 직렬화를 위한 비표준 데이터 레이아웃 또는 특이한 데이터 형식을 지정하는 외부 API와의 상호 작용을 처리할 때 기본 #[derive(Serialize, Deserialize)] 매크로는 부족합니다. 이것이 serde의 진정한 힘이 발휘되는 곳입니다. 즉, serde::Serialize 및 serde::Deserialize의 사용자 정의 구현을 만드는 것입니다. 이러한 트레이트를 수동으로 구현하는 방법을 이해하면 개발자는 거의 모든 직렬화 문제를 해결하여 데이터 무결성과 상호 운용성을 보장할 수 있습니다.
serde 사용자 정의의 핵심 개념
구현 세부 정보로 들어가기 전에 사용자 정의 serde 구현과 관련된 핵심 개념에 대한 기본적인 이해를 확립해 보겠습니다.
serde::Serialize 트레이트: 이 트레이트는 Rust 유형이 serde에서 이해하는 중간 데이터 형식으로 자신을 변환하는 방법을 정의합니다. Serializer를 인수로 받는 serialize라는 단일 메서드가 필요합니다. Serializer는 Rust 유형의 내부 구조를 나타내는 방법을 알고 있는 serde에서 제공하는 추상 인터페이스입니다. 구현은 Serializer에게 유형의 내부 구조를 나타내는 방법을 지시합니다.
serde::Deserialize 트레이트: 이 트레이트는 Rust 유형이 중간 데이터 형식에서 구성되는 방법을 정의합니다. Deserializer를 인수로 받는 deserialize라는 단일 메서드가 필요합니다. Deserializer는 다른 데이터 기본값을 읽는 메서드를 제공하는 추상 인터페이스입니다. 구현은 Deserializer를 사용하여 데이터를 추출하고 종종 "Visitor" 패턴을 사용하여 유형의 인스턴스를 구성합니다.
serde::Serializer: 이 트레이트는 일반적으로 출력 형식(예: JSON, YAML, Bincode)을 나타냅니다. serialize_i32, serialize_str, serialize_struct, serialize_seq 등과 같은 메서드를 제공하며, Serialize 구현에서 데이터를 출력하기 위해 호출됩니다.
serde::Deserializer: 이 트레이트도 입력 형식을 나타냅니다. deserialize_i32, deserialize_string, deserialize_struct, deserialize_seq와 같은 메서드를 제공하며, Deserialize 구현(특히 Visitor)에서 데이터를 읽기 위해 호출됩니다.
serde::de::Visitor: 복잡한 유형에 대해 Deserialize를 구현할 때 실제 구문 분석 로직을 Visitor에 위임하는 경우가 많습니다. Visitor는 다른 종류의 데이터 유형(예: visit_i32, visit_str, visit_map, visit_seq)을 처리하는 메서드를 정의하는 트레이트입니다. Deserializer는 Deserializer가 발생하는 데이터에 따라 Visitor에서 적절한 visit_ 메서드를 호출합니다. 이 패턴은 강력하고 유연한 역직렬화 로직을 가능하게 합니다.
사용자 정의 직렬화 및 역직렬화 구현
실용적인 예제를 통해 이러한 개념을 설명해 보겠습니다. 데카르트 좌표를 저장하는 Point 구조체가 있다고 가정해 보겠습니다. 그러나 특정 외부 API의 경우 기본 구조체 표현이 아닌 단일 문자열 "x,y"로 직렬화하고 동일한 형식에서 역직렬화해야 합니다.
use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use std::fmt; // 우리의 복잡한 데이터 유형: Point 구조체 #[derive(Debug, PartialEq)] struct Point { x: i32, y: i32, } // Point에 대한 사용자 정의 직렬화 impl Serialize for Point { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer, { // Point를 "x,y" 문자열로 직렬화하려고 합니다. let s = format!("{},{}", self.x, self.y); serializer.serialize_str(&s) } } // Point에 대한 사용자 정의 역직렬화 impl<'de> Deserialize<'de> for Point { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { // 역직렬화에는 Visitor를 만드는 것이 포함됩니다. struct PointVisitor; impl<'de> de::Visitor<'de> for PointVisitor { type Value = Point; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a string in the format 'x,y'") } fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> where E: de::Error, { // 문자열을 쉼표로 분할하고 부분을 구문 분석합니다. let parts: Vec<&str> = value.split(',').collect(); if parts.len() != 2 { return Err(de::Error::invalid_value( de::Unexpected::Str(value), &self, )); } let x = parts[0] .parse::<i32>() .map_err(|_| de::Error::invalid_value(de::Unexpected::Str(value), &self))?; let y = parts[1] .parse::<i32>() .map_err(|_| de::Error::invalid_value(de::Unexpected::Str(value), &self))?; Ok(Point { x, y }) } } // 실제 역직렬화를 Visitor에 위임합니다. deserializer.deserialize_str(PointVisitor) } } // 예제 사용 fn main() { let p = Point { x: 10, y: 20 }; // 포인트를 직렬화합니다. let serialized_json = serde_json::to_string(&p).unwrap(); println!("Serialized JSON: {}", serialized_json); // 예상: "10,20" // 포인트를 역직렬화합니다. let deserialized_p: Point = serde_json::from_str(&serialized_json).unwrap(); println!("Deserialized Point: {:?}", deserialized_p); // 예상: Point { x: 10, y: 20 } assert_eq!(p, deserialized_p); // 잘못된 입력으로 테스트 let invalid_json = r#"10"#; let result: Result<Point, serde_json::Error> = serde_json::from_str(invalid_json); assert!(result.is_err()); println!("Invalid input deserialization error: {:?}", result.unwrap_err()); }
Point 예제에서는 다음과 같습니다.
Serialize구현:x와y좌표를 단일 문자열로 형식화한 다음serializer.serialize_str을 사용하여 해당 문자열을 출력합니다. 이것은Point를 사용자 정의 문자열 형식으로 직렬화하라는 요구 사항을 충족합니다.Deserialize구현: 구문 분석의 필요성 때문에 더 복잡합니다.serde::de::Visitor를 구현하는 내부 구조체PointVisitor를 정의합니다.expecting은 역직렬화자가 예상하는 것에 대한 사용자 친화적인 메시지를 제공합니다.visit_str은 역직렬화 로직의 핵심입니다. 데이터가 문자열로 온다는 것을 알고 있으므로 "x,y" 형식을 구문 분석하기 위해 이 메서드를 구현합니다. 잘못된 형식의 문자열 및 구문 분석 실패에 대한 오류 처리를 수행하고 이를serde::de::Error에 매핑합니다.- 마지막으로
impl<'de> Deserialize<'de> for Point에서deserializer에deserializer.deserialize_str(PointVisitor)를 호출하여 문자열 유형을 예상하도록 합니다.Deserializer는 내부적으로PointVisitor::visit_str메서드를 호출하여 문자열을 구문 분석합니다.
이 패턴은 더 복잡한 시나리오로 확장됩니다. 사용자 정의 키가 있는 맵에서 역직렬화하거나 항목 유형을 다르게 하는 시퀀스에서 역직렬화해야 하는 경우, Visitor는 visit_map 또는 visit_seq를 구현하고 특정 로직에 따라 반복되는 요소를 처리합니다.
또 다른 일반적인 시나리오는 serde의 기본 유형에 직접 매핑되지 않는 유형, 예를 들어 특정 타임스탬프 형식으로 직렬화해야 하는 사용자 정의 DateTime 구조체와 관련이 있습니다.
사용자 정의 serde 구현의 적용은 다양한 사용 사례에 걸쳐 있습니다.
- 레거시 시스템 또는 비표준 API와의 상호 작용: 데이터 형식이 고정되어 있고 Rust의 구조체 레이아웃과 정확히 일치하지 않는 경우.
- 역직렬화 중 특정 데이터 유효성 검사 구현:
Visitor의visit_메서드는 사용자 정의 유효성 검사 로직을 추가하고 유효성 검사가 실패하면de::Error를 반환하기에 완벽한 장소입니다. - 데이터 크기 최적화: 복잡한 객체를 압축된 형식(예:
Point예제의 "x,y" 문자열)으로 직렬화합니다. - 표준적이지 않은 변형이 있는 외부 열거형 처리: 외부 문자열 표현을 내부 열거형 변형에 매핑합니다.
결론
serde::Serialize 및 serde::Deserialize의 사용자 정의 구현은 기본 추론이 불충분할 때 Rust 개발자 도구 키트에서 필수적인 도구입니다. Serializer, Deserializer 및 Visitor 트레이트를 이해함으로써 복잡한 데이터 유형이 직렬화된 형식으로 표현되는 방식과 거기에서 신중하게 재구성되는 방식을 완전히 제어할 수 있습니다. 이러한 숙달은 데이터 무결성을 보장하고 데이터 형식의 독특함에 관계없이 모든 데이터 소스 또는 싱크와의 원활한 상호 운용성을 보장합니다. 효과적으로, 사용자 정의 serde 구현은 Rust 애플리케이션이 모든 데이터 언어를 말할 수 있도록 합니다.

