serde 역직렬화에서 &'a str 또는 &'a [u8] 같은 빌린 타입을 사용할 때, 제로-카피가 불가능하거나 지원되지 않으면 컴파일 타임이 아니라 런타임에서 오류가 발생할 수 있다.
URL: https://yossarian.net/til/post/serde-s-borrowing-can-be-treacherous/
(곰곰이 생각해 보면 그리 놀라운 일은 아니지만, 최근에 이걸로 한 방 맞아서 정리해 둔다.)
TL;DR: serde 디시리얼라이저에서 &'a str 또는 &'a [u8]를 사용할 때는 조심하자. serde는 제로-카피 역직렬화가 불가능하거나(혹은 지원되지 않아) 성립하지 않는 경우에 대해 적절한 컴파일 타임 오류를 만들어낼 방법이 없다. 대신, 한참 나중에 런타임 오류를 만나게 된다.
serde는 Rust의 사실상 표준 직렬화/역직렬화 프레임워크다. 또한 Rust 생태계의 “왕관 보석(crown jewels)” 중 하나라고 해도 과언이 아닌데, 널리 사용되기도 하고, 까다로운 인체공학(ergonomics) 문제—즉 어떤 구조체의 직렬화된 표현을 그 타입과 대응시키는 문제—를 포맷별 제약을 타입 자체에 강요하지 않고도1 해결해 주기 때문이다.
serde는 제로-카피(zero-copy) 역직렬화도 어느 정도 지원한다. 즉, 디시리얼라이저가 소유(owned) 복사본을 반환하는 대신, 입력 데이터에 대한 빌린 참조를 반환하는 방식이다. 이는 보통 JSON 같은 포맷에서 가능하다. JSON 문자열의 인코딩 형태는 적절한 빌린 형태로 대응될 수 있기 때문이다:
{ "foo": "this is a string" }
^^^^^^^^^^^^^^^^
|
+-- &str로 표현 가능
이를 활용하려면 String 대신 Deserializer에서 &str을 꺼내면 된다. derive API에서도 동작한다:
use serde::Deserialize;
#[derive(Deserialize)]
struct Example<'a> {
foo: &'a str,
}
하지만 여기에는 중요한 함정이 하나 있다. 이런 입력은 어떻게 될까?
{ "foo": "this is a string\nwith two lines" }
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
+-- &str로 표현 가능?
Example { ... }이 나오는 대신, 런타임에 오류가 발생한다:
Err(Error("invalid type: string \"this is a string\\nwith two lines\", expected a borrowed string", line: 1, column: 42))
이 오류는 좀 헷갈리는데, 우리는 serde에 빌린 문자열 타입을 줬고, 에러가 난 문자열 자체도 겉보기엔 별 문제가 없어 보이기 때문이다. 하지만 serde가 말하는 바는 이렇다: 입력에서 &str을 빌려서 가져오려고 했지만, 그럴 수 없었다는 것이다.
왜 안 될까? 그 문자열에는 이스케이프(여기서는 \n)가 들어 있다. serde는 이를 실제 개행 문자로 디코딩해야 한다. 즉, 단순히 입력을 참조하는 것으로는 안 되고 변형(mutation) 이 필요하다. 그 결과, serde가 원본 입력 데이터 안을 가리키는 &str을 반환하는 것은 불가능해진다.
불행히도 이는 런타임에서만 드러난다. serde는 어떤 특정 입력이 복사 없이 역직렬화 가능한지 미리 알 방법이 없기 때문이다. 특히 JSON 같은 사람이 읽을 수 있는(human-readable) 포맷에서는 이스케이프가 흔하므로 더 그렇다.
(또한 serde-yaml 같은 serde 디시리얼라이저는 JSON에서의 “잘 되는 경우”와 비슷한 케이스가 있더라도, 빌림(borrowing) 지원이 매우 제한적인 편이다.)
이에 대한 우회책은 두 가지가 있다:
&str 대신 String.Cow<'a, str>를 사용하고, 해당 필드에 #[serde(borrow)]를 함께 붙인다. 기본적으로는 serde가 blanket implementation 때문에 Cow::Owned를 선택하므로 둘 다 필요하다. 자세한 내용은 #1852를 참고.위 예제를 고치면 이렇게 된다:
use serde::Deserialize;
use std::borrow::Cow;
#[derive(Deserialize)]
struct Example<'a> {
#[serde(borrow)]
foo: Cow<'a, str>,
}