C++ vs Rust 완전 비교 | 소유권·메모리 안전성·에러 처리·동시성·성능 실전 가이드
이 글의 핵심
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++ vs Rust 비교표
- 자주 하는 실수와 해결법
- 모범 사례·베스트 프랙티스
- 프로덕션 패턴
- 정리 및 선택 가이드
1. 문제 시나리오: 기술 선택·마이그레이션 시 겪는 상황
기술 선택이나 마이그레이션을 잘못하면 아래와 같은 문제가 발생합니다.
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가 되지만, 일반 타입은 “비어 있음”을 보장하지 않습니다. 재사용하면 미정의 동작이 될 수 있고, 컴파일러는 대부분 막지 않습니다.
#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에서는 이동이 기본입니다. 값이 이동하면 원래 변수는 사용 불가가 됩니다. 컴파일러가 “이미 이동된 값”을 쓰려 하면 컴파일 에러입니다.
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++:
#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:
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에서는 컴파일 에러입니다.
소유권 비교 다이어그램
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++: 로컬 객체의 포인터를 반환하면 댕글링이 됩니다. 컴파일러가 반드시 막는 것은 아닙니다.
const char* get_name() {
std::string s = "hello";
return s.c_str(); // s 소멸 후 댕글링, 컴파일 통과 가능
}
Rust: 수명(lifetime)으로 “참조가 가리키는 값보다 참조가 더 오래 살 수 없다”를 검사합니다.
// 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 등으로 컨테이너가 변경되면 이터레이터가 무효화됩니다. 컴파일러는 막지 않습니다.
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를 가변으로 빌리면 컴파일 에러입니다.
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>로 “값이 있거나 없음”을 타입으로 표현합니다.
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으로 검사 |
| 이중 해제 | 수동 관리 시 가능 | 소유권으로 불가능 |
4. 에러 처리 비교
C++: 예외와 std::expected
예외: 전통적인 방식. 호출자가 처리하지 않으면 스택 언와인딩 후 종료됩니다.
#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): 예외 없이 에러를 값으로 반환합니다.
#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: 에러 가능성을 타입으로 강제합니다. ? 연산자로 전파할 수 있습니다.
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: 값이 없을 수 있는 경우입니다.
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입니다. 컴파일러가 막지 않습니다.
#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;
}
뮤텍스:
#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가 아니어서 스레드로 넘기면 컴파일 에러입니다.
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
}
채널:
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남용 지양
성능 다이어그램
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++ (위험한 패턴):
#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 (안전):
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++ (예외):
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):
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 후 포인터 사용.
해결법:
// ❌ 잘못된 코드
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” — 댕글링 포인터
원인: 로컬 객체의 포인터/참조 반환.
해결법:
// ❌ 잘못된 코드
const char* get_name() {
std::string s = "hello";
return s.c_str();
}
// ✅ 올바른 코드
std::string get_name() {
return "hello";
}
문제 3: “Data race” — 동기화 없이 공유
해결법:
// ❌ 잘못된 코드
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”
해결법:
// ❌ 잘못된 코드
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”
해결법:
// ❌ 잘못된 코드
// fn get_name() -> &str {
// let s = String::from("hello");
// &s
// }
// ✅ 올바른 코드
fn get_name() -> String {
String::from("hello")
}
문제 3: “cannot be sent between threads safely”
해결법:
// ❌ 잘못된 코드: 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”
해결법:
// ❌ 잘못된 코드
// 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++에서 안전한 이동
[[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):
std::expected<Result, Error> parse(const std::string& input) {
if (input.empty()) return std::unexpected(Error::Empty);
return Result{};
}
Rust:
fn parse(input: &str) -> Result<ResultType, Error> {
if input.is_empty() {
return Err(Error::Empty);
}
Ok(ResultType::new())
}
패턴 4: 스레드 안전 공유
C++:
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:
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++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 The Rust Book을 참고하세요.
한 줄 요약: C++과 Rust의 소유권·메모리·에러 처리·동시성·성능 차이를 이해하면 기술 선택이 명확해집니다. 다음으로 Rust vs C++ 메모리 안전성(#47-3)를 읽어보면 좋습니다.
이전 글: [C++ vs 타 언어 #46-3] 도메인별 C++ 요구 역량 차이
다음 글: [C++ vs 타 언어 #47-2] C++ 개발자의 뇌 구조로 이해하는 Go 언어
관련 글
- Rust vs C++ 메모리 안전성 | 컴파일러 오류 차이 [#47-3]
- Rust 메모리 안전성 완벽 가이드 | 소유권·Borrow checker·수명·unsafe 실전
- C++ vs Go | 성능·동시성·선택 가이드 완전 비교 [#47-1]
- C++ 개발자의 뇌 구조로 이해하는 Go 언어 [#47-2]
- C++ 함수 객체(Functor) 완벽 가이드 | operator·상태 보유