Rust 구조체와 enum, Rust 벡터, Rust 컬렉션을 활용한 체계적인 데이터 모델링 방법을 실전 예제와 함께 완벽하게 마스터하는 종합 가이드입니다.
들어가며
Rust는 강력한 타입 시스템과 메모리 안전성을 제공하는 시스템 프로그래밍 언어입니다.
이번 글에서는 Rust의 핵심 데이터 구조인 구조체(struct), 열거형(enum), 그리고 다양한 컬렉션들을 깊이 있게 다루어보겠습니다.
특히 실무에서 자주 사용하는 패턴들과 함께 효율적인 데이터 모델링 방법을 알아보겠습니다.
Rust 구조체(Struct) 완전 정복
구조체의 기본 개념과 정의
Rust에서 구조체는 관련된 데이터들을 하나의 단위로 묶어서 관리할 수 있는 사용자 정의 데이터 타입입니다.
구조체를 통해 복잡한 데이터를 체계적으로 모델링할 수 있으며, 객체 지향 프로그래밍의 기초가 됩니다.
// 기본 구조체 정의
struct User {
username: String,
email: String,
age: u32,
active: bool,
}
// 구조체 인스턴스 생성
let user1 = User {
username: String::from("johndoe"),
email: String::from("john@example.com"),
age: 30,
active: true,
};
구조체의 다양한 형태
Rust에서는 세 가지 형태의 구조체를 제공합니다:
- Named Field Struct: 일반적인 구조체 형태
- Tuple Struct: 필드명 없이 순서로만 구분되는 구조체
- Unit Struct: 필드가 없는 구조체
// Named Field Struct
struct Person {
name: String,
age: u32,
}
// Tuple Struct
struct Color(u8, u8, u8);
struct Point(f64, f64, f64);
// Unit Struct
struct AlwaysEqual;
구조체 메서드와 연관 함수
구조체에는 메서드와 연관 함수를 정의할 수 있습니다.
메서드는 구조체 인스턴스에 대해 호출되며, 연관 함수는 구조체 타입 자체에 대해 호출됩니다.
impl User {
// 연관 함수 (생성자 역할)
fn new(username: String, email: String) -> User {
User {
username,
email,
age: 0,
active: true,
}
}
// 메서드
fn get_info(&self) -> String {
format!("User: {}, Email: {}", self.username, self.email)
}
// 가변 메서드
fn update_age(&mut self, new_age: u32) {
self.age = new_age;
}
}
// 사용 예시
let mut user = User::new(String::from("alice"), String::from("alice@example.com"));
user.update_age(25);
println!("{}", user.get_info());
Rust 열거형(Enum) 심화 가이드
열거형의 기본 구조
Rust의 열거형은 다른 언어들과 비교해 매우 강력한 기능을 제공합니다.
각 variant가 서로 다른 타입의 데이터를 가질 수 있으며, 패턴 매칭을 통해 안전하게 처리할 수 있습니다.
// 기본 열거형
enum Direction {
Up,
Down,
Left,
Right,
}
// 데이터를 가지는 열거형
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
Option과 Result 활용
Rust에서 가장 중요한 두 가지 열거형은 Option
과 Result
입니다.
이들은 null pointer exception과 에러 처리를 안전하게 해결해줍니다.
// Option 활용
fn find_user(id: u32) -> Option<User> {
if id == 1 {
Some(User::new(String::from("user1"), String::from("user1@example.com")))
} else {
None
}
}
// Result 활용
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("Division by zero"))
} else {
Ok(a / b)
}
}
// 패턴 매칭을 통한 안전한 처리
match find_user(1) {
Some(user) => println!("Found user: {}", user.get_info()),
None => println!("User not found"),
}
열거형 메서드 구현
열거형에도 메서드를 구현할 수 있습니다.
이를 통해 각 variant에 대한 특별한 동작을 정의할 수 있습니다.
impl Message {
fn call(&self) {
match self {
Message::Quit => println!("Quitting application"),
Message::Move { x, y } => println!("Moving to ({}, {})", x, y),
Message::Write(text) => println!("Writing: {}", text),
Message::ChangeColor(r, g, b) => println!("Changing color to RGB({}, {}, {})", r, g, b),
}
}
}
// 사용 예시
let msg = Message::Write(String::from("Hello, Rust!"));
msg.call();
Rust 벡터(Vec) 완전 활용법
벡터의 기본 개념과 생성
Rust 벡터는 동적 배열로, 런타임에 크기가 변경될 수 있는 컬렉션입니다.
힙 메모리에 연속적으로 저장되며, 인덱스를 통한 빠른 접근이 가능합니다.
// 벡터 생성 방법들
let mut v1: Vec<i32> = Vec::new();
let mut v2 = vec![1, 2, 3, 4, 5];
let mut v3 = Vec::with_capacity(10);
// 요소 추가
v1.push(10);
v1.push(20);
v1.push(30);
// 요소 접근
println!("First element: {}", v2[0]);
match v2.get(1) {
Some(value) => println!("Second element: {}", value),
None => println!("No element at index 1"),
}
벡터 조작과 성능 최적화
벡터의 효율적인 사용을 위해서는 메모리 할당과 재할당을 이해해야 합니다.
// 용량 관리
let mut v = Vec::with_capacity(100);
println!("Capacity: {}, Length: {}", v.capacity(), v.len());
// 요소 제거
v.push(1);
v.push(2);
v.push(3);
let popped = v.pop(); // Some(3)
v.remove(0); // 첫 번째 요소 제거
// 벡터 반복
for item in &v {
println!("{}", item);
}
// 소유권을 가져오는 반복
for item in v {
println!("{}", item);
}
Rust 컬렉션 종합 분석
주요 컬렉션 타입 비교
컬렉션 타입 | 특징 | 사용 시기 | 시간 복잡도 |
---|---|---|---|
Vec | 연속 메모리, 인덱스 접근 | 순서가 중요하고 인덱스 접근이 필요할 때 | O(1) 접근, O(n) 삽입/삭제 |
HashMap<K,V> | 키-값 쌍, 해시 기반 | 키를 통한 빠른 조회가 필요할 때 | O(1) 평균 접근 |
BTreeMap<K,V> | 정렬된 키-값 쌍 | 정렬된 순서가 필요할 때 | O(log n) 접근 |
HashSet | 중복 없는 값들 | 유니크한 값들의 집합이 필요할 때 | O(1) 평균 접근 |
HashMap 활용 패턴
HashMap은 키-값 쌍을 저장하는 데 사용되며, 해시 함수를 통해 빠른 조회가 가능합니다.
use std::collections::HashMap;
// HashMap 생성과 사용
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
// 값 조회
let team_name = String::from("Blue");
let score = scores.get(&team_name);
// 키가 없을 때만 삽입
scores.entry(String::from("Red")).or_insert(20);
// 값 업데이트
let count = scores.entry(String::from("Blue")).or_insert(0);
*count += 10;
// 반복 처리
for (key, value) in &scores {
println!("{}: {}", key, value);
}
컬렉션 성능 최적화 전략
use std::collections::{HashMap, BTreeMap, HashSet};
// 적절한 초기 용량 설정
let mut map = HashMap::with_capacity(1000);
let mut set = HashSet::with_capacity(500);
// 메모리 효율적인 문자열 처리
let mut string_map: HashMap<&str, i32> = HashMap::new();
string_map.insert("key1", 100);
string_map.insert("key2", 200);
// 대용량 데이터 처리 시 Iterator 활용
let large_vec: Vec<i32> = (0..1000000).collect();
let sum: i32 = large_vec.iter().filter(|&&x| x % 2 == 0).sum();
실전 데이터 모델링 예제
복합 데이터 구조 설계
실제 프로젝트에서 사용할 수 있는 복합 데이터 구조를 설계해보겠습니다.
use std::collections::HashMap;
// 사용자 정보 구조체
#[derive(Debug, Clone)]
struct User {
id: u32,
username: String,
email: String,
profile: UserProfile,
}
// 사용자 프로필 구조체
#[derive(Debug, Clone)]
struct UserProfile {
first_name: String,
last_name: String,
age: Option<u32>,
interests: Vec<String>,
}
// 게시물 상태 열거형
#[derive(Debug, PartialEq)]
enum PostStatus {
Draft,
Published,
Archived,
}
// 게시물 구조체
#[derive(Debug)]
struct Post {
id: u32,
title: String,
content: String,
author_id: u32,
status: PostStatus,
tags: Vec<String>,
metadata: HashMap<String, String>,
}
// 블로그 시스템 구조체
struct BlogSystem {
users: HashMap<u32, User>,
posts: HashMap<u32, Post>,
user_posts: HashMap<u32, Vec<u32>>, // user_id -> post_ids
}
impl BlogSystem {
fn new() -> Self {
BlogSystem {
users: HashMap::new(),
posts: HashMap::new(),
user_posts: HashMap::new(),
}
}
fn add_user(&mut self, user: User) {
self.user_posts.insert(user.id, Vec::new());
self.users.insert(user.id, user);
}
fn add_post(&mut self, post: Post) {
let author_id = post.author_id;
let post_id = post.id;
self.posts.insert(post_id, post);
self.user_posts.get_mut(&author_id).unwrap().push(post_id);
}
fn get_user_posts(&self, user_id: u32) -> Vec<&Post> {
if let Some(post_ids) = self.user_posts.get(&user_id) {
post_ids.iter()
.filter_map(|&post_id| self.posts.get(&post_id))
.collect()
} else {
Vec::new()
}
}
}
에러 처리와 검증
실제 애플리케이션에서는 데이터 검증과 에러 처리가 중요합니다.
// 사용자 정의 에러 타입
#[derive(Debug)]
enum BlogError {
UserNotFound,
PostNotFound,
InvalidEmail,
DuplicateUser,
}
impl std::fmt::Display for BlogError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
BlogError::UserNotFound => write!(f, "User not found"),
BlogError::PostNotFound => write!(f, "Post not found"),
BlogError::InvalidEmail => write!(f, "Invalid email format"),
BlogError::DuplicateUser => write!(f, "User already exists"),
}
}
}
impl std::error::Error for BlogError {}
// 검증 함수들
fn validate_email(email: &str) -> Result<(), BlogError> {
if email.contains('@') && email.contains('.') {
Ok(())
} else {
Err(BlogError::InvalidEmail)
}
}
// 안전한 사용자 생성
fn create_user(id: u32, username: String, email: String) -> Result<User, BlogError> {
validate_email(&email)?;
Ok(User {
id,
username,
email,
profile: UserProfile {
first_name: String::new(),
last_name: String::new(),
age: None,
interests: Vec::new(),
},
})
}
성능 최적화 팁
메모리 효율성 개선
// 1. 적절한 초기 용량 설정
let mut vec = Vec::with_capacity(expected_size);
let mut map = HashMap::with_capacity(expected_size);
// 2. 불필요한 클론 방지
fn process_data(data: &[i32]) -> i32 {
data.iter().sum() // 소유권을 가져오지 않음
}
// 3. 문자열 처리 최적화
use std::borrow::Cow;
fn process_string(s: Cow<str>) -> Cow<str> {
if s.contains("special") {
Cow::Owned(s.replace("special", "modified"))
} else {
s // 변경하지 않으면 원본 반환
}
}
반복자 활용 최적화
// 효율적인 컬렉션 처리
let numbers: Vec<i32> = (1..=1000).collect();
// 체이닝을 통한 효율적 처리
let result: Vec<i32> = numbers
.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * x)
.collect();
// 지연 평가 활용
let large_data: Vec<i32> = (1..=1000000).collect();
let first_match = large_data
.iter()
.filter(|&&x| x % 997 == 0)
.next(); // 첫 번째 매치만 찾으면 중단
트레이트와 제네릭 활용
제네릭 구조체와 열거형
// 제네릭 구조체
struct Container<T> {
value: T,
}
impl<T> Container<T> {
fn new(value: T) -> Self {
Container { value }
}
fn get(&self) -> &T {
&self.value
}
}
// 제네릭 열거형
enum Result<T, E> {
Ok(T),
Err(E),
}
// 제네릭 함수
fn find_item<T: PartialEq>(items: &[T], target: &T) -> Option<usize> {
items.iter().position(|item| item == target)
}
트레이트 구현과 활용
// 디스플레이 트레이트 구현
impl std::fmt::Display for User {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "User(id: {}, username: {})", self.id, self.username)
}
}
// 사용자 정의 트레이트
trait Summarizable {
fn summarize(&self) -> String;
}
impl Summarizable for Post {
fn summarize(&self) -> String {
format!("{} - {}", self.title, self.status)
}
}
실무 활용 사례
웹 애플리케이션 데이터 모델
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Serialize, Deserialize)]
struct ApiResponse<T> {
success: bool,
data: Option<T>,
error: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct ProductCatalog {
products: Vec<Product>,
categories: HashMap<String, Vec<u32>>,
metadata: CatalogMetadata,
}
#[derive(Debug, Serialize, Deserialize)]
struct Product {
id: u32,
name: String,
price: f64,
category: String,
inventory: InventoryStatus,
}
#[derive(Debug, Serialize, Deserialize)]
enum InventoryStatus {
InStock(u32),
OutOfStock,
Discontinued,
}
#[derive(Debug, Serialize, Deserialize)]
struct CatalogMetadata {
version: String,
last_updated: String,
total_products: u32,
}
데이터베이스 연동 패턴
// 데이터베이스 엔티티 매핑
#[derive(Debug)]
struct DatabaseEntity {
id: Option<u32>,
created_at: Option<String>,
updated_at: Option<String>,
}
trait Repository<T> {
fn find_by_id(&self, id: u32) -> Option<T>;
fn save(&mut self, entity: T) -> Result<T, String>;
fn delete(&mut self, id: u32) -> Result<(), String>;
}
// 실제 구현 예시
struct UserRepository {
users: HashMap<u32, User>,
next_id: u32,
}
impl Repository<User> for UserRepository {
fn find_by_id(&self, id: u32) -> Option<User> {
self.users.get(&id).cloned()
}
fn save(&mut self, mut user: User) -> Result<User, String> {
if user.id == 0 {
user.id = self.next_id;
self.next_id += 1;
}
validate_email(&user.email)
.map_err(|e| e.to_string())?;
self.users.insert(user.id, user.clone());
Ok(user)
}
fn delete(&mut self, id: u32) -> Result<(), String> {
self.users.remove(&id)
.map(|_| ())
.ok_or_else(|| "User not found".to_string())
}
}
디버깅과 테스트
디버깅 도구 활용
// Debug 트레이트 자동 구현
#[derive(Debug)]
struct DebugStruct {
field1: i32,
field2: String,
}
// 사용자 정의 Debug 구현
impl std::fmt::Debug for User {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("User")
.field("id", &self.id)
.field("username", &self.username)
.field("email", &"[REDACTED]") // 민감한 정보 숨김
.finish()
}
}
// 조건부 디버깅
fn debug_collection<T: std::fmt::Debug>(items: &[T]) {
if cfg!(debug_assertions) {
for (i, item) in items.iter().enumerate() {
println!("Item {}: {:?}", i, item);
}
}
}
단위 테스트 작성
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_user_creation() {
let user = create_user(
1,
"testuser".to_string(),
"test@example.com".to_string()
);
assert!(user.is_ok());
let user = user.unwrap();
assert_eq!(user.username, "testuser");
assert_eq!(user.email, "test@example.com");
}
#[test]
fn test_invalid_email() {
let result = create_user(
1,
"testuser".to_string(),
"invalid-email".to_string()
);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), BlogError::InvalidEmail));
}
#[test]
fn test_blog_system() {
let mut blog = BlogSystem::new();
let user = create_user(1, "author".to_string(), "author@example.com".to_string()).unwrap();
blog.add_user(user);
let post = Post {
id: 1,
title: "Test Post".to_string(),
content: "This is a test post".to_string(),
author_id: 1,
status: PostStatus::Published,
tags: vec!["test".to_string(), "rust".to_string()],
metadata: HashMap::new(),
};
blog.add_post(post);
let user_posts = blog.get_user_posts(1);
assert_eq!(user_posts.len(), 1);
assert_eq!(user_posts[0].title, "Test Post");
}
}
마무리
이 글에서는 Rust의 핵심 데이터 구조인 구조체, 열거형, 컬렉션에 대해 체계적으로 알아보았습니다.
각 데이터 구조의 특징과 활용법을 이해하고, 실전에서 사용할 수 있는 패턴들을 익혔습니다.
Rust의 타입 시스템과 소유권 개념을 활용하여 안전하고 효율적인 프로그램을 작성할 수 있게 되었습니다.
다음 글에서는 "Rust 모듈, 패키지, 크레이트, 트레잇, 제네릭, 표준 라이브러리 한방에 끝내기"를 통해 더 큰 규모의 Rust 프로젝트 구조화 방법을 다루겠습니다.
[Rust입문 #6] Rust 모듈, 패키지, 크레이트, 트레잇, 제네릭, 표준 라이브러리 한방에 끝내기
Rust 모듈 구조부터 패키지, 크레이트, 트레잇, 제네릭, 표준 라이브러리까지 실무에서 필요한 핵심 개념들을 체계적으로 정리한 완전 가이드입니다.왜 Rust 모듈과 패키지 시스템을 알아야 할까?R
notavoid.tistory.com
참고 자료
관련 글 추천:
[Rust입문 #4] Rust 소유권과 참조, 에러 처리 완벽 이해하기
Rust의 소유권(ownership), 참조(reference), 에러 처리는 메모리 안전성을 보장하는 핵심 개념으로,이 가이드에서는 실무에서 바로 활용할 수 있는 실전 예제와 함께 완벽하게 마스터할 수 있는 방법을
notavoid.tistory.com
'프로그래밍 언어 실전 가이드' 카테고리의 다른 글
[Rust입문 #6] Rust 모듈, 패키지, 크레이트, 트레잇, 제네릭, 표준 라이브러리 한방에 끝내기 (0) | 2025.07.14 |
---|---|
[Rust입문 #4] Rust 소유권과 참조, 에러 처리 완벽 이해하기 (0) | 2025.07.08 |
[Rust입문 #3] Rust 조건문, 반복문, 패턴매칭 기초 한 번에 끝내기 (0) | 2025.07.08 |
[Rust입문 #2] Rust 첫 프로그램부터 변수, 데이터 타입, 함수 완전정복 (0) | 2025.07.07 |
[Rust입문 #1] Rust란 무엇인가? 특징, 장점, 설치와 개발환경 완벽 가이드 (0) | 2025.07.07 |