Lobsters

Rusty thoughts on "Parse, don't validate"

Rust로 살펴보는 ‘검증하지 말고 파싱하라’

‘검증하지 말고 파싱하라’는 원칙을 Rust의 타입으로 설명합니다. 비어 있지 않은 컬렉션, 절대 경로, 0이 아닌 정수처럼 조건을 타입에 담으면 이후 코드에서 같은 검사를 되풀이하지 않아도 됩니다.

AI 요약

Alexis King의 ‘Parse, don’t validate’를 Rust 코드에 적용해 설명합니다. 핵심은 데이터가 조건을 만족하는지 검사한 뒤 기존 타입에 그대로 두는 대신, 조건을 보장하는 타입으로 변환하는 것입니다. 그러면 한 번 확인한 불변 조건을 타입 시스템에 기록하고, 이를 사용하는 코드가 다시 검사할 필요를 줄입니다.

Vec 대신 NonEmpty 사용하기

예시 함수는 환경 변수 CONFIG_DIRS를 읽어 쉼표로 나누고 Vec<PathBuf>를 만듭니다. 빈 목록은 오류로 거르지만, 반환 타입은 빈 상태도 표현할 수 있는 Vec입니다. 호출부에서 첫 경로를 꺼낼 때 first()가 Option<&T>를 반환하므로 None을 처리해야 합니다. 이미 빈 목록을 검사했는데도 같은 가능성을 다시 다루며, 나중에 함수의 불변 조건이 바뀌면 unreachable!가 잘못된 가정이 될 수도 있습니다.

대안은 NonEmpty<T>처럼 적어도 원소 하나를 담는 타입을 반환하는 것입니다. 이 타입은 첫 원소 head와 나머지 원소를 담는 tail: Vec<T>로 구성됩니다. 생성할 때 원소 하나를 요구하고, first()는 Option 없이 &T를 반환합니다. NonEmpty::from_vec는 일반 벡터를 받아 비어 있으면 None, 원소가 있으면 Some(NonEmpty)를 돌려줍니다. 환경 변수 처리 함수가 이 변환을 거쳐 Result<NonEmpty<PathBuf>>를 반환하면 호출부는 별도 빈 목록 검사를 하지 않고 첫 경로를 사용합니다. 불변 조건을 세우는 시점은 변환을 수행하는 곳입니다.

파서가 만드는 유효한 구조

Rust로 POSIX 유틸리티를 다시 구현하는 프로젝트도 NonEmpty를 파이프라인에 사용합니다. Pipeline 구조체의 commands 필드는 NonEmpty<Command>입니다. 파서는 명령을 찾으면 이를 첫 원소로 NonEmpty를 만들고, 명령이 없으면 파이프라인을 반환하지 않습니다. 유효한 Pipeline이 만들어진 뒤에는 명령이 하나 이상 있다는 조건이 타입으로 보장되므로 사용 코드가 commands.first()의 빈 경우를 다루지 않습니다.

타입으로 조건을 단계별로 좁히기

rust-analyzer의 AbsPathBuf는 절대 경로를 표현하는 타입입니다. TryFrom<Utf8PathBuf> 구현은 경로가 절대 경로인지 확인하고, 조건을 만족하면 AbsPathBuf로 변환합니다. 이후 코드는 경로의 절대성을 다시 검사하지 않아도 됩니다. 여기서 Utf8PathBuf도 일반 경로 타입을 한 단계 좁힌 타입입니다. Rust 표준 라이브러리의 PathBuf는 UTF-8이 아닌 경로도 담을 수 있지만, camino의 Utf8PathBuf는 UTF-8 경로를 표현해 &str로 접근할 수 있습니다. 이처럼 입력을 여러 차례 변환하며 조건을 타입에 쌓을 수 있습니다.

정수에도 같은 원칙을 적용합니다. Rust의 NonZero는 0이 아닌 값을 표현하며, thread::available_parallelism은 성공하면 NonZero<usize>를 반환합니다. 호출부는 결과가 0인지 재검사하지 않아도 됩니다. 또 0은 NonZero에서 유효한 값이 아니므로, Option<NonZeroUsize>는 NonZeroUsize와 같은 크기와 정렬을 사용하도록 보장됩니다. 일반적인 Option<usize>에 비해 추가 저장 공간을 피할 수 있습니다.

JSON 역직렬화에 담긴 검증

serde로 JSON을 역직렬화할 때도 필드 타입에 조건을 표현할 수 있습니다. 설정 구조체에서 workers를 NonZeroUsize로, mode를 Fast와 Safe만 허용하는 열거형으로 선언하면 역직렬화 과정에서 값의 형태와 허용 범위를 확인합니다. 변환 뒤에는 mode가 유효한 열거형 값이고 workers가 0이 아닌 정수라는 조건이 타입에 담깁니다. 반면 Python의 json.loads는 사전을 반환하므로 사용 코드가 필드 내용을 직접 검증하는 경우가 많습니다. Pydantic 같은 라이브러리는 Rust의 타입 중심 접근에 가까운 방식을 제공합니다.

Lobsters 반응

  • @ssokolow — AbsPathBuf가 PathBuf가 아니라 Utf8PathBuf를 감싼다는 점도 보세요. Utf8PathBuf는 camino 크레이트가 제공하는 사용자 정의 ‘파싱된’ 타입 정제입니다. Rust 표준 라이브러리의 일반 경로는 유효한 UTF-8이라고 보장되지 않으므로 String으로 쉽게 바꿀 수 없습니다. camino::Utf8PathBuf는 생성 시 UTF-8 유효성을 보장하고, 이후 문자열로 바꿀 수 있습니다. 다행히 이런 선택은 rust-analyzer에만 있는 것 같네요. 아마추어 기록관리자이자 ‘수집품은 있지만 수집가는 아닌 사람’으로서 제 파일 시스템에는 올바른 iconv 변환을 찾을 시간이 없었던 깨진 문자 파일명이 있습니다. 예전 DOS 디스크의 날짜 메타데이터가 망가졌는데 파일 자체는 검증을 통과하는 경우도 있습니다. POSIX에서 유효한 경로를 처리하지 못해 도구가 종료되는 상황은 정말 싫습니다.

원문: Eli Bendersky / 번역·요약: Trawling