Rust 기초 강좌 #5 구조체와 열거형: match와 Option, null이 없는 언어의 설계
소유권과 빌림이라는 고비를 넘었으니, 이제 데이터를 프로그램의 형태로 조직하는 도구를 배울 차례입니다. Rust의 답은 두 가지입니다. “이 값들은 항상 함께 다닌다"를 표현하는 구조체(struct), 그리고 “이 값은 여러 모습 중 하나다"를 표현하는 열거형(enum)입니다. 특히 열거형과 match의 조합은 Rust에서 null 포인터 에러가 원천적으로 불가능한 이유로 이어집니다.
구조체: 함께 다니는 값들의 묶음 #
struct User {
email: String,
name: String,
active: bool,
}
let user = User {
email: String::from("kim@example.com"),
name: String::from("김개발"),
active: true,
};
println!("{}", user.email);클래스가 있는 언어의 데이터 부분과 비슷하지만, 상속은 없습니다. 동작은 impl 블록으로 별도로 붙입니다.
impl User {
fn display_name(&self) -> String {
format!("{} <{}>", self.name, self.email)
}
fn deactivate(&mut self) {
self.active = false;
}
}첫 매개변수가 곧 지난 편의 빌림 규칙입니다. 읽기만 하는 메서드는 &self, 상태를 바꾸는 메서드는 &mut self로 받습니다. 메서드 시그니처만 봐도 이 호출이 값을 바꾸는지 아닌지가 드러나고, mut로 선언하지 않은 인스턴스에서 deactivate를 부르면 컴파일 에러가 납니다. 디버깅 출력은 구조체 선언 위에 #[derive(Debug)]를 붙이고 println!("{user:?}")로 찍는 것이 기본기인데, 이 derive의 정체는 8편에서 다룹니다.
열거형: 여러 모습 중 하나 #
다른 언어의 열거형이 “이름 붙은 상수 목록"에 가깝다면, Rust의 열거형은 variant마다 다른 데이터를 담을 수 있습니다.
enum PaymentMethod {
Cash,
Card { number: String, installments: u8 },
Transfer(String), // 은행 계좌
}현금은 추가 정보가 없고, 카드는 번호와 할부 개월이 필요하고, 계좌 이체는 계좌 문자열 하나면 됩니다. 이런 데이터를 구조체 하나에 nullable 필드들로 욱여넣으면 “현금인데 카드 번호가 있는” 모순 상태가 가능해집니다. 열거형은 그 모순을 타입 차원에서 제거합니다. 잘못된 상태를 표현조차 할 수 없게 만드는 것이 Rust 데이터 설계의 핵심 습관입니다.
match: 모든 경우를 검사했는가 #
열거형을 소비하는 도구가 match입니다.
fn fee(method: &PaymentMethod) -> u32 {
match method {
PaymentMethod::Cash => 0,
PaymentMethod::Card { installments, .. } if *installments > 3 => 500,
PaymentMethod::Card { .. } => 300,
PaymentMethod::Transfer(_) => 100,
}
}switch와 비슷해 보이지만 결정적 차이가 둘 있습니다. 첫째, match는 표현식이라서 각 갈래의 값이 그대로 반환됩니다(2편의 “모든 것이 표현식"이 여기서 진가를 발휘합니다). 둘째, 모든 variant를 처리하지 않으면 컴파일 에러입니다.
error[E0004]: non-exhaustive patterns: `PaymentMethod::Transfer(_)` not coveredTransfer 갈래를 지우면 이 에러가 납니다. 나중에 열거형에 variant를 추가하면, 그 열거형을 match하는 모든 코드가 컴파일 에러로 바뀝니다. “결제 수단을 하나 추가했는데 처리 안 한 곳이 어딘가에 남아 있는” 종류의 버그가 컴파일러의 할 일 목록으로 바뀌는 것입니다. 나머지 전부를 뜻하는 _ 갈래도 있지만, 이 안전망을 끄는 행위이므로 기본값 처리가 정말 맞을 때만 씁니다.
Option: null을 대체하는 열거형 #
Rust에는 null이 없습니다. 대신 표준 라이브러리의 열거형 하나가 그 역할을 맡습니다.
enum Option<T> {
Some(T),
None,
}“값이 없을 수도 있음"이 타입에 드러나고, 컴파일러가 그 처리를 강제합니다. 이미 여러 번 만났습니다. 2편에서 parse의 결과를 unwrap으로 꺼낸 것이 사실 이 처리(정확히는 6편에서 다룰 Result)를 건너뛴 것이었습니다.
fn find_user(email: &str) -> Option<User> { /* ... */ }
match find_user("kim@example.com") {
Some(user) => println!("{}", user.display_name()),
None => println!("해당 사용자가 없습니다."),
}Option<User>는 User와 다른 타입이라서, 없을 가능성을 처리하기 전에는 안의 값을 사용할 수 없습니다. null 체크를 잊어서 나는 NullPointerException 계열 사고가 컴파일 에러로 바뀌는 구조입니다. 한 갈래만 관심 있을 때는 match 대신 if let이 간결합니다.
if let Some(user) = find_user("kim@example.com") {
println!("{}", user.display_name());
}unwrap()은 None이면 그대로 패닉으로 죽는 지름길입니다. 예제와 프로토타입에서는 편하지만, 실무 코드에서 unwrap을 쓸 때의 판단 기준은 다음 편에서 에러 처리 전체와 함께 정리하겠습니다.
정리 #
- 구조체는 함께 다니는 값의 묶음이고, 동작은
impl블록으로 붙입니다.&self와&mut self구분에 빌림 규칙이 그대로 이어집니다. - Rust의 열거형은 variant마다 다른 데이터를 담습니다. 모순된 상태를 타입 차원에서 표현할 수 없게 만드는 것이 설계의 핵심 습관입니다.
match는 모든 경우의 처리를 컴파일러가 강제하는 표현식입니다. variant 추가가 곧 “처리 누락 지점의 컴파일 에러 목록"이 됩니다.- null 대신
Option<T>가 있습니다. 값이 없을 가능성이 타입에 드러나므로, null 체크 누락이라는 버그 클래스가 사라집니다. - 다음 편은 에러 처리입니다. Option의 형제인
Result, 에러를 전파하는?연산자, 그리고 panic과의 경계를 다루겠습니다.