이 모듈은 커리큘럼의 전환점이에요. Phase 1에서 "일단 동작하게" 만들었던 코드를 Phase 3에서 배운 도구로 "Rust답게" 다듬어볼 거예요.
완벽한 코드를 처음부터 쓸 필요 없어요. 먼저 동작하게 만들고, 나중에 다듬는 것이 Rust 개발의 자연스러운 흐름이에요.
패턴 1: clone() → 빌림(&)
Before
모듈 06에서 이런 코드를 썼어요. 함수에 벡터를 넘기면 소유권이 이동하니까 clone()으로 해결했죠.
fn print_list(list: Vec<String>) {
for item in &list {
println!("- {item}");
}
}
fn main() {
let fruits = vec![
String::from("사과"),
String::from("바나나"),
String::from("포도"),
];
print_list(fruits.clone()); // clone()으로 복사!
println!("총 {}개", fruits.len());
}
After
이제 빌림을 알아요. &[String]으로 빌려주면 복사 비용이 사라져요.
fn print_list(list: &[String]) {
for item in list {
println!("- {item}");
}
}
fn main() {
let fruits = vec![
String::from("사과"),
String::from("바나나"),
String::from("포도"),
];
print_list(&fruits); // 빌려주기만!
println!("총 {}개", fruits.len());
}
무엇이 바뀌었나?
| | Before | After |
| ----------- | --------------------------- | --------------------------- |
| 매개변수 | Vec<String> (소유권 이동) | &[String] (슬라이스 참조) |
| 호출 방식 | fruits.clone() | &fruits |
| 메모리 비용 | 전체 벡터 복사 | 포인터 하나 전달 |
| 원본 사용 | clone() 덕분에 가능 | 빌림이니 당연히 가능 |
패턴 2: unwrap() → ? 연산자
Before
모듈 07의 숫자 맞추기 게임에서 unwrap()을 썼어요.
use std::io;
fn main() {
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap(); // 💥 에러 시 panic!
let number: i32 = input.trim().parse().unwrap(); // 💥 에러 시 panic!
println!("입력한 숫자: {number}");
}
unwrap()이 두 군데 있어요. 입력을 읽다 실패하거나, 숫자가 아닌 값을 파싱하면 프로그램이 바로 죽어버려요.
After
? 연산자로 에러를 우아하게 처리할 수 있어요.
use std::io;
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(ParseIntError),
}
impl From<io::Error> for AppError {
fn from(e: io::Error) -> Self {
AppError::Io(e)
}
}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::Parse(e)
}
}
fn read_number() -> Result<i32, AppError> {
let mut input = String::new();
io::stdin().read_line(&mut input)?; // ? 로 에러 전파
let number: i32 = input.trim().parse()?; // ? 로 에러 전파
Ok(number)
}
fn main() {
match read_number() {
Ok(n) => println!("입력한 숫자: {n}"),
Err(e) => println!("에러 발생: {:?}", e),
}
}
더 간단한 방법: Box<dyn Error>
에러 타입을 직접 정의하기 번거로우면, Box<dyn std::error::Error>를 쓸 수 있어요.
use std::io;
fn read_number() -> Result<i32, Box<dyn std::error::Error>> {
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let number: i32 = input.trim().parse()?;
Ok(number)
}
fn main() {
match read_number() {
Ok(n) => println!("입력한 숫자: {n}"),
Err(e) => println!("에러 발생: {e}"),
}
}
간단한 프로그램에서는 이 방식이 편해요!
무엇이 바뀌었나?
| | Before | After |
| --------------- | ------------------- | --------------------------------- |
| 에러 처리 | unwrap() — panic! | ? — 에러 전파 |
| 프로그램 안정성 | 에러 시 즉시 종료 | 에러를 받아서 대응 가능 |
| 함수 시그니처 | fn main() | fn read_number() -> Result<...> |
| 코드 가독성 | 짧지만 위험 | 약간 길지만 안전 |
패턴 3: for 루프 → 이터레이터 체인
Before
모듈 06에서 점수를 처리하는 코드를 for 루프로 썼어요.
fn main() {
let scores = vec![45, 82, 67, 93, 55, 78, 88];
// 60점 이상 합격자 수
let mut pass_count = 0;
for score in &scores {
if *score >= 60 {
pass_count += 1;
}
}
// 합격자 평균
let mut pass_total = 0;
for score in &scores {
if *score >= 60 {
pass_total += score;
}
}
let pass_avg = pass_total as f64 / pass_count as f64;
println!("합격자 수: {pass_count}");
println!("합격자 평균: {pass_avg:.1}");
}
After
이터레이터 체인으로 바꾸면 의도가 더 명확해져요.
fn main() {
let scores = vec![45, 82, 67, 93, 55, 78, 88];
let passing: Vec<&i32> = scores.iter().filter(|s| **s >= 60).collect();
let pass_count = passing.len();
let pass_avg = passing.iter().map(|s| **s as f64).sum::<f64>() / pass_count as f64;
println!("합격자 수: {pass_count}");
println!("합격자 평균: {pass_avg:.1}");
}
무엇이 바뀌었나?
| | Before | After |
| --------- | ----------------------- | --------------------- |
| 반복 횟수 | 벡터를 2번 순회 | 1번 필터링 + 합계 |
| 가변 변수 | mut 2개 필요 | 없음 |
| 의도 표현 | 루프 내부를 읽어야 파악 | 체인만 보면 바로 파악 |
리팩토링 체크리스트
코드를 다시 볼 때 이 목록을 확인해보세요.
- [ ]
clone()이 있나? → 빌림(&)으로 대체할 수 있는지 확인 - [ ]
unwrap()이 있나? →?또는match/unwrap_or()로 대체 - [ ]
for루프에서push()로 벡터를 쌓고 있나? →map().collect()를 고려 - [ ]
for루프에서 조건문으로 걸러내고 있나? →filter()를 고려 - [ ]
for루프에서 누적값을 계산하나? →fold()또는sum()을 고려 - [ ] 가변 변수(
let mut)가 많나? → 이터레이터 체인으로 줄일 수 있는지 확인
흔한 실수
1. 모든 clone()을 없앨 필요는 없어요
// 이 경우 clone()이 적절해요
let config = load_config();
let backup = config.clone(); // 백업용 복사본이 필요한 상황
modify_config(config);
값을 실제로 복사해야 하는 상황이라면 clone()이 맞아요. "불필요한" clone()만 제거하면 돼요.
2. 이터레이터 체인이 항상 더 좋은 건 아니에요
// 너무 긴 체인은 오히려 읽기 어려워요
let result = data.iter()
.filter(|x| x.is_valid())
.map(|x| x.transform())
.flat_map(|x| x.children())
.filter(|c| c.score > 0)
.map(|c| c.name.clone())
.take(5)
.collect::<Vec<_>>();
// 중간 단계를 변수로 분리하는 게 나을 수 있어요
let valid_items: Vec<_> = data.iter().filter(|x| x.is_valid()).collect();
let result: Vec<_> = valid_items.iter()
.flat_map(|x| x.children())
.filter(|c| c.score > 0)
.map(|c| c.name.clone())
.take(5)
.collect();
가독성이 떨어지면 체인을 나누세요!
연습 1. 아래 코드의 clone()과 unwrap()을 제거하세요.
fn greet(name: String) {
println!("안녕하세요, {name}님!");
}
fn main() {
let name = String::from("민수");
greet(name.clone());
greet(name.clone());
println!("이름: {name}");
}
힌트
greet 함수가 소유권을 가져갈 필요가 없어요. &str로 바꿔보세요.
연습 2. 아래 for 루프를 이터레이터 체인으로 리팩토링하세요.
fn main() {
let words = vec!["hello", "hi", "rust", "programming", "go", "iterator"];
let mut long_words = Vec::new();
for word in &words {
if word.len() >= 4 {
long_words.push(word.to_uppercase());
}
}
println!("{:?}", long_words);
}
연습 3 (도전). 아래 함수를 unwrap()이 없도록 리팩토링하세요. Result를 반환하게 바꿔야 해요.
fn parse_scores(input: &str) -> Vec<i32> {
let mut scores = Vec::new();
for part in input.split(',') {
let score: i32 = part.trim().parse().unwrap();
scores.push(score);
}
scores
}
fn main() {
let data = "90, 85, 77, 92";
let scores = parse_scores(data);
println!("{:?}", scores);
}
힌트
parse_scores의 반환 타입을 Result<Vec<i32>, std::num::ParseIntError>로 바꾸고, ?를 사용하세요.
:::
Q1. clone()을 빌림(&)으로 대체하면 좋은 이유는?
- A) 코드가 짧아져서
- B) 데이터를 복사하지 않아 메모리와 시간을 절약한다
- C) 컴파일 시간이 줄어든다
- D) 모든 타입에서 항상 가능하기 때문에
Q2. unwrap()과 ?의 차이로 올바른 것은?
- A)
unwrap()은 에러를 무시하고,?는 에러를 출력한다 - B)
unwrap()은 에러 시 panic하고,?는 에러를 호출자에게 전파한다 - C)
?가 더 느리다 - D) 차이가 없다
Q3. 아래 for 루프와 동일한 이터레이터 체인은?
let mut result = Vec::new();
for n in &numbers {
if *n > 0 {
result.push(n * 2);
}
}
- A)
numbers.iter().map(|n| n * 2).collect() - B)
numbers.iter().filter(|n| **n > 0).map(|n| n * 2).collect() - C)
numbers.iter().filter(|n| **n > 0).collect() - D)
numbers.into_iter().map(|n| n * 2).filter(|n| *n > 0).collect()