이번 모듈은 연습 중심이에요. 모듈 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`
진단: name이 greeting으로 이동했어요. 이동 후에는 원래 변수를 사용할 수 없어요.
해결 방법 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();
}
에러가 두 가지 있어요. 찾아서 고쳐보세요!
힌트
add_song의self는 소유권을 가져가요. 수정만 하면 되니까 어떤 형태가 맞을까요?self.songs.push()로 수정하려면self가 가변이어야 해요.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로 바꾸고, playlist을 mut으로 선언하면 돼요.
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) 모든 타입에 사용할 수 있어서