Rust에서 ‘검증하지 말고 파싱하라’와 타입 주도 설계

5 days ago 4

  • 타입 시스템을 활용해 런타임 검증 대신 컴파일 타임에 불변식을 보장하는 Rust 설계 방식을 설명함
  • NonZeroF32, NonEmptyVec 같은 새로운 타입(newtype) 을 정의해 잘못된 상태(0, 빈 벡터 등)를 표현 불가능하게 만듦
  • Option이나 Result로 실패를 반환하는 대신, 함수 인자에서 제약을 강화해 오류를 사전에 차단함
  • String::from_utf8이나 serde_json::from_str처럼 파싱을 통해 의미 있는 타입으로 변환하는 사례를 제시함
  • 불법 상태를 표현 불가능하게 만들고, 검증을 가능한 한 앞당기는 설계 원칙이 코드 안정성과 가독성을 높임

1. 런타임 검증 대신 타입으로 제약 표현

  • divide(a, b) 함수에서 0으로 나누면 런타임 패닉이 발생함
    • Option을 반환해 실패를 표현할 수 있지만, 이는 반환 타입을 약화시키는 형태임
  • NonZeroF32 타입을 정의해 0이 아닌 값만 생성 가능하게 함
    • 생성자는 fn new(n: f32) -> Option<NonZeroF32> 형태로, 실패 시 None 반환
    • divide_floats(a: f32, b: NonZeroF32)로 정의하면 런타임 검증이 불필요함
  • 검증 책임을 함수 내부에서 호출자 쪽으로 이동시켜, 오류를 사전에 제거함

2. 중복 검증 제거와 코드 단순화

  • roots(a, b, c) 함수에서 a == 0 검증을 Option으로 처리하면 호출자와 함수 양쪽에서 중복 검증 발생
  • NonZeroF32를 사용하면 검증이 한 번만 수행되고, 이후 로직은 단순화됨
  • 같은 원리로 NonEmptyVec<T>를 정의해 빈 벡터를 허용하지 않음
    • get_cfg_dirs()가 NonEmptyVec<PathBuf>를 반환하면, 이후 main()에서 추가 검증이 불필요함

3. 실제 사례: String과 serde_json

  • String은 내부적으로 Vec<u8>의 새로운 타입(newtype) 이며, String::from_utf8이 유효성 검사를 수행함
    • 이후에는 UTF-8이 보장된 문자열로 안전하게 사용 가능
  • serde_json의 from_str::<Sample>은 JSON을 구조체로 파싱해 필드 존재와 타입 일관성을 컴파일 타임에 보장
    • foo, bar 필드 존재, 타입 일치, 배열 길이 등 모든 제약이 타입 수준에서 확인됨

4. 타입 주도 설계의 두 가지 원칙

  • 불법 상태를 표현 불가능하게 만들기
    • NonZeroF32는 0을, NonEmptyVec은 빈 상태를 표현할 수 없음
    • 단순 검증 함수(is_nonzero)는 여전히 잘못된 상태를 표현할 수 있어 불완전함
  • 검증은 가능한 한 앞당겨 수행하기
    • ‘Shotgun Parsing’처럼 검증이 코드 전반에 흩어지면 보안 취약점(CVE-2016-0752 등)으로 이어질 수 있음
    • 파싱 단계에서 모든 제약을 확인하면 이후 로직은 안전하게 실행 가능

5. Rust에서의 타입 기반 증명과 응용

  • Curry-Howard 대응에 따라 타입은 논리 명제, 값은 그 증명으로 볼 수 있음
    • typenum 크레이트를 사용하면 컴파일 타임에 수학적 관계(3 + 4 = 8)를 검증 가능
  • 타입 시스템을 이용해 프로그램의 정확성을 컴파일 시점에 증명할 수 있음

6. 실무 적용 조언

  • 외부 API가 단순 타입(bool, i32)을 요구하더라도, 내부에서는 의미 있는 enum이나 newtype으로 표현할 것
    • 예: LightBulbState { On, Off }를 정의하고 From<LightBulbState> for bool 구현
  • verify()나 do_something_fallible()처럼 단순 검증 함수가 있다면, 파싱을 통한 구조화된 타입 변환을 고려할 것
  • 부작용 없는 함수라면 Result<Infallible, MyError>처럼 의도적으로 불가능한 상태를 타입으로 표현할 수 있음

7. 결론

  • Rust의 타입 시스템을 검증 도구로 활용하면 코드의 명확성과 안정성이 향상됨
  • Vec, sqlx, bon 등 Rust 생태계의 여러 도구가 이미 타입 기반 설계를 활용 중임
  • 모든 문제를 타입으로 해결할 수는 없지만, 검증 로직을 타입으로 끌어올리는 접근은 유지보수성과 안전성을 높임
  • Rust의 강력한 타입 시스템을 최대한 활용해 컴파일러가 오류를 잡아주는 코드 작성을 권장함

Read Entire Article