프로그래밍 언어 실전 가이드

[Rust입문 #4] Rust 소유권과 참조, 에러 처리 완벽 이해하기

devcomet 2025. 7. 8. 15:50
728x90
반응형

[Rust입문 #4] Rust 소유권과 참조, 에러 처리 완벽 이해하기

 

Rust의 소유권(ownership), 참조(reference), 에러 처리는 메모리 안전성을 보장하는 핵심 개념으로,

이 가이드에서는 실무에서 바로 활용할 수 있는 실전 예제와 함께 완벽하게 마스터할 수 있는 방법을 제공합니다.


Rust 소유권 시스템의 핵심 개념

Rust 소유권 시스템은 가비지 컬렉터 없이도 메모리 안전성을 보장하는 혁신적인 메커니즘입니다.

C++나 Java와 달리, Rust는 컴파일 타임에 메모리 관리를 해결하여 런타임 오버헤드를 최소화합니다.

소유권을 쉽게 이해하는 비유

소유권을 도서관의 책으로 비유해보겠습니다.

도서관에서 책을 빌릴 때, 그 책은 한 번에 한 사람만 가져갈 수 있습니다.
만약 친구가 그 책을 보고 싶다면, 현재 가지고 있는 사람이 먼저 반납해야 합니다.

// 도서관 시스템으로 비유한 소유권
fn main() {
    let book = String::from("Rust Programming");  // 철수가 책을 빌림
    let new_owner = book;                         // 영희에게 책을 넘김

    // println!("{}", book);  // 철수는 더 이상 책을 가지고 있지 않음 (에러!)
    println!("{}", new_owner);  // 영희만 책을 읽을 수 있음
}

 

또 다른 비유: 자동차 열쇠

자동차 열쇠는 한 번에 한 사람만 가질 수 있습니다.
열쇠를 다른 사람에게 넘기면, 이전 소유자는 더 이상 차를 운전할 수 없습니다.

소유권의 3가지 규칙

  1. 각 값은 단 하나의 소유자(owner)만 가질 수 있습니다
    • 마치 자동차 열쇠처럼, 한 번에 한 사람만 소유할 수 있습니다
  2. 한 번에 하나의 소유자만 존재할 수 있습니다
    • 도서관 책처럼, 동시에 여러 사람이 같은 책을 가질 수 없습니다
  3. 소유자가 스코프를 벗어나면 값은 자동으로 해제됩니다
    • 사람이 도서관을 떠날 때 자동으로 책을 반납하는 것과 같습니다
fn main() {
    let s1 = String::from("hello");  // s1이 소유자 (철수가 책을 빌림)
    let s2 = s1;                     // 소유권이 s2로 이동 (영희에게 책을 넘김)

    // println!("{}", s1);           // 컴파일 에러! s1은 더 이상 유효하지 않음
    println!("{}", s2);              // 정상 동작 (영희만 책을 읽을 수 있음)
}

실생활 예시로 보는 소유권의 장점

문제 상황: 여러 사람이 동시에 같은 문서를 수정하려고 할 때

// 기존 언어에서 발생할 수 있는 문제 상황 (의사 코드)
let document = "중요한 문서";
let person_a = document;  // A가 문서를 가져감
let person_b = document;  // B도 같은 문서를 가져감

// 두 사람이 동시에 수정하면 충돌 발생!

 

Rust의 해결책: 소유권 시스템으로 이런 문제를 컴파일 타임에 방지

fn main() {
    let document = String::from("중요한 문서");
    let person_a = document;  // A가 문서의 소유권을 가져감

    // let person_b = document;  // 컴파일 에러! 이미 소유권이 이동됨

    // 이제 person_a만 문서를 수정할 수 있음
    println!("A가 보는 문서: {}", person_a);
}

이런 방식으로 Rust는 "한 번에 하나의 소유자만" 원칙을 통해 데이터 경쟁 상태를 원천 차단합니다.

Move와 Copy 트레이트

Rust에서는 값의 이동과 복사를 명확히 구분합니다.

 

Move 시맨틱스를 가지는 타입들:

  • String
  • Vec<T>
  • 커스텀 구조체

Copy 트레이트를 구현하는 타입들:

  • 정수형 (i32, u64 등)
  • 부동소수점 (f32, f64)
  • 불린 (bool)
  • 문자 (char)
// Copy 트레이트 예제
let x = 5;
let y = x;  // x는 여전히 유효함 (Copy 트레이트)
println!("x: {}, y: {}", x, y);  // 정상 동작

// Move 시맨틱스 예제
let s1 = String::from("hello");
let s2 = s1;  // s1의 소유권이 s2로 이동
// println!("{}", s1);  // 컴파일 에러!

 

Rust ownership transfer diagram showing how variable ownership moves from s1 to s2 with string data
소유권 이동 다이어그램


Rust 참조와 빌림(Borrow) 시스템

참조(Reference)는 소유권을 이전하지 않고 값에 접근할 수 있는 방법입니다.

이를 통해 함수 호출 시 소유권을 유지하면서도 데이터를 활용할 수 있습니다.

불변 참조(Immutable Reference)

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);  // 참조 전달

    println!("The length of '{}' is {}.", s1, len);  // s1은 여전히 유효
}

fn calculate_length(s: &String) -> usize {
    s.len()  // 참조를 통해 길이 계산
}

가변 참조(Mutable Reference)

fn main() {
    let mut s = String::from("hello");
    change(&mut s);  // 가변 참조 전달
    println!("{}", s);  // "hello, world"
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

참조 규칙과 제약사항

Rust의 참조 시스템에는 엄격한 규칙이 있습니다:

참조 타입 동시 허용 개수 특징
불변 참조 (&T) 무제한 읽기 전용
가변 참조 (&mut T) 1개만 읽기/쓰기 가능
fn main() {
    let mut s = String::from("hello");

    // 불변 참조 여러 개 가능
    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);

    // 가변 참조는 하나만 가능
    let r3 = &mut s;
    // let r4 = &mut s;  // 컴파일 에러!
    println!("{}", r3);
}

 

Rust reference borrowing rules diagram showing immutable and mutable reference constraints
참조 규칙 다이어그램


Rust 메모리 관리와 Drop 트레이트

Rust는 RAII(Resource Acquisition Is Initialization) 패턴을 통해 자동 메모리 관리를 구현합니다.

Drop 트레이트 활용

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer {
        data: String::from("my stuff"),
    };
    let d = CustomSmartPointer {
        data: String::from("other stuff"),
    };
    println!("CustomSmartPointers created.");
}  // 스코프를 벗어나면 자동으로 drop 호출

메모리 누수 방지

Rust의 소유권 시스템은 다음과 같은 메모리 관련 문제를 컴파일 타임에 방지합니다:

  • 메모리 누수(Memory Leak)
  • 이중 해제(Double Free)
  • 댕글링 포인터(Dangling Pointer)
  • 버퍼 오버플로우(Buffer Overflow)
fn main() {
    let reference_to_nothing = dangle();  // 컴파일 에러!
}

fn dangle() -> &String {  // 댕글링 포인터 반환 시도
    let s = String::from("hello");
    &s  // s는 스코프를 벗어나면 해제됨
}  // 컴파일러가 에러를 발생시킴

Rust 에러 처리의 두 가지 방법

Rust는 두 가지 주요 에러 처리 방식을 제공합니다.

복구 가능한 에러: Result<T, E>

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt");

    let _f = match f {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: {:?}", e),
            },
            other_error => {
                panic!("Problem opening the file: {:?}", other_error)
            }
        },
    };
}

복구 불가능한 에러: panic!

fn main() {
    let v = vec![1, 2, 3];
    v[99];  // panic! 발생
}

? 연산자를 활용한 에러 전파

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?;  // ? 연산자로 에러 전파
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

 

Rust Result and Option types flowchart diagram showing error handling patterns
Result와 Option 타입 다이어그램


Option 타입으로 null 안전성 보장

Rust는 null 개념이 없는 대신 Option 타입을 제공합니다.

Option 타입의 활용

fn main() {
    let some_number = Some(5);
    let some_string = Some("a string");
    let absent_number: Option<i32> = None;

    // match를 통한 Option 처리
    match some_number {
        Some(value) => println!("Got a value: {}", value),
        None => println!("No value"),
    }

    // if let을 통한 간단한 처리
    if let Some(value) = some_number {
        println!("The value is: {}", value);
    }
}

Option과 Result의 조합 활용

fn find_user_by_id(id: u32) -> Option<String> {
    match id {
        1 => Some("Alice".to_string()),
        2 => Some("Bob".to_string()),
        _ => None,
    }
}

fn get_user_data(id: u32) -> Result<String, String> {
    match find_user_by_id(id) {
        Some(user) => Ok(user),
        None => Err("User not found".to_string()),
    }
}

실무에서 활용하는 고급 패턴

1. Rc와 Arc를 통한 참조 카운팅

use std::rc::Rc;

fn main() {
    let data = Rc::new(String::from("shared data"));
    let data1 = Rc::clone(&data);
    let data2 = Rc::clone(&data);

    println!("Reference count: {}", Rc::strong_count(&data));  // 3
}

2. RefCell를 통한 내부 가변성

use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let data = Rc::new(RefCell::new(String::from("hello")));

    {
        let mut s = data.borrow_mut();
        s.push_str(", world");
    }

    println!("{}", data.borrow());  // "hello, world"
}

3. 커스텀 에러 타입 정의

use std::fmt;

#[derive(Debug)]
enum CustomError {
    InvalidInput,
    NetworkError,
    DatabaseError,
}

impl fmt::Display for CustomError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            CustomError::InvalidInput => write!(f, "Invalid input provided"),
            CustomError::NetworkError => write!(f, "Network connection failed"),
            CustomError::DatabaseError => write!(f, "Database operation failed"),
        }
    }
}

impl std::error::Error for CustomError {}

성능 최적화 팁

1. 불필요한 복사 방지

// 비효율적인 방법
fn process_data(data: String) -> String {
    format!("Processed: {}", data)
}

// 효율적인 방법
fn process_data_efficient(data: &str) -> String {
    format!("Processed: {}", data)
}

2. 제네릭과 트레이트 활용

fn print_info<T: std::fmt::Display>(item: &T) {
    println!("Info: {}", item);
}

3. 벡터 용량 미리 할당

fn create_large_vector() -> Vec<i32> {
    let mut v = Vec::with_capacity(1000);  // 용량 미리 할당
    for i in 0..1000 {
        v.push(i);
    }
    v
}

마무리

Rust의 소유권, 참조, 에러 처리 시스템은 처음에는 복잡해 보일 수 있지만,

이를 마스터하면 메모리 안전성과 성능을 동시에 확보할 수 있습니다.

이 가이드에서 다룬 개념들을 실제 프로젝트에 적용하여 안전하고 효율적인 Rust 코드를 작성해보세요.

다음 글에서는 Rust 구조체, 열거형, 컬렉션 완전 정리에 대해 자세히 알아보겠습니다.

추가적인 학습을 위해서는 공식 Rust 문서를 참고하시기 바랍니다.


강의추천

인프런 - 세상에서 제일 쉬운 러스트 프로그래밍

 

세상에서 제일 쉬운 러스트 프로그래밍 강의 | 윤인도 - 인프런

윤인도 | 이 강의를 통해 여러분은 가장 핫한 언어, Rust를 활용하실 수 있게 됩니다. 파이썬의 단점인 GIL을 극복하고 빠르게 동작하는 코드를 만들 수 있습니다., 최근 3년, 가장 핫한 언어 러스트

www.inflearn.com

 

 

728x90
반응형