[2026] C++ vs Rust 완전 비교 | 소유권·메모리 안전성·에러 처리·동시성·성능 실전 가이드
이 글의 핵심
시스템 프로그래밍·고성능 서버·임베디드 영역에서 C++과 Rust는 모두 제로 코스트 추상화를 내세우는 언어입니다. C++은 40년 이상의 생태계와 레거시가 있고, Rust는 소유권·Borrow checker로 메모리… 개념과 예제 코드를 단계적으로 다루며, 실무·학습에 참고할 수 있도록 구성했습니다.
들어가며: “C++로 갈까, Rust로 갈까” 실무에서 겪는 고민
왜 비교하는가
시스템 프로그래밍·고성능 서버·임베디드 영역에서 C++과 Rust는 모두 “제로 코스트 추상화”를 내세우는 언어입니다. C++은 40년 이상의 생태계와 레거시가 있고, Rust는 소유권·Borrow checker로 메모리 안전성과 Data Race를 컴파일 타임에 보장합니다. 이 글은 실제 겪는 문제 시나리오, 완전한 비교 예제(소유권·메모리·에러 처리·동시성·성능), 자주 하는 실수, 프로덕션 패턴까지 포함해 두 언어를 실전 관점에서 비교합니다. 이 글에서 다루는 것:
- 문제 시나리오: 기술 선택·마이그레이션 시 겪는 실제 상황
- 소유권·메모리 안전성: C++ 이동 vs Rust 소유권·빌림 검사
- 에러 처리: 예외·expected vs Result·Option
- 동시성: 스레드·뮤텍스 vs Send·Sync·채널
- 성능: 벤치마크·트레이드오프
- 자주 하는 실수: C++·Rust 각각에서 피해야 할 패턴
- 프로덕션 패턴: 실전에서 쓰는 설계 패턴 관련 글: Rust vs C++ 메모리 안전성, C++ 스마트 포인터.
개념을 잡는 비유
이 글의 주제는 여러 부품이 맞물리는 시스템으로 보시면 이해가 빠릅니다. 한 레이어(저장·네트워크·관측)의 선택이 옆 레이어에도 영향을 주므로, 본문에서는 트레이드오프를 숫자와 패턴으로 정리합니다.
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
목차
- 문제 시나리오: 기술 선택·마이그레이션 시 겪는 상황
- 소유권·이동 비교
- 메모리 안전성 비교
- 에러 처리 비교
- 동시성 비교
- 성능 비교
- 완전한 C++ vs Rust 비교표
- 자주 하는 실수와 해결법
- 모범 사례·베스트 프랙티스
- 프로덕션 패턴
- 정리 및 선택 가이드
1. 문제 시나리오: 기술 선택·마이그레이션 시 겪는 상황
기술 선택이나 마이그레이션을 잘못하면 아래와 같은 문제가 발생합니다. 다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart TB
subgraph Problems[실무 문제 시나리오]
P1[메모리 버그 런타임 크래시]
P2[에러 처리 누락]
P3[Data Race·동시성 버그]
P4[성능 요구 미충족]
P5[팀 학습 곡선]
end
subgraph Solutions[해결 방향]
S1[C++: Sanitizer·Clang-Tidy]
S2[Rust: 타입 시스템]
S3[요구사항에 맞는 언어 선택]
end
P1 --> S1
P1 --> S2
P2 --> S2
P3 --> S2
P4 --> S3
P5 --> S3
시나리오 1: “C++ 네트워크 서버에서 use-after-free 크래시”
상황: C++로 패킷 처리 서버를 만들었습니다. std::vector<uint8_t> 버퍼를 스레드 풀로 넘긴 뒤, 실수로 원본을 다시 사용했는데 컴파일은 통과했고 런타임에 간헐적 크래시가 발생했습니다.
원인: C++에서 std::move 후 원본은 “유효하지만 unspecified” 상태입니다. 컴파일러가 “이동 후 사용 금지”를 강제하지 않아, 실수로 접근하면 use-after-free가 됩니다.
해결 방향: Rust로 전환하면 이동 후 원본 사용 시 컴파일 에러가 납니다. C++ 유지 시 Clang-Tidy·AddressSanitizer로 검증합니다.
시나리오 2: “Rust 도입 후 borrow checker에 막혀 개발이 느려졌다”
상황: C++ 팀이 Rust로 새 모듈을 작성하기 시작했습니다. 참조·수명 에러가 잦아서 초기 개발 속도가 크게 떨어졌습니다.
원인: Rust의 borrow checker는 C++에 없는 제약을 둡니다. “동시에 하나의 가변 참조만”, “참조 수명이 값보다 길 수 없음” 등. 익숙해지기 전에는 clone()으로 우회하거나 구조 재설계가 필요합니다.
해결 방향: Rc·Arc로 소유권 공유, clone()으로 초기 우회 후 점진적으로 참조·수명 익히기. The Rust Book 4장(Understanding Ownership)부터 체계적으로 학습합니다.
시나리오 3: “에러 처리를 누락해 프로덕션에서 예기치 않은 종료”
상황: C++에서 파일 열기·네트워크 연결 실패를 예외로 처리하지 않았고, 호출자가 try-catch를 쓰지 않아 프로세스가 비정상 종료했습니다.
원인: C++ 예외는 “선택적”입니다. 호출자가 처리하지 않으면 스택 언와인딩 후 종료됩니다. Rust의 Result는 “에러 가능성”을 타입으로 강제해, ? 연산자나 match로 처리하지 않으면 컴파일 에러가 납니다.
해결 방향: C++에서는 std::expected(C++23) 또는 예외 규칙을 팀에 정착. Rust에서는 Result·?를 일관되게 사용합니다.
시나리오 4: “Data Race로 프로덕션에서 간헐적 오류”
상황: C++에서 여러 스레드가 공유 카운터를 mutex 없이 증가시켰습니다. ThreadSanitizer로 확인했을 때만 발견되었고, 그 전까지는 “가끔 잘못된 값”으로만 드러났습니다.
원인: C++에서 Data Race는 undefined behavior입니다. 컴파일러가 막지 않습니다. Rust의 Send·Sync 트레이트는 “스레드 간 전달·공유가 안전한 타입”만 허용해, Data Race 가능 코드는 컴파일이 되지 않습니다.
해결 방향: C++에서는 std::atomic·std::mutex 필수, TSan을 CI에 적용. Rust는 타입 시스템이 이를 강제합니다.
시나리오 5: “성능 요구를 충족하지 못했다”
상황: Rust로 작성한 파서가 C++ 버전보다 느렸습니다. 조사 결과 clone() 과다 사용과 불필요한 Arc 래핑이 원인이었습니다.
원인: Rust도 “제로 코스트”를 지향하지만, clone()·Arc는 런타임 비용이 있습니다. 참조·소유권을 제대로 활용하지 않으면 C++보다 느려질 수 있습니다.
해결 방향: 참조(&T·&mut T)로 빌림, clone() 최소화, 프로파일링으로 병목 확인. C++과 Rust 모두 “올바르게 쓰면” 비슷한 성능을 냅니다.
시나리오 6: “레거시 C++ 라이브러리와 연동이 필요하다”
상황: Rust 프로젝트에서 기존 C++ 라이브러리를 사용해야 합니다. FFI 경계에서 메모리 관리·에러 전달이 복잡했습니다.
원인: C++과 Rust의 메모리 모델이 다릅니다. FFI 경계는 unsafe 블록이 필요하고, 수명·소유권을 수동으로 맞춰야 합니다.
해결 방향: bindgen, cxx 등 도구 활용. 경계를 최소화하고, unsafe 블록을 잘 격리합니다.
2. 소유권·이동 비교
C++: std::move와 “유효하지만 unspecified”
C++에서 이동은 std::move로 명시합니다. 이동 후 원본은 “moved-from” 상태로, 표준은 유효하지만 unspecified만 보장합니다. unique_ptr는 nullptr가 되지만, 일반 타입은 “비어 있음”을 보장하지 않습니다. 재사용하면 미정의 동작이 될 수 있고, 컴파일러는 대부분 막지 않습니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <memory>
#include <iostream>
int main() {
auto p = std::make_unique<int>(42);
auto q = std::move(p);
// *p = 1; // UB: p는 nullptr, 컴파일은 통과 가능
std::cout << *q << "\n"; // 42
return 0;
}
코드 설명:
std::move(p):p의 소유권을q로 이동합니다. 이동 후p는 nullptr입니다.*p = 1: nullptr 역참조로 미정의 동작입니다. 컴파일러는 이를 막지 않습니다.- 주의: 이동 후
p를 사용하지 않는 것이 개발자 책임입니다.
Rust: 이동이 기본, 사용 불가 강제
Rust에서는 이동이 기본입니다. 값이 이동하면 원래 변수는 사용 불가가 됩니다. 컴파일러가 “이미 이동된 값”을 쓰려 하면 컴파일 에러입니다. 아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
fn main() {
let s = String::from("hello");
let t = s;
// println!("{}", s); // 컴파일 에러: use of moved value: `s`
println!("{}", t); // OK
}
코드 설명:
let t = s;:s의 소유권이t로 이동합니다.s는 더 이상 유효하지 않습니다.println!("{}", s);: 컴파일 에러 — “use of moved value:s”- Rust는 이동 후 사용을 문법적으로 금지합니다.
완전한 비교 예제: 벡터 반환
C++: 아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <vector>
std::vector<int> create_vector() {
std::vector<int> v = {1, 2, 3, 4, 5};
return v; // RVO 또는 이동
}
int main() {
auto data = create_vector();
// data 사용
return 0;
}
Rust: 아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 함수 정의 및 구현
fn create_vector() -> Vec<i32> {
let v = vec![1, 2, 3, 4, 5];
v // 소유권 반환 (이동)
}
fn main() {
let data = create_vector();
// data 사용
}
비교: 두 언어 모두 반환 시 이동이 적용됩니다. C++은 RVO로 복사/이동을 생략할 수 있고, Rust는 항상 소유권이 이동합니다. C++에서 이동 후 v를 실수로 쓰면 UB, Rust에서는 컴파일 에러입니다.
소유권 비교 다이어그램
아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart TB
subgraph cpp[C++ 이동]
C1[unique_ptr p] -->|std::move| C2[unique_ptr q]
C2 --> C3[p = nullptr]
C3 --> C4["*p 사용 시도 가능br/(컴파일 통과)"]
C4 --> C5[런타임 크래시·UB]
end
subgraph rust[Rust 이동]
R1[String s] -->|let t = s| R2[String t]
R2 --> R3[s 사용 불가]
R3 --> R4["s 사용 시도"]
R4 --> R5[컴파일 에러]
end
3. 메모리 안전성 비교
댕글링 포인터·use-after-free
C++: 로컬 객체의 포인터를 반환하면 댕글링이 됩니다. 컴파일러가 반드시 막는 것은 아닙니다. 다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
const char* get_name() {
std::string s = "hello";
return s.c_str(); // s 소멸 후 댕글링, 컴파일 통과 가능
}
Rust: 수명(lifetime)으로 “참조가 가리키는 값보다 참조가 더 오래 살 수 없다”를 검사합니다. 아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// fn get_name() -> &str {
// let s = String::from("hello");
// &s // 컴파일 에러: borrowed value does not live long enough
// }
fn get_name() -> String {
String::from("hello") // 소유권 반환
}
이터레이터 무효화
C++: 반복 중 push_back·erase 등으로 컨테이너가 변경되면 이터레이터가 무효화됩니다. 컴파일러는 막지 않습니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
std::vector<int> v = {1, 2, 3};
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it == 2) {
v.push_back(4); // UB: it 무효화
}
}
Rust: 반복 중 Vec를 가변으로 빌리면 컴파일 에러입니다.
아래 코드는 rust를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
let mut v = vec![1, 2, 3];
for x in &v {
if *x == 2 {
// v.push(4); // 컴파일 에러: cannot borrow `v` as mutable
}
}
null 안전성
C++: raw 포인터는 null일 수 있고, 역참조 시 UB입니다.
int* p = nullptr;
// int x = *p; // UB
Rust: Option<T>로 “값이 있거나 없음”을 타입으로 표현합니다.
아래 코드는 rust를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
let maybe: Option<i32> = Some(42);
match maybe {
Some(x) => println!("{}", x),
None => println!("없음"),
}
// maybe.unwrap(); // None이면 패닉
메모리 안전성 요약표
| 문제 | C++ | Rust |
|---|---|---|
| 이동 후 사용 | 통과 가능, UB | 컴파일 에러 |
| 댕글링 참조 | 통과 가능, UB | 수명으로 컴파일 에러 |
| 이터레이터 무효화 | 런타임 UB | borrow checker로 컴파일 에러 |
| null 역참조 | 가능, UB | Option으로 검사 |
| 이중 해제 | 수동 관리 시 가능 | 소유권으로 불가능 |
일상 비유로 이해하기: 메모리를 아파트 건물로 생각해보세요. 스택은 엘리베이터 같아서 빠르지만 공간이 제한적입니다. 힙은 창고처럼 넓지만 물건을 찾는 데 시간이 걸립니다. 포인터는 “3층 302호”처럼 주소를 가리키는 메모지라고 보면 됩니다.
4. 에러 처리 비교
C++: 예외와 std::expected
예외: 전통적인 방식. 호출자가 처리하지 않으면 스택 언와인딩 후 종료됩니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <stdexcept>
#include <string>
int parse_int(const std::string& s) {
if (s.empty()) {
throw std::invalid_argument("empty string");
}
return std::stoi(s);
}
int main() {
try {
int x = parse_int("42");
} catch (const std::exception& e) {
// 에러 처리
}
return 0;
}
std::expected (C++23): 예외 없이 에러를 값으로 반환합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <expected>
#include <string>
std::expected<int, std::string> parse_int(const std::string& s) {
if (s.empty()) {
return std::unexpected("empty string");
}
try {
return std::stoi(s);
} catch (...) {
return std::unexpected("parse error");
}
}
int main() {
auto result = parse_int("42");
if (result) {
int x = *result;
} else {
// result.error() 처리
}
return 0;
}
Rust: Result와 Option
Result: 에러 가능성을 타입으로 강제합니다. ? 연산자로 전파할 수 있습니다.
아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
use std::num::ParseIntError;
fn parse_int(s: &str) -> Result<i32, ParseIntError> {
s.parse()
}
fn main() -> Result<(), ParseIntError> {
let x = parse_int("42")?;
println!("{}", x);
Ok(())
}
Option: 값이 없을 수 있는 경우입니다. 아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
fn find_first(v: &[i32], target: i32) -> Option<usize> {
v.iter().position(|&x| x == target)
}
fn main() {
let v = vec![1, 2, 3];
match find_first(&v, 2) {
Some(i) => println!("인덱스: {}", i),
None => println!("없음"),
}
}
에러 처리 비교표
| 항목 | C++ | Rust |
|---|---|---|
| 예외 | 지원, 선택적 사용 | 패닉(복구 불가)만 |
| 에러 값 반환 | std::expected (C++23) | Result<T, E> |
| null 대신 | optional (C++17) | Option |
| 에러 전파 | try-catch | ? 연산자 |
| 컴파일 시 검사 | 없음 | Result 미처리 시 에러 |
5. 동시성
일상 비유로 이해하기: 동시성은 주방에서 여러 요리를 동시에 하는 것과 비슷합니다. 한 명의 요리사(싱글 스레드)가 국을 끓이다가 불을 줄이고, 그 사이에 야채를 썰고, 다시 국을 확인하는 식이죠. 반면 병렬성은 요리사 여러 명(멀티 스레드)이 각자 다른 요리를 동시에 만드는 겁니다. 비교
C++: 스레드·뮤텍스·atomic
Data Race: C++에서 mutex 없이 공유 변수를 수정하면 UB입니다. 컴파일러가 막지 않습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <thread>
#include <atomic>
int main() {
std::atomic<int> counter{0};
std::thread t1([&] { for (int i = 0; i < 1000000; ++i) ++counter; });
std::thread t2([&] { for (int i = 0; i < 1000000; ++i) ++counter; });
t1.join();
t2.join();
// counter는 2000000
return 0;
}
뮤텍스: 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <thread>
#include <mutex>
int main() {
std::mutex mtx;
int counter = 0;
std::thread t1([&] {
for (int i = 0; i < 1000000; ++i) {
std::lock_guard g(mtx);
++counter;
}
});
std::thread t2([&] {
for (int i = 0; i < 1000000; ++i) {
std::lock_guard g(mtx);
++counter;
}
});
t1.join();
t2.join();
return 0;
}
Rust: Send·Sync·Arc·Mutex
Send·Sync: 스레드 간 전달·공유가 안전한 타입만 허용합니다. Rc는 Send가 아니어서 스레드로 넘기면 컴파일 에러입니다.
다음은 rust를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 실행 예제
use std::thread;
use std::sync::{Arc, Mutex};
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..2 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
for _ in 0..1_000_000 {
*counter.lock().unwrap() += 1;
}
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("{}", *counter.lock().unwrap()); // 2000000
}
채널: 아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
use std::thread;
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
tx.send(42).unwrap();
});
let received = rx.recv().unwrap();
println!("{}", received);
}
동시성 비교표
| 항목 | C++ | Rust |
|---|---|---|
| Data Race | UB, 컴파일러 미검사 | Send/Sync로 컴파일 에러 |
| 공유 메모리 | mutex, atomic | Arc<Mutex |
| 메시지 전달 | 수동 구현 또는 라이브러리 | std::sync::mpsc 채널 |
| 스레드 로컬 | thread_local | thread_local! |
6. 성능 비교
벤치마크 관점
| 항목 | C++ | Rust |
|---|---|---|
| 컴파일 타임 | 상대적으로 짧음 | borrow checker로 다소 김 |
| 런타임 오버헤드 | 없음 | 없음 (제로 코스트) |
| 메모리 사용 | 수동 제어 가능 | 비슷한 수준 |
| 최적화 | 오래된 컴파일러·풍부한 옵션 | LLVM 백엔드, 비슷한 최적화 |
실전 성능 팁
C++:
-O2·-O3·LTO 활용std::move로 불필요한 복사 제거- 캐시 친화적 데이터 구조 Rust:
clone()최소화, 참조 활용release빌드 (cargo build --release)Arc·Rc남용 지양
성능 다이어그램
다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart LR
subgraph cpp[C++]
C1[수동 메모리]
C2[이동 의미론]
C3[최적화]
C1 --> C2 --> C3
end
subgraph rust[Rust]
R1[소유권]
R2[제로 코스트]
R3[LLVM]
R1 --> R2 --> R3
end
cpp --> P[비슷한 성능]
rust --> P
7. 완전한 C++ vs Rust 비교표
실전 예시: 네트워크 패킷 처리 파이프라인
동일한 로직을 C++과 Rust로 작성했을 때의 차이입니다. C++ (위험한 패턴): 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <vector>
#include <thread>
#include <functional>
void parse_packet(const std::vector<uint8_t>& packet) {
// 패킷 파싱 로직
}
void on_packet_received(std::vector<uint8_t> packet) {
auto worker = [packet = std::move(packet)]() {
parse_packet(packet);
};
std::thread t(std::move(worker));
t.join();
// packet 사용 불가 — 하지만 실수로 packet.size() 호출하면?
// 컴파일 통과, 런타임 UB (moved-from vector)
}
Rust (안전): 아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 함수 정의 및 구현
fn parse_packet(packet: &[u8]) {
// 패킷 파싱 로직
}
fn on_packet_received(packet: Vec<u8>) {
let handle = std::thread::spawn(move || {
parse_packet(&packet);
});
handle.join().unwrap();
// packet.size() 호출 시도 → 컴파일 에러: use of moved value
}
Rust에서는 packet이 클로저로 이동했으므로, 이후 packet 사용 시 컴파일 에러가 납니다. C++에서는 같은 실수가 런타임에만 드러납니다.
실전 예시: 에러 전파 체인
C++ (예외): 아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
int process_file(const std::string& path) {
std::ifstream f(path);
if (!f) throw std::runtime_error("파일 열기 실패");
std::string line;
std::getline(f, line);
return std::stoi(line); // 변환 실패 시 예외
}
Rust (Result): 아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
use std::fs::File;
use std::io::{BufRead, BufReader};
fn process_file(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
let f = File::open(path)?;
let mut reader = BufReader::new(f);
let mut line = String::new();
reader.read_line(&mut line)?;
let n: i32 = line.trim().parse()?;
Ok(n)
}
Rust의 ? 연산자는 에러를 자동으로 전파하되, 호출자가 Result를 처리하지 않으면 컴파일 에러가 납니다. C++ 예외는 호출자가 try-catch를 쓰지 않아도 컴파일이 됩니다.
타입·메모리 대응표
| C++ | Rust | 비고 |
|---|---|---|
std::unique_ptr<T> | Box<T> | 단일 소유권 |
std::shared_ptr<T> | Arc<T> | 스레드 안전 공유 |
std::shared_ptr<T> (단일 스레드) | Rc<T> | non-Send |
T* (raw) | *mut T / *const T | unsafe |
T& | &T / &mut T | 빌림 |
std::string | String | 소유 문자열 |
std::string_view | &str | 수명 있는 슬라이스 |
std::vector<T> | Vec<T> | 동적 배열 |
std::optional<T> | Option<T> | 값 없음 표현 |
std::expected<T,E> | Result<T,E> | 에러 처리 |
핵심 차이 요약
| 영역 | C++ | Rust |
|---|---|---|
| 소유권 | 수동, std::move | 기본 이동, 컴파일 검사 |
| 메모리 안전성 | 개발자·도구 책임 | 타입 시스템 보장 |
| 에러 처리 | 예외, expected | Result, Option |
| 동시성 | mutex, atomic | Send, Sync |
| null | raw 포인터 | Option |
| 빌드 | 상대적으로 빠름 | borrow checker로 다소 김 |
8. 자주 하는 실수와 해결법
C++ 에러
문제 1: “Segmentation fault” — nullptr 역참조
원인: std::move 후 unique_ptr 역참조, 또는 수동 delete 후 포인터 사용.
해결법:
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 잘못된 코드
auto p = std::make_unique<int>(42);
auto q = std::move(p);
int x = *p; // UB
// ✅ 올바른 코드
auto p = std::make_unique<int>(42);
auto q = std::move(p);
int x = *q; // q 사용
문제 2: “Use-after-free” — 댕글링 포인터
원인: 로컬 객체의 포인터/참조 반환. 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 잘못된 코드
const char* get_name() {
std::string s = "hello";
return s.c_str();
}
// ✅ 올바른 코드
std::string get_name() {
return "hello";
}
문제 3: “Data race” — 동기화 없이 공유
해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 잘못된 코드
int counter = 0;
std::thread t1([&]{ ++counter; });
std::thread t2([&]{ ++counter; });
// ✅ 올바른 코드
std::atomic<int> counter{0};
std::thread t1([&]{ ++counter; });
std::thread t2([&]{ ++counter; });
Rust 에러
문제 1: “use of moved value”
해결법: 아래 코드는 rust를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 잘못된 코드
let s = String::from("hello");
let t = s;
println!("{}", s); // 에러
// ✅ 올바른 코드: clone 또는 참조
let s = String::from("hello");
let t = s.clone();
println!("{}", s); // OK
문제 2: “borrowed value does not live long enough”
해결법: 아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 잘못된 코드
// fn get_name() -> &str {
// let s = String::from("hello");
// &s
// }
// ✅ 올바른 코드
fn get_name() -> String {
String::from("hello")
}
문제 3: “cannot be sent between threads safely”
해결법: 아래 코드는 rust를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 잘못된 코드: Rc는 Send가 아님
// let r = Rc::new(42);
// thread::spawn(move || { println!("{}", r); });
// ✅ 올바른 코드: Arc 사용
let r = Arc::new(42);
let r_clone = r.clone();
thread::spawn(move || {
println!("{}", r_clone);
});
문제 4: “cannot borrow as mutable”
해결법: 아래 코드는 rust를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 잘못된 코드
// let mut v = vec![1, 2, 3];
// let r = &v[0];
// v.push(4); // 에러
// ✅ 올바른 코드: 스코프 분리
let mut v = vec![1, 2, 3];
let x = v[0];
v.push(4); // OK
C++ Sanitizer 활용
| Sanitizer | 검출 대상 | 빌드 옵션 |
|---|---|---|
| AddressSanitizer | use-after-free, 댕글링 | -fsanitize=address |
| ThreadSanitizer | Data Race | -fsanitize=thread |
| UBSan | nullptr 역참조, 정수 오버플로우 | -fsanitize=undefined |
9. 모범 사례·베스트 프랙티스
C++ 베스트 프랙티스
- 스마트 포인터 일관 사용: raw 포인터 최소화,
unique_ptr·shared_ptr우선 - 이동 후 사용 금지:
std::move후 원본 접근 금지, Clang-Tidy로 검사 - RAII: 리소스 획득은 생성자, 해제는 소멸자
- const 정확성: 변경 없으면
const사용 - Sanitizer CI 적용: ASan, TSan으로 버그 조기 발견
Rust 베스트 프랙티스
- clone() 최소화: 참조·빌림으로 대체
- Result·Option 처리:
?연산자,match·if let활용 - Rc vs Arc: 스레드 사용 시
Arc만 - unsafe 최소화: 경계를 좁게, 문서화
- Clippy:
cargo clippy로 경고 수정
공통 베스트 프랙티스
- 테스트: 단위 테스트·통합 테스트 작성
- 문서화: 공개 API 문서화
- 코드 리뷰: 메모리·동시성 관련 패턴 검토
10. 프로덕션 패턴
패턴 1: C++에서 안전한 이동
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
[[nodiscard]] std::unique_ptr<Buffer> create_buffer() {
return std::make_unique<Buffer>(1024);
}
void process() {
auto buf = create_buffer();
if (!buf) return;
consume(std::move(buf));
// buf 사용 금지
}
패턴 2: Rust에서 수명 명시
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
패턴 3: 에러 처리
C++ (std::expected): 다음은 간단한 cpp 코드 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
std::expected<Result, Error> parse(const std::string& input) {
if (input.empty()) return std::unexpected(Error::Empty);
return Result{};
}
Rust: 아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
fn parse(input: &str) -> Result<ResultType, Error> {
if input.is_empty() {
return Err(Error::Empty);
}
Ok(ResultType::new())
}
패턴 4: 스레드 안전 공유
C++: 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.
struct SharedState {
std::mutex mtx;
int counter;
};
auto state = std::make_shared<SharedState>();
std::thread t([state] {
std::lock_guard g(state->mtx);
++state->counter;
});
Rust: 아래 코드는 rust를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
let state = Arc::new(Mutex::new(0));
let state_clone = state.clone();
thread::spawn(move || {
*state_clone.lock().unwrap() += 1;
});
패턴 5: 버퍼 전달
C++:
void process(std::span<const uint8_t> data) {
// data.data(), data.size() 사용
}
Rust:
fn process(data: &[u8]) {
// data 사용
}
구현 체크리스트
- C++: 이동 후 사용 금지, Clang-Tidy 검사
- C++: 댕글링 방지 — 값 반환 또는 수명 명확화
- C++: Data Race 방지 — atomic 또는 mutex
- Rust: Rc vs Arc — 스레드 사용 시 Arc
- Rust: 수명 에러 시 소유권 반환 또는 수명 파라미터
- 양쪽: Sanitizer(ASan, TSan)로 C++ 경계 검증
11. 정리 및 선택 가이드
핵심 요약
| 문제 | C++ | Rust |
|---|---|---|
| 이동 후 사용 | 통과 가능, UB | 컴파일 에러 |
| 댕글링 참조 | 통과 가능, UB | 수명으로 컴파일 에러 |
| Data Race | 통과, UB | Send/Sync로 컴파일 에러 |
| 에러 처리 | 예외·expected | Result·Option 강제 |
- C++: 유연하고 생태계가 크지만, 메모리·동시성 오류는 개발자 책임과 도구(ASan, TSan)에 많이 의존합니다.
- Rust: 컴파일이 더 까다롭지만, 같은 종류의 버그를 빌드 단계에서 줄일 수 있습니다.
언어 선택 가이드
| 상황 | 권장 |
|---|---|
| 레거시 C++ 코드베이스가 큼 | C++ 유지 + Sanitizer·Clang-Tidy |
| 새 프로젝트, 메모리 안전성 우선 | Rust |
| 임베디드·리소스 극한 | C++ 또는 Rust no_std |
| 동시성 중심 (서버·파이프라인) | Rust (Send/Sync) |
| FFI·기존 라이브러리 연동 | cxx·bindgen 활용 |
성능: 컴파일 vs 런타임
| 항목 | C++ | Rust |
|---|---|---|
| 컴파일 타임 검사 | 제한적 | 소유권·수명·Send/Sync |
| 런타임 오버헤드 | 없음 | 없음 |
| Sanitizer 사용 시 | 2~3배 느림 | 해당 없음 |
| 빌드 시간 | 상대적으로 짧음 | borrow checker로 다소 김 |
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 스마트 포인터 | 3일 동안 찾지 못한 순환 참조 버그 해결법
- Rust vs C++ 메모리 안전성 | 컴파일러 오류 차이 [#47-3]
- C++ std::thread 입문 | join 누락·디태치 남용 등 자주 하는 실수 3가지와 해결법
이 글에서 다루는 키워드 (관련 검색어)
C++ Rust 비교, 소유권, Borrow checker, 메모리 안전성, 에러 처리, 동시성, 성능 등으로 검색하시면 이 글이 도움이 됩니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 새 프로젝트 기술 선택, C++ 코드베이스에 Rust 도입 검토, 메모리 안전성·동시성 요구사항 분석 시 C++ vs Rust 비교 가이드를 참고하세요.
Q. C++에서 Rust 수준의 안전성을 얻으려면?
A. 스마트 포인터를 일관되게 사용하고, Clang-Tidy·AddressSanitizer·ThreadSanitizer를 CI에 적용하면 많은 버그를 사전에 잡을 수 있습니다.
Q. Rust의 borrow checker가 너무 까다로운데요?
A. 초기에는 clone()으로 우회할 수 있지만, 성능이 중요하면 참조와 수명을 익혀야 합니다. Rc·Arc로 소유권 공유를 명시하면 요구사항을 자연스럽게 만족할 수 있습니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.