프로그래밍/Rust

Rust 타입 시스템의 마법

Lou Park 2024. 3. 22. 01:41

Rust의 장점 중 하나는 강력한 타입 시스템을 지원한다는 것이다.

이러한 Rust의 특징을 이용해 Type-driven Design을 도입해볼 수 있는데, <Let's Get Rusty>라는 유튜브 채널의 영상 내용을 빌려왔다.

 

Invaraint

Invariant란 컴퓨터 프로그램의 실행중 항상 참이되는 조건이다.

 

다음과 같은 BankAccount 구조체가 있다고하면, balance를 사용하려할때마다 코드 곳곳에서 balance가 음수인지 체크하게 될 것이다. 한 곳에 모여있다면 그나마 다행이지만 여러 군데 흩뿌려져 있거나... 검사하고 또 검사하는 상황도 벌어진다.

struct BankAccount {
	balance: i32,
}

 

 

BankAccount의 Invariant는 balance를 Unsigned Integer로 바꿈으로써 확보할 수 있다.

struct BankAccount {
	balance: u32,
}

 

 

Type-driven Design

Type-driven Design은 타입 시스템을 활용하여 Invariant를 강제하는 프로그래밍 기조다. 이를통해 프로그램의 안정성과 신뢰성을 높이고, 오류 가능성을 줄인다. 

#[derive(Debug)]
struct User {
	pub email: String,
	pub password: String,
}

https://www.youtube.com/watch?v=NDIU1GSBrVI

 

영상에 나오는 코드 한 조각을 캡쳐했다. 사용자로 부터 유저 정보를 입력받아 DB에 저장하는 API인데,

Form의 내용을 신뢰할 수 없으니 유효성 검사를 하고있다.

 

나도 이렇게 개발하고 있고, 일반적으로는 거리낌없이 저렇게 코드를 작성한다.

악몽은 다음 부분에서 시작된다.

https://www.youtube.com/watch?v=NDIU1GSBrVI

 

insert_user()는 DB에 넣기전에 "유효성 검사가 되어있다고 생각되는" User 구조체를 받는다.

이건 믿음의 영역이다. 여기에 들어온 User 구조체의 이메일과 비밀번호는 유효하다고, 앞단에서 검사했을 것이라고 가정해야한다. 혹여나 register_user() 함수를 리팩토링하다 빼먹을 수 있지만 말이다.

 

조금 더 꼼꼼하게 하고 싶은 프로그래머는 사진처럼 insert_user() 안에서 한 번 더 유효성 검사를 수행하기도 할 것이다. register_user()에서 실행하지만 말이다...

 

"Parse, don't validate"

Type-driven Design이 그래서 이 상황을 어떻게 해결하지? 궁금했다.

흥미롭게도 "파싱해라, 유효성 검사 말고"라는 신조에따르면 이렇게 전개된다.

#[derive(Debug)]
struct User {
	pub email: Email,
	pub password: Password,
}

pub struct Email(String);
put struct Password(String);

 

User 구조체에서 기존에 String이었던 이메일과 비밀번호는 Email, Password라는 새로운 타입으로 정의했다. (이런 패턴을 "Newtype Pattern"이라고 한다)

 

impl Email {
	pub fn parse(email: String) -> Result<Email, AuthError> {
		if !is_valid_email(&email) {
			Err(AuthError::ValidationError("Email must be valid".to_string()))
		} else {
			Ok(Email(email))
		}
	}
}

impl Password {
	pub fn parse(password: String) -> Result<Password, AuthError> {
		if !is_valid_password(&password) {
			Err(AuthError::ValidationError("Password must be valid".to_string()))
		} else {
			Ok(Password(password))
		}
	}
}

 

Email과 Password 구조체는 parse() 메소드를 가진다. 

이로서 User 구조체는 유효성이 통과된 이메일과 비밀번호 데이터만 가질 수 있도록 Invariant가 확보되었다.

 

https://www.youtube.com/watch?v=NDIU1GSBrVI

 

여기저기서 유효성 검사를 했던 이전 코드와는 달리, Type-driven Design을 도입하여 훨씬 안정성있는 형태가 되었다. 다소 강박적이라는 생각도 들 수 있지만, 타입 시스템이 주는 안정감은 이루말할 수 없다.