Rust에서 '검증하지 말고 파싱하라' 원칙을 타입으로 구현하는 방법

Rusty thoughts on "Parse, don't validate"

Alexis King의 'Parse, don't validate' 패턴을 Rust에 적용한 사례 연구. Vec 대신 NonEmpty 타입을 사용하면 컴파일 타임에 비어 있지 않음이 보장되어 Option 처리가 사라진다. rust-analyzer의 AbsPathBuf와 NonZero, serde 역직렬화 등 표준 라이브러리와 실제 프로젝트에서 이 패턴이 어떻게 쓰이는지 구체적인 예제로 설명한다.

성공적인 결과가 NonEmpty라면, 클라이언트 코드는 다시 비어 있는지 확인할 필요가 없다. 이는 타입 시스템에 의해 강제된다!
  1. articulatepang

    저는 좀 더 일반적인 규칙을 선호합니다: 불법 상태를 표현 불가능하게 만들라(Make Illegal States Unrepresentable). "검증하지 말고 파싱하라" 규칙은 MISU의 특수한 경우입니다.

    차이점이 뭘까요? MISU는 파싱 같은 변환이 일어나지 않을 때도 적용됩니다. 예를 들어, 네트워크 연결의 현재 상태를 나타내는 변수가 있고, 그 변수가 Disconnected이거나 어떤 IP 주소에 Connected일 수 있다고 합시다(이건 지나치게 단순화한 예입니다).

    그러면 한 가지 방법은 이렇게 하는 겁니다:

    struct {

    connected: bool,

    peer_ip: int32

    }

    문제는 이렇게 하면 불법적이거나 의미 없는 상태를 표현할 수 있다는 점입니다: 연결이 끊어졌는데도 예전 peer_ip 찌꺼기가 남아 있는 경우죠. 더 나쁜 경우로, 이렇게 작성했을 수도 있습니다:

    struct {

    connected: bool,

    peer_ip: Option<int32>

    }

    이제 connected = true인데 peer_ip = None인 상태가 가능해집니다.

    해결책은 합 타입(sum type)을 사용하는 것입니다:

    type connection =

    Disconnected

    | Connected of int32

    (임의로 만든 문법을 써서 죄송합니다; Rust에 익숙한 분이라면 이해하실 거라 믿습니다.)

    "불법 상태를 표현 불가능하게 만들라"는 프로그램 전체에, 프로그램 내 모듈이나 함수 사이의 모든 인터페이스에 적용됩니다. 입력 파싱도 포함되지만 이에 국한되지는 않습니다.

  2. Fluorescence

    그 타입이 좋은 조언인지 잘 모르겠습니다:

    pub struct NonEmpty<T> {

    pub head: T,

    pub tail: Vec<T>,

    }

    슬라이스와 반복(iteration)의 편의성을 지원하려면 트레이트를 수동으로 구현해야 하고, 소유권을 Vec으로 넘겨야 할 때는 비용이 큰 재할당이 필요합니다.

    저는 이렇게 기대합니다:

    pub struct NonEmpty<T> {

    v: Vec<T>,

    }

    생성자가 불변식을 강제하고, 그런 다음 [T]에 대한 Deref와 DerefMut를 구현해서 일반적인 len/is_empty/인덱싱/반복을 얻고, 다른 함수에 &[T]로 전달하고 값을 변경할 수 있습니다(불변식을 깨뜨릴 수 없습니다).

    불변식을 유지하면서 길이를 변경하려면 선택은 자유입니다. 예를 들어:

    - unwrap/mutate/rewrap을 위한 .into_vec() 추가

    - 원하는 불변식 보존 변경자(mutator) 추가

  3. OptionOfT

    각주에 대해:

    > [1] Go나 Python 같은 다른 언어들은 빈 리스트나 슬라이스에서 lst[0]에 접근할 때 예외나 패닉을 발생시키는 런타임 검사를 합니다.

    Rust도 같은 것을 가지고 있습니다. `Vec`을 인덱스로 접근하는 것은 인덱스 트레이트를 통해 이루어집니다: https://doc.rust-lang.org/std/ops/trait.Index.html#tymethod....

    Vec은 여기서 Index를 구현합니다: https://github.com/rust-lang/rust/blob/d080e7dff1b0fc5454154...

    Vec의 Index는 슬라이스의 Index로 위임합니다: https://github.com/rust-lang/rust/blob/d080e7dff1b0fc5454154...

    슬라이스는 ... 인트린직으로 위임합니다: https://github.com/rust-lang/rust/blob/d080e7dff1b0fc5454154...

    그리고 이것이 경계 검사를 삽입합니다: https://github.com/rust-lang/rust/blob/d080e7dff1b0fc5454154...

    그 경계 검사는: https://github.com/rust-lang/rust/blob/d080e7dff1b0fc5454154...

    따라서 각주는 정확하지 않다고 봅니다.

  4. jelder

    이거 정말 좋네요. Alexis King은 실제로 "Parse, Don't Validate"가 이렇게 인기를 끌 줄 알았더라면 Haskell보다 더 널리 쓰이는 언어로 썼을 거라고 말했다고 합니다.

  5. jph

    Rust의 타입 강점에 관한 좋은 글입니다. 이게 마음에 든다면, 여러분만의 파싱 기능을 어떻게 만들지 궁금할 수 있습니다. 저는 Rust 크레이트인 Winnow와 Nom, 그리고 Rust 트레이트인 From과 Into를 좋아합니다.

이 날의 다른 글

2026-09-27