-
타입 시스템을 활용해 런타임 검증 대신 컴파일 타임에 불변식을 보장하는 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의 강력한 타입 시스템을 최대한 활용해 컴파일러가 오류를 잡아주는 코드 작성을 권장함