All posts

Rust 초급 4편: String으로 이해하는 소유권과 이동

String을 다른 변수에 대입한 뒤 원래 변수를 출력했는데 컴파일 오류가 날 수 있습니다. Rust에서 이 동작은 값을 누가 관리하는지 바꾸는 이동일 수 있기 때문입니다. 이번에는 일부러 오류를 만들고, 소유권이 이동하는 지점을 찾아보겠습니다.

하나의 상자 열쇠를 건네는 두 게로 소유권 이동을 표현한 일러스트

String을 다른 변수에 대입한 뒤 원래 변수를 출력했는데 컴파일 오류가 날 수 있습니다. Rust에서 이 동작은 값을 누가 관리하는지 바꾸는 이동일 수 있기 때문입니다. 이번에는 일부러 오류를 만들고, 소유권이 이동하는 지점을 찾아보겠습니다.

준비와 목표

3편까지의 함수와 변수를 사용합니다. 새 프로젝트를 만든 뒤 각 예제를 src/main.rs 전체로 넣고 cargo check 또는 cargo run으로 확인하세요.

cargo new study_ownership
cd study_ownership

이번 목표는 String의 소유자가 바뀌는 순간을 설명하고, 이동·복제·숫자의 복사를 구분하는 것입니다.

먼저 실패하는 코드 읽기

아래는 의도적으로 컴파일되지 않는 예제입니다.

fn main() {
    let title = String::from("소유권 공부");
    let moved_title = title;
    println!("{moved_title}");
    println!("{title}");
}

cargo check를 실행하면 이동한 값을 다시 빌리려 했다는 취지의 E0382 오류가 나옵니다. 마지막 줄에서 사용하려는 title은 이미 유효한 소유자가 아닙니다.

let movedtitle = title은 이 String에서 새 문자열 버퍼를 하나 더 만들지 않습니다. 문자열의 관리를 movedtitle로 넘깁니다. 같은 버퍼를 두 소유자가 각각 해제하는 상황을 막기 위해 이전 바인딩을 사용할 수 없게 하는 것입니다.

String은 힙에 문자 데이터를 보관합니다. 소유권 규칙은 런타임의 가비지 컬렉터가 아니라 주로 컴파일 단계에서 검사됩니다. 모든 대입이 이동하는 것은 아니며 타입에 따라 복사가 일어나기도 합니다. 소유권 공식 설명

실행되는 예제: 함수로 보내고 돌려받기

이번에는 전체 코드를 아래로 바꿉니다.

fn finish(title: String) -> String {
    println!("학습 중: {title}");
    title
}

fn main() {
    let title = String::from("소유권 공부");
    let title = finish(title);
    println!("다시 받은 제목: {title}");

    let backup = title.clone();
    println!("원본: {title}");
    println!("복제: {backup}");

    let minutes: u32 = 30;
    let copied = minutes;
    println!("시간: {minutes}, 복사한 시간: {copied}");
}
학습 중: 소유권 공부
다시 받은 제목: 소유권 공부
원본: 소유권 공부
복제: 소유권 공부
시간: 30, 복사한 시간: 30

finish(title) 호출에서 String이 함수의 매개변수로 이동합니다. 함수는 마지막 title 식으로 그 값을 반환하고, 호출자는 반환된 값을 새로운 title에 바인딩합니다. 이렇게 소유권을 돌려받아 다음 출력에서 사용할 수 있습니다.

두 title 이름이 같아도 코드는 단계가 나뉩니다. 먼저 이전 title을 함수에 보내고, 반환된 값에 같은 이름을 새로 붙인 것입니다. 이름 자체보다 값이 어느 경로로 전달되는지 보세요.

clone은 필요할 때 명시적으로

title.clone()은 별도로 소유할 수 있는 문자열을 복제합니다. 원본과 복제본을 둘 다 유지해야 할 때 사용할 수 있습니다. 그만큼 데이터 복사와 메모리 비용이 생기므로 이동 오류가 날 때마다 clone을 붙이는 습관은 피하는 편이 좋습니다.

반면 u32 같은 정수 타입은 Copy를 구현하므로 대입 후 원래 값도 계속 사용할 수 있습니다. 작은 타입이 모두 Copy인 것도 아니고, 사용자가 만든 타입이 자동으로 Copy가 되는 것도 아닙니다. 이번에는 숫자와 String의 차이를 먼저 기억하세요.

스코프 끝에서 무슨 일이 생기나요?

소유한 값은 소유자의 스코프가 끝나면 정리됩니다. 함수로 보낸 String을 반환하지 않으면 그 함수의 끝에서 정리될 수 있습니다. 중괄호는 값이 유효한 범위를 읽는 중요한 단서입니다.

String 변수의 관리 정보와 실제 문자 버퍼를 구분하면 도움이 됩니다. 이동을 문자열의 모든 바이트가 항상 새 장소로 복사된다는 뜻으로 이해하지 마세요. 이번 예제의 clone과 이동은 비용과 의미가 다릅니다.

오류 해결을 선택하는 순서

원래 실패 예제에서 마지막 println!("{title}")를 지우면 이동 후의 사용이 없어져 컴파일됩니다. 이후 코드는 movedtitle을 사용하면 됩니다. 원본과 복제본이 모두 필요하다면 let movedtitle = title.clone();으로 바꿀 수 있습니다.

함수가 읽기만 하면 되는 상황이라면 소유권을 넘기거나 복제할 필요가 없습니다. 이때 쓰는 참조를 다음 편에서 배웁니다. 먼저 함수가 값을 가져가야 하는지부터 판단하세요.

연습과 해설

finish 함수 안에서 title 뒤에 " 완료"를 붙여 반환해 보세요. main의 나머지 코드는 그대로 둡니다.

해설: 매개변수를 mut title: String으로 바꾸고 println! 다음에 title.push_str(" 완료");를 추가합니다. 마지막 title 반환식은 유지합니다. 출력되는 원본과 복제본은 모두 "소유권 공부 완료"가 됩니다. 변경 후의 String을 반환받고 그다음 복제하기 때문입니다.

이 연습은 함수가 소유한 String을 수정하고 반환하는 흐름을 보여줍니다. 다음 5편에서는 원래 소유자가 값을 계속 가진 채 함수에 잠시 빌려주는 방법을 사용합니다.

실행 확인

이 글의 핵심 실습은 Rust 1.99.1, 2024 Edition의 Linux 환경에서 확인했습니다. 의도적으로 실패하는 예제는 설명한 오류가 발생하는지 구분해 검사했습니다. 운영체제별 오류 문구는 다를 수 있습니다.

Rust 초급 시리즈 전체 목차

  1. 설치와 Cargo로 첫 프로그램 만들기
  2. 변수·자료형·문자열로 학습 기록 만들기
  3. 함수·조건문·반복문으로 학습 기록 계산하기
  4. String으로 이해하는 소유권과 이동
  5. 참조와 빌림으로 문자열 읽고 수정하기
  6. 구조체와 메서드로 학습 할 일 만들기
  7. 열거형과 패턴 매칭으로 학습 상태 표현하기
  8. Option·Result로 빈 값과 오류 처리하기
  9. 벡터·해시맵·문자열로 학습 기록 모으기
  10. 모듈과 외부 크레이트로 코드 정리하기
  11. 테스트와 디버깅으로 목표 계산 검증하기
  12. 파일을 검색하는 CLI 프로젝트 완성하기

시리즈 모아보기

Discussion

No comments yet