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