IT/IT 잡학다식

C++의 망령에서 벗어나게 해줄 구원자? Rust

오덕왕 2026. 2. 14. 15:40
728x90

지식 공유 플랫폼인 지후(Zhihu) 같은 곳에서도 "이미 좋은 언어가 많은데 왜 굳이 Rust를 또 배워야 하느냐"는 질문과 함께 뜨거운 설전이 벌어지곤 하죠. 저도 처음에는 "또 새로운 언어 공부해야 해?"라며 투덜댔지만, 직접 써보니 이건 단순한 유행이 아니라 소프트웨어 공학의 정수를 모아놓은 결정체라는 확신이 들었습니다. 오늘은 제가 Rust를 파헤치며 느꼈던 매력과 삽질의 기록을 담아 깊이 있게 다뤄보겠습니다.

1. Rust, 왜 다들 그렇게 열광할까요?

기존의 C++은 강력하지만 메모리 관리라는 거대한 짐을 개발자에게 지웁니다. 반면 Java나 Python은 가비지 컬렉터(GC) 덕분에 편하지만 성능 손실이 발생하죠. Rust는 이 두 마리 토끼를 다 잡으려고 나온 언어입니다.

리서치 자료에서도 언급되었듯이, Rust는 C++과의 호환성이라는 과거의 굴레를 과감히 벗어던졌습니다. 덕분에 현대적인 컴파일러 이론과 설계 기법을 아낌없이 쏟아부을 수 있었죠. 제가 직접 경험해 보니, Rust는 단순히 코드를 짜는 도구가 아니라 "안전하게 코딩하는 법"을 강제로 가르쳐 주는 엄격한 스승님 같더라고요.

2. Rust의 핵심 영혼: 소유권(Ownership)과 빌림(Borrowing)

Rust를 이해하려면 이 개념을 반드시 넘어야 합니다. 저도 여기서 며칠 동안 컴파일러와 싸우며 고생 좀 했거든요.

  • 소유권(Ownership): 데이터의 주인은 오직 하나입니다. 주인이 범위를 벗어나면 데이터는 즉시 메모리에서 해제됩니다.
  • 빌림(Borrowing): 주인에게 데이터를 빌려올 수 있습니다. 단, 읽기 전용으로 여러 명이 빌리거나, 쓰기 전용으로 딱 한 명만 빌릴 수 있다는 규칙이 있습니다.

이 규칙 덕분에 Rust는 실행 중에 메모리를 감시하는 GC 없이도 메모리 안전성을 완벽하게 보장합니다.

3. 실전 코드로 보는 Rust의 철학

자, 백문이 불여일견이죠. 간단한 문자열 처리 예제를 통해 Rust가 어떻게 에러를 방지하는지 살펴봅시다. 아래 코드는 제가 리팩토링을 거치며 방어 로직까지 추가한 버전입니다.

[초기 코드: 흔히 발생하는 실수]

fn main() {
    let s1 = String::from("Hello Rust");
    process_string(s1); 
    // println!("{}", s1); // 에러 발생! s1의 소유권이 process_string으로 이동했기 때문입니다.
}

fn process_string(s: String) {
    println!("Processing: {}", s);
}

[리팩토링 및 방어 로직 적용 코드]

초보자분들이 가장 많이 겪는 소유권 이동 문제를 해결하기 위해 참조(&)를 사용하고, 데이터가 비어있을 경우를 대비한 방어 로직을 추가해 보겠습니다.

/// 문자열의 첫 단어를 안전하게 반환하는 함수
/// 빈 문자열이나 공백만 있는 경우를 대비해 Option 타입을 반환합니다.
fn get_first_word(s: &str) -> Option<&str> {
    // 방어 로직: 문자열이 비어있는지 먼저 확인
    if s.trim().is_empty() {
        return None;
    }

    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return Some(&s[0..i]);
        }
    }

    Some(s)
}

fn main() {
    let my_string = String::from("Rust_is_awesome amazing");

    // 1. 읽기 전용 참조를 전달하여 소유권을 유지함
    match get_first_word(&my_string) {
        Some(word) => println!("찾은 단어: {}", word),
        None => println!("처리할 수 있는 단어가 없더라고요."),
    }

    // 2. 소유권이 유지되었으므로 여기서도 my_string을 다시 사용할 수 있습니다.
    println!("원본 데이터 유지 확인: {}", my_string);

    // 3. 방어 로직 테스트: 빈 문자열 케이스
    let empty_str = "";
    if let None = get_first_word(empty_str) {
        println!("빈 문자열을 넣었더니 예상대로 None이 반환되었네요. 안전합니다.");
    }
}

4. 코드 리뷰 및 심화 팁

위 코드에서 주목할 점은 Option 타입의 사용입니다. Rust에는 null이 없습니다. 대신 값이 있을 수도 있고 없을 수도 있는 상황을 Option<T>로 강제하죠.

  • 코드 리뷰: 위 예제에서는 &str 타입을 매개변수로 받아 메모리 복사 없이 효율적으로 처리했습니다. String 대신 &str을 쓰면 리터럴 문자열과 String 객체 모두를 유연하게 받을 수 있어 범용성이 높아집니다.
  • 방어 로직의 중요성: s[0..i] 같은 슬라이싱은 잘못하면 런타임에 패닉(Panic)을 일으킬 수 있습니다. 하지만 Rust의 반복자와 인덱스 체킹을 활용하면 이런 위험을 컴파일 타임에 상당 부분 걸러낼 수 있죠.
  • 에러 핸들링: 실제 현업 로직에서는 Result<T, E>를 사용하여 에러 발생 원인을 구체적으로 전달하는 것이 좋습니다.

5. 결론: Rust는 "인생 언어"가 될 수 있을까?

어떤 분들은 Rust의 학습 곡선이 너무 가파르다고 말합니다. 저도 동의해요. 하지만 그 고비를 넘기면 "컴파일만 되면 실행 중에는 뻗지 않는다"는 강력한 믿음을 얻게 됩니다.

지후의 답변 중 "미래 언어는 없고, 당신의 인생 버전에 맞는 언어만 있을 뿐이다"라는 말이 참 인상 깊더라고요. 만약 여러분이 시스템의 성능을 끝까지 쥐어짜야 하거나, 메모리 버그로 밤을 지새우는 데 지쳤다면 Rust는 분명 최고의 선택지가 될 겁니다.

처음에는 컴파일러가 계속 잔소리하는 것 같아 짜증 날 수도 있지만, 그게 다 우리 코드를 지켜주려는 애정 어린 조언이라는 걸 잊지 마세요.

728x90