Rust 튜토리얼 #4: 구조체와 열거형
이번 포스팅에서는 Rust의 중요한 데이터 타입인 구조체(Structs)와 열거형(Enums)에 대해 알아보겠습니다. 이 두 가지 개념은 복잡한 데이터를 효과적으로 표현하고 다루는 데 필수적입니다.
Rust의 구조체와 열거형은 C언어의 그것들과 기본 개념은 유사하지만, Rust는 더 강력한 타입 안전성, 데이터를 포함할 수 있는 열거형, 메서드 정의 기능, 강력한 패턴 매칭, 그리고 소유권 시스템을 통한 메모리 안전성을 제공하여 더 표현력이 풍부하고 안전한 프로그래밍을 가능하게 합니다.
특징 | Rust | C |
---|---|---|
구조체 정의 | struct Point { x: i32, y: i32 } |
struct Point { int x; int y; }; |
구조체 메서드 | 직접 정의 가능 (impl 블록 사용) |
직접 정의 불가 (함수로 대체) |
열거형 정의 | enum Color { Red, Green, Blue } |
enum Color { RED, GREEN, BLUE }; |
열거형 값 접근 | Color::Red |
RED |
데이터 포함 열거형 | 지원 (enum Value { Int(i32), Float(f32) } ) |
미지원 (공용체로 유사 구현) |
패턴 매칭 | match 표현식 (철저한 검사) |
switch 문 (철저한 검사 없음) |
타입 안전성 | 높음 | 상대적으로 낮음 |
메모리 안전성 | 컴파일 시간에 보장 | 프로그래머 책임 |
NULL 포인터 | Option<T> 사용 (더 안전) |
가능 (런타임 오류 위험) |
불변성 | 기본적으로 불변 (mut 키워드로 가변) |
기본적으로 가변 |
1. 구조체 (Structs)
구조체는 여러 관련 데이터를 하나의 의미 있는 그룹으로 묶는 사용자 정의 데이터 타입입니다.
1.1 구조체 정의와 인스턴스 생성
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn main() {
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
}
이 메모리 레이아웃 다이어그램은 Rust의 'User' 구조체를 시각화한 것입니다. 여기서 중요한 점은 String 타입의 표현 방식입니다.
실제로 Rust의 String 타입은 구조체 내에서 24바이트를 차지합니다 (64비트 시스템 기준). 그리고 나머지 필드의 내용은 다음과 같습니다.
- 'username' 필드: String 타입으로 24바이트를 차지하며, 메모리 주소 0x00에서 시작합니다.
- 'email' 필드: String 타입으로 24바이트를 차지하며, 메모리 주소 0x18에서 시작합니다.
- 'sign_in_count' 필드: u64 타입으로 24바이트를 차지하며, 메모리 주소 0x30에서 시작합니다.
- 'active' 필드: bool 타입으로 1바이트를 차지하며, 메모리 주소 0x38에 위치합니다.
- 패딩: 메모리 정렬을 위해 7바이트의 패딩이 추가되어 있으며, 메모리 주소 0x39에서 시작합니다.
각 필드는 색상으로 구분되어 있으며, 하단에는 String 타입이 힙에 할당된 데이터를 가리키는 8바이트 포인터로 표현된다는 설명이 포함되어 있습니다. 이 레이아웃은 64비트 시스템을 가정하고 있으며, 메모리 정렬과 효율성을 고려하여 구성되어 있습니다.
1.2 구조체 업데이트 문법
구조체 업데이트 문법은 기존 구조체 인스턴스의 일부 필드만 변경하고 나머지는 그대로 유지하여 새로운 인스턴스를 생성하는 간편한 방법으로, .. 문법을 사용하여 명시되지 않은 필드들을 기존 인스턴스에서 자동으로 복사합니다.
let user2 = User {
email: String::from("another@example.com"),
..user1
};
1.3 튜플 구조체
튜플 구조체는 필드에 이름을 부여하지 않고 타입만 선언하는 Rust의 데이터 구조로, 일반 구조체의 명명된 필드와 튜플의 간결함을 결합하여 간단한 데이터 그룹화가 필요할 때 유용하며, struct Name(Type1, Type2, ...) 형태로 정의하고 인덱스를 통해 필드에 접근할 수 있어 관련 데이터를 묶되 각 요소의 의미가 명확할 때 주로 사용됩니다.
struct Color(i32, i32, i32);
let black = Color(0, 0, 0);
1.4 메서드 정의
Rust의 메서드 정의는 C언어와 달리 구조체나 열거형에 직접 연관된 함수를 impl 블록 내에서 정의할 수 있어, 객체 지향적 접근이 가능하며, C에서는 구조체 포인터를 명시적으로 전달해야 하는 반면 Rust에서는 self를 통해 인스턴스에 자동으로 접근할 수 있어 더 간결하고 안전한 코드 작성이 가능합니다. 또한, Rust의 이 방식은 데이터와 그 데이터를 다루는 함수를 논리적으로 그룹화하여 코드의 구조화와 가독성을 크게 향상시킵니다.
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!("The area of the rectangle is {} square pixels.", rect1.area());
}
2. 열거형 (Enums)
열거형은 가능한 값들의 집합을 정의하는 데이터 타입입니다.
2.1 열거형 정의
Rust의 열거형 정의는 enum 키워드를 사용하여 서로 관련된 여러 가지 값들을 하나의 타입으로 그룹화하는 방법으로, C언어의 열거형보다 훨씬 강력하여 각 variant에 다양한 타입의 데이터를 연관시킬 수 있고, 패턴 매칭과 함께 사용되어 복잡한 로직을 간결하고 타입 안전하게 표현할 수 있으며, 이를 통해 코드의 가독성과 유지보수성을 높이고 컴파일 시점에 많은 오류를 방지할 수 있습니다.
enum IpAddrKind {
V4,
V6,
}
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
2.2 데이터를 포함하는 열거형
Rust의 데이터를 포함하는 열거형은 각 variant가 서로 다른 타입과 양의 데이터를 포함할 수 있는 강력한 기능으로, C언어의 union과 enum을 결합한 것보다 더 안전하고 표현력이 뛰어나며, 예를 들어 enum IpAddr { V4(u8, u8, u8, u8), V6(String) }와 같이 정의하여 하나의 타입 내에서 다양한 구조의 데이터를 표현할 수 있고, 패턴 매칭과 함께 사용하여 각 경우를 타입 안전하게 처리할 수 있어, 복잡한 데이터 구조를 간결하고 안전하게 모델링하는 데 매우 유용합니다.
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
2.3 Option 열거형
Rust의 Option 열거형은 값이 있거나 없을 수 있는 상황을 안전하게 처리하기 위한 표준 라이브러리 타입으로, Some(T) 와 None 두 가지 variant를 가지며, C언어의 널 포인터 개념을 대체하여 더 안전하고 명시적인 방식으로 '값의 부재'를 표현합니다. 이 열거형은 컴파일러가 모든 가능한 경우를 처리하도록 강제하여 런타임 에러를 크게 줄이고, unwrap(), expect(), map(), and_then() 등의 메서드를 통해 값의 존재 여부에 따른 다양한 연산을 안전하고 편리하게 수행할 수 있어, Rust의 타입 시스템을 활용한 견고한 프로그래밍을 가능하게 합니다.
enum Option<T> {
Some(T),
None,
}
let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None;
2.4 match 표현식
Rust의 match 표현식은 값을 여러 패턴과 비교하여 일치하는 패턴의 코드를 실행하는 강력한 제어 흐름 구조입니다. C의 switch문보다 더 안전하고 표현력이 뛰어나며, 모든 가능한 경우를 처리해야 하는 철저한 검사를 강제합니다. 복잡한 데이터 구조의 분해, 변수 바인딩, 가드 조건 사용이 가능하여 열거형 처리와 에러 핸들링에 특히 유용합니다.
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
2.5 if let 간단한 제어 흐름
Rust의 if let 구문은 match 표현식의 간결한 대안으로, 단일 패턴만 매칭하고 나머지 경우는 무시하고자 할 때 사용됩니다. 이는 특히 Option<T>나 Result<T, E> 타입을 다룰 때 유용하며, 전체 match 문을 작성하는 것보다 더 간단하고 가독성 있게 코드를 작성할 수 있습니다. if let Some(value) = optional_value { ... } 형태로 사용되어, 특정 패턴에 일치할 때만 코드 블록을 실행하므로, 불필요한 boilerplate 코드를 줄이고 의도를 명확하게 표현할 수 있습니다.
let some_u8_value = Some(0u8);
if let Some(3) = some_u8_value {
println!("three");
}
마무리
이번 포스트에서는 Rust의 구조체와 열거형에 대해 알아보았습니다. 이 두 가지 개념은 복잡한 데이터 구조를 표현하고 다루는 데 매우 유용합니다. 구조체를 통해 관련 데이터를 그룹화하고, 열거형을 통해 여러 가능한 값들을 표현할 수 있습니다. 다음 포스트에서는 Rust의 모듈 시스템과 패키지 관리에 대해 알아보겠습니다. 이를 통해 더 큰 규모의 프로젝트를 구조화하고 관리하는 방법을 배우게 될 것입니다.