Rust 소유권
소유권(ownership)은 Rust를 Rust답게 만드는 핵심 개념이에요. 가비지 컬렉터 없이도, 그리고 개발자가 직접 메모리를 해제하지 않아도 메모리 안전성을 보장하는 방법이 바로 소유권이에요. 처음에는 낯설게 느껴지는 게 당연해요. 누구나 그래요. 하지만 핵심 아이디어는 단순하니까, 천천히 하나씩 쌓아 올려 볼게요.
소유권이 해결하는 문제
섹션 제목: “소유권이 해결하는 문제”프로그램이 실행되면 데이터를 메모리에 저장해요. 그 메모리는 언젠가 정리해야 하고, 그러지 않으면 메모리가 새다가(메모리 누수) 결국 프로그램이 멈춰 버려요. 언어들은 보통 이 문제를 두 가지 방법으로 다뤄요.
- 수동 관리(C/C++): 개발자가 직접 메모리를 해제해요. 깜빡하면 메모리가 새고, 너무 일찍 해제하면 프로그램이 죽거나 보안 버그가 생겨요.
- 가비지 컬렉터(Java, Go, Python): 뒤에서 돌아가는 프로세스가 대신 정리해 줘요. 편하지만 성능과 예측 가능성을 어느 정도 포기해야 해요.
Rust는 세 번째 길을 택했어요. 소유권에 관한 몇 가지 규칙을 바탕으로, 각 값을 정확히 언제 정리할지 컴파일할 때 결정해요. 직접 해제할 필요도 없고, 가비지 컬렉터도 없어요.
소유권의 세 가지 규칙
섹션 제목: “소유권의 세 가지 규칙”소유권 시스템은 이 세 가지가 전부예요. 나머지는 모두 여기서 나와요.
- 각 값에는 소유자(owner)가 하나 있어요. 값을 “소유하는” 변수예요.
- 소유자는 한 번에 하나만 있을 수 있어요.
- 소유자가 스코프를 벗어나면 값은 자동으로 정리돼요.
규칙 하나하나가 실제로 어떻게 동작하는지 볼게요.
규칙 3: 스코프가 끝나면 값이 해제돼요
섹션 제목: “규칙 3: 스코프가 끝나면 값이 해제돼요”스코프(scope)는 {와 } 사이의 영역이에요. 변수의 스코프가 끝나면 Rust가 그 값을 자동으로 해제해요. free() 같은 걸 직접 쓸 일이 없어요.
fn main() { { let message = String::from("안녕하세요"); // 여기서 message는 유효해요 println!("{message}"); } // <- 스코프가 끝나고, `message`는 자동으로 정리돼요
// println!("{message}"); // ❌ 에러: 여기서는 message가 더 이상 존재하지 않아요}아무것도 해제하지 않았는데도, message가 스코프를 벗어나는 순간 Rust가 알아서 정리해 줬어요.
규칙 1과 2: “이동”
섹션 제목: “규칙 1과 2: “이동””이제부터가 재미있는 부분이에요. 변수 하나를 다른 변수에 대입하면 어떤 일이 일어나는지 보세요.
fn main() { let s1 = String::from("안녕하세요"); let s2 = s1; // 소유권이 s1에서 s2로 이동해요
println!("{s2}"); // ✅ 잘 동작해요 // println!("{s1}"); // ❌ 에러: s1은 이미 이동됐어요}let s2 = s1이라고 쓰면 Rust는 문자열을 복사하지 않아요. 소유권을 s1에서 s2로 이동(move)시켜요. 규칙 2에 따라 소유자는 하나뿐이어야 하니까, 이제 s1은 쓸 수 없어요. 그래도 쓰려고 하면 이런 에러가 나요.
error[E0382]: borrow of moved value: `s1`“이미 이동된 값 s1을 대여하려고 한다”는 뜻이에요. 컴파일러 메시지는 영어로 나오지만, 무엇이 언제 이동했는지 정확하게 알려 줘요.
복사본이 두 개 필요하다면?
섹션 제목: “복사본이 두 개 필요하다면?”정말로 독립된 복사본이 하나 더 필요하다면 .clone()을 호출하세요.
fn main() { let s1 = String::from("안녕하세요"); let s2 = s1.clone(); // 완전한 복사본을 만들어요
println!("s1 = {s1}, s2 = {s2}"); // ✅ 이제 둘 다 쓸 수 있어요}clone()을 굳이 직접 쓰게 만든 데는 이유가 있어요. 복사는 비용이 클 수 있으니까, Rust는 여러분이 명시적으로 요청하도록 해요.
단순한 타입은 이동하지 않고 복사돼요
섹션 제목: “단순한 타입은 이동하지 않고 복사돼요”그런데 이 코드는 왜 에러가 안 나는지 궁금할 수 있어요.
fn main() { let x = 5; let y = x; println!("x = {x}, y = {y}"); // ✅ 둘 다 쓸 수 있어요}정수, 부동소수점 수, 불리언, char처럼 크기가 작고 고정된 값은 복사하는 비용이 아주 적어서, Rust는 이동 대신 그냥 복사해요. 처음 배울 때는 이렇게 기억하면 돼요.
- 단순한 값(숫자,
bool,char) → 복사돼요. 원래 변수도 그대로 쓸 수 있어요. - 데이터를 소유하는 값(
String등) → 이동해요. 원래 변수는 더 이상 쓸 수 없어요.
소유권과 함수
섹션 제목: “소유권과 함수”값을 함수에 넘겨도 이동이 일어나요. 변수에 대입할 때와 똑같아요.
fn take_ownership(s: String) { println!("이제 내 거예요: {s}");} // 여기서 s가 해제돼요
fn main() { let text = String::from("안녕하세요"); take_ownership(text); // println!("{text}"); // ❌ 에러: text는 함수 안으로 이동했어요}바로 이 문제 때문에 다음 주제인 대여(borrowing)가 필요해요. 대여를 쓰면 함수가 소유권을 가져가지 않고도 값을 사용할 수 있어서, 호출한 뒤에도 값을 계속 쓸 수 있어요.
연습 문제
섹션 제목: “연습 문제”먼저 Rust Playground나 내 컴퓨터의 프로젝트에서 직접 풀어 보세요. 중간에 컴파일 에러가 나는 건 자연스러운 일이에요. 에러 메시지를 읽어 보세요. 대부분 이동이 일어난 곳을 정확히 짚어 줘요.
1. 원본도 지키기
섹션 제목: “1. 원본도 지키기”이 프로그램은 컴파일되지 않아요. 두 변수 중 어느 것도 지우지 말고, 초안 -> 초안 v2가 출력되도록 고쳐 보세요.
fn main() { let original = String::from("초안"); let mut edited = original; edited.push_str(" v2"); println!("{original} -> {edited}");}error[E0382]: borrow of moved value: `original` --> src/main.rs:5:16 |2 | let original = String::from("초안"); | -------- move occurs because `original` has type `String`, which does not implement the `Copy` trait3 | let mut edited = original; | -------- value moved here4 | edited.push_str(" v2");5 | println!("{original} -> {edited}"); | ^^^^^^^^ value borrowed here after moveoriginal은 Copy 트레이트를 구현하지 않은 String이라서 3번째 줄에서 이동했고, 5번째 줄에서 이동한 뒤에 다시 쓰였다는 뜻이에요.
힌트: 하나를 바꿔도 다른 하나에는 영향이 없는, 서로 독립된 문자열 두 개가 필요해요.
정답 보기
fn main() { let original = String::from("초안"); let mut edited = original.clone(); // 완전히 별개인 복사본 edited.push_str(" v2"); println!("{original} -> {edited}");}실행 결과:
초안 -> 초안 v2let mut edited = original;은 문자열을 이동시키기 때문에 original은 더 이상 유효하지 않아요. .clone()은 자기만의 메모리를 가진 두 번째 문자열을 만들어요. 이제 push_str은 edited만 바꾸고, original은 여전히 초안이에요.
2. 돌려주기
섹션 제목: “2. 돌려주기”String의 소유권을 가져와서 끝에 " -- Ferris"를 붙인 다음, 그 문자열을 돌려주는 함수 add_signature를 작성해 보세요. main에서 호출하고 결과를 출력하세요.
힌트: 함수는 전달받은 값을 그대로 반환할 수 있어요. 시그니처에 -> String을 쓰고, 값을 바꿀 수 있도록 매개변수를 mut text: String으로 받으세요.
정답 보기
fn add_signature(mut text: String) -> String { text.push_str(" -- Ferris"); text // 소유권이 호출한 쪽으로 다시 이동해요}
fn main() { let letter = String::from("곧 만나요!"); let letter = add_signature(letter); println!("{letter}");}실행 결과:
곧 만나요! -- Ferris소유권은 함수를 호출할 때 함수 안으로 이동하고, 반환값과 함께 밖으로 다시 이동해요. 그걸 새 변수 letter로 받았어요(이전 letter를 섀도잉해요). 이렇게 해도 동작은 하지만, 값을 매번 넘겼다가 돌려받는 건 꽤 번거로워요. 바로 이 문제를 대여가 해결해 줘요.
3. 지금 주인은 누구일까요?
섹션 제목: “3. 지금 주인은 누구일까요?”이 프로그램은 컴파일되지 않아요. Ana님이 목록에 들어왔어요를 출력하고, 이어서 ["Ana"]를 출력하도록 고쳐 보세요.
fn main() { let mut names = Vec::new(); let name = String::from("Ana"); names.push(name); println!("{name}님이 목록에 들어왔어요"); println!("{:?}", names);}error[E0382]: borrow of moved value: `name` --> src/main.rs:5:16 |3 | let name = String::from("Ana"); | ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait4 | names.push(name); | ---- value moved here5 | println!("{name}님이 목록에 들어왔어요"); | ^^^^ value borrowed here after move힌트: push도 함수 호출처럼 소유권을 가져가요. 4번째 줄이 끝난 뒤 문자열의 주인은 누구일까요? 이동하기 전에 name을 쓰거나, 새 주인에게 물어보세요.
정답 보기
fn main() { let mut names = Vec::new(); let name = String::from("Ana"); println!("{name}님이 목록에 들어왔어요"); // 아직 소유하고 있을 때 써요 names.push(name); // 이제 벡터가 주인이에요 println!("{:?}", names);}실행 결과:
Ana님이 목록에 들어왔어요["Ana"]names.push(name) 다음부터는 벡터가 주인이라서 name을 더 이상 쓸 수 없어요. 먼저 출력하는 게 한 가지 방법이에요. 원래 순서를 그대로 두고 names[0]을 출력하는 방법도 있어요. 벡터가 아직 그 문자열을 갖고 있으니까요. 복제해도 동작하지만, 여기서는 굳이 복사할 필요가 없어요.
- 소유권 덕분에 Rust는 가비지 컬렉터 없이도 메모리를 안전하게 관리해요.
- 세 가지 규칙: 값마다 소유자는 하나, 한 번에 소유자도 하나, 소유자가 스코프를 벗어나면 값이 해제돼요.
- 데이터를 소유하는 값(
String등)을 대입하면 이동해요. 원래 변수는 더 이상 쓸 수 없어요. - 독립된 복사본이 필요하면
.clone()을 쓰세요. 단순한 타입(숫자,bool,char)은 자동으로 복사돼요. - 값을 함수에 넘겨도 이동이 일어나요. 그래서 다음 주제인 대여가 필요해요.
이해도 점검
섹션 제목: “이해도 점검”이해도 점검
1. Rust에서 하나의 값은 한 번에 몇 개의 소유자를 가질 수 있나요?
2. String을 이동시키지 않고 독립된 복사본을 만들려면 어떤 메서드를 호출하나요?
3. 다른 변수에 대입할 때 이동하지 않고 자동으로 복사되는 타입은 무엇인가요?
4. String을 값으로(참조자가 아니라) 받는 함수에 넘기면 어떻게 되나요?
5. 이 페이지에 따르면 Rust는 언제 값을 자동으로 정리(drop)하나요?
점수: 0 / 5
더 알아보기
섹션 제목: “더 알아보기”- 소유권이 뭔가요? — The Rust Programming Language 한국어판: 공식 책의 소유권 장이에요.
String이 메모리에서 어떻게 생겼는지, 이동할 때 왜 복사가 일어나지 않는지 그림으로 보여 줘요. - Ownership and moves — Rust by Example (영어): 함수 안으로 이동하는 경우를 포함해, 이동을 보여 주는 짧은 실행 예제들이에요.
- RAII — Rust by Example (영어): 값이 스코프를 벗어날 때 어떻게 정리되는지,
Drop트레이트가 어떤 역할을 하는지 설명해요. Copy— 표준 라이브러리 문서 (영어): 어떤 타입이 이동 대신 복사되는지,Copy와Clone이 어떻게 다른지 알려 줘요.- Rust 트랙 — Exercism (영어): 이 페이지보다 더 많은 연습 문제가 필요할 때, 피드백과 함께 무료로 풀어 볼 수 있어요.