DaleSchool

소유권 실전 연습

중급15분

학습 목표

  • 소유권 에러가 발생하는 코드를 보고 원인을 진단할 수 있다
  • 이동, 빌림, 복사 중 적절한 방법을 선택할 수 있다
  • 구조체와 열거형에서 소유권 규칙을 적용할 수 있다

이번 모듈은 연습 중심이에요. 모듈 09~13에서 배운 소유권, 빌림, 구조체, 열거형을 종합해서 실전 문제를 풀어볼 거예요.

에러가 있는 코드를 읽고, 원인을 진단하고, 고치는 과정을 반복하면서 감을 잡아보세요!

레벨 1: 단순 이동 에러

문제 1-1

fn main() {
    let name = String::from("민수");
    let greeting = name;
    println!("이름: {name}");
    println!("인사: {greeting}");
}
error[E0382]: borrow of moved value: `name`

진단: namegreeting으로 이동했어요. 이동 후에는 원래 변수를 사용할 수 없어요.

해결 방법 1 — 이동 후에는 새 주인을 사용:

fn main() {
    let name = String::from("민수");
    let greeting = name;
    println!("인사: {greeting}");
}

해결 방법 2 — 복사본을 만들기:

fn main() {
    let name = String::from("민수");
    let greeting = name.clone();
    println!("이름: {name}");
    println!("인사: {greeting}");
}

문제 1-2

아래 코드를 clone() 없이 컴파일되도록 고쳐보세요.

fn print_length(s: String) -> usize {
    println!("문자열: {s}");
    s.len()
}

fn main() {
    let word = String::from("Rust");
    let len = print_length(word);
    println!("{word}의 길이: {len}");
}
힌트

함수가 소유권을 가져가지 않도록 매개변수 타입을 바꿔보세요. &str이나 &String을 사용하면 돼요.

정답
fn print_length(s: &str) -> usize {
    println!("문자열: {s}");
    s.len()
}

fn main() {
    let word = String::from("Rust");
    let len = print_length(&word);
    println!("{word}의 길이: {len}");
}

레벨 2: 함수 매개변수에서의 빌림

문제 2-1

fn add_greeting(names: &Vec<String>) {
    for name in names {
        names.push(format!("안녕, {name}!"));
    }
}

이 코드는 왜 안 될까요?

진단

names&Vec(불변 참조)로 빌렸는데, push()는 벡터를 수정하려 해요. 읽기 권한만 있는데 편집하려는 거예요!

게다가 순회 중에 벡터를 수정하면 메모리가 꼬일 수 있어요. Rust가 이걸 컴파일 시점에 잡아줘요.

문제 2-2

아래 코드를 컴파일되도록 고쳐보세요.

fn longest(s1: String, s2: String) -> String {
    if s1.len() >= s2.len() {
        s1
    } else {
        s2
    }
}

fn main() {
    let a = String::from("hello");
    let b = String::from("hi");
    let result = longest(a, b);
    println!("더 긴 문자열: {result}");
    println!("a = {a}");  // 에러!
}
힌트

longest 함수가 소유권을 가져가지 않도록 빌림을 사용하세요. 반환 타입도 참조로 바꿔야 해요.

정답
fn longest(s1: &str, s2: &str) -> &str {
    if s1.len() >= s2.len() {
        s1
    } else {
        s2
    }
}

fn main() {
    let a = String::from("hello");
    let b = String::from("hi");
    let result = longest(&a, &b);
    println!("더 긴 문자열: {result}");
    println!("a = {a}");  // OK!
}

참고: 이 코드는 수명(lifetime) 표기가 필요할 수 있어요. 다음 모듈에서 배울 내용이에요!

레벨 3: 구조체 + 메서드에서의 소유권

문제 3-1

#[derive(Debug)]
struct Playlist {
    name: String,
    songs: Vec<String>,
}

impl Playlist {
    fn add_song(self, song: String) {
        self.songs.push(song);
    }

    fn show(&self) {
        println!("--- {} ---", self.name);
        for song in &self.songs {
            println!("  {song}");
        }
    }
}

fn main() {
    let playlist = Playlist {
        name: String::from("내 플레이리스트"),
        songs: vec![String::from("노래 1")],
    };

    playlist.add_song(String::from("노래 2"));
    playlist.show();
}

에러가 두 가지 있어요. 찾아서 고쳐보세요!

힌트
  1. add_songself는 소유권을 가져가요. 수정만 하면 되니까 어떤 형태가 맞을까요?
  2. self.songs.push()로 수정하려면 self가 가변이어야 해요.
  3. playlist 변수도 가변이어야 해요.
정답
impl Playlist {
    fn add_song(&mut self, song: String) {
        self.songs.push(song);
    }
    // show는 그대로
}

fn main() {
    let mut playlist = Playlist {
        name: String::from("내 플레이리스트"),
        songs: vec![String::from("노래 1")],
    };

    playlist.add_song(String::from("노래 2"));
    playlist.show();
}

self -> &mut self로 바꾸고, playlistmut으로 선언하면 돼요.

Phase 1 코드 돌아보기

모듈 06에서 이런 코드를 썼어요.

fn print_scores(scores: Vec<i32>) {
    for s in scores {
        println!("{s}");
    }
}

fn main() {
    let scores = vec![90, 85, 77];
    print_scores(scores.clone()); // clone()으로 해결했었죠
    println!("총 {}개의 점수", scores.len());
}

이제 clone() 없이 고칠 수 있어요!

fn print_scores(scores: &[i32]) {
    for s in scores {
        println!("{s}");
    }
}

fn main() {
    let scores = vec![90, 85, 77];
    print_scores(&scores);  // 빌려주기만 해요
    println!("총 {}개의 점수", scores.len());
}

매개변수를 &[i32](슬라이스 참조)로 바꿨어요. 복사 비용 없이 값을 사용할 수 있게 됐어요. clone()이 왜 필요했는지, 그리고 더 좋은 방법이 뭔지 이제 알겠죠?

소유권 판단 치트시트

코드를 쓸 때 이 흐름을 따라가보세요.

값을 넘겨야 한다
  ├─ 읽기만 한다? → &T (불변 참조)
  ├─ 수정도 한다? → &mut T (가변 참조)
  ├─ 소유권이 필요하다? → T (이동)
  └─ 잘 모르겠다? → 일단 & 로 시작, 에러 나면 조정

연습 1 (쉬움). 아래 코드가 컴파일되도록 고쳐보세요.

fn main() {
    let mut items = vec!["사과", "바나나"];
    let first = &items[0];
    items.push("포도");
    println!("첫 번째: {first}");
}

연습 2 (보통). Student 구조체를 완성하세요. add_score는 점수를 추가하고, average는 평균을 반환해요.

#[derive(Debug)]
struct Student {
    name: String,
    scores: Vec<f64>,
}

impl Student {
    fn new(name: &str) -> Student {
        // 여기를 완성하세요
    }

    fn add_score(/* ??? */, score: f64) {
        // 여기를 완성하세요
    }

    fn average(/* ??? */) -> f64 {
        // 여기를 완성하세요
        // 힌트: scores가 비어있으면 0.0 반환
    }
}

fn main() {
    let mut student = Student::new("민수");
    student.add_score(90.0);
    student.add_score(85.0);
    student.add_score(92.0);
    println!("{}: 평균 {:.1}", student.name, student.average());
}

연습 3 (도전). 아래 enum과 함수를 활용해서 간단한 계산기를 만들어보세요.

enum Operation {
    Add(f64, f64),
    Subtract(f64, f64),
    Multiply(f64, f64),
}

fn calculate(op: &Operation) -> f64 {
    // match로 각 연산의 결과를 반환
}

fn describe(op: &Operation) -> String {
    // "3 + 5 = 8" 형태의 문자열 반환
}

Q1. 함수가 값을 읽기만 할 때, 매개변수 타입으로 가장 적절한 것은?

  • A) String
  • B) &str
  • C) mut String
  • D) String::from()

Q2. 아래 코드에서 에러가 나는 이유는?

fn main() {
    let mut v = vec![1, 2, 3];
    let first = &v[0];
    v.push(4);
    println!("{first}");
}
  • A) 벡터에 4개 이상 넣을 수 없다
  • B) 불변 참조(first)가 있는데 벡터를 수정하려 했다
  • C) println!에 참조를 넣을 수 없다
  • D) vec!으로 만든 벡터는 수정할 수 없다

Q3. clone()보다 빌림(&)이 좋은 이유는?

  • A) 코드가 더 짧아서
  • B) 데이터를 복사하지 않아서 메모리와 시간을 절약한다
  • C) 에러가 절대 발생하지 않아서
  • D) 모든 타입에 사용할 수 있어서