DaleSchool

리팩토링 실전: clone()에서 빌림으로, unwrap()에서 ?로

중급20분

학습 목표

  • clone() 호출을 빌림으로 대체할 수 있다
  • unwrap()을 ? 연산자로 대체할 수 있다
  • for 루프를 이터레이터 체인으로 전환할 수 있다
  • 리팩토링 전후의 코드 품질 차이를 설명할 수 있다

이 모듈은 커리큘럼의 전환점이에요. 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()