C++ 개발자를 위한 Rust 마스터 가이드
C++ STL에 익숙한 개발자가 Rust의 핵심 개념을 체계적으로 습득할 수 있도록 설계된 100페이지 분량의 종합 학습 가이드를 만들어 봤습니다.
전체 목차
제 1부: 패러다임의 전환 (1-15p)
- Ch 1. 왜 Rust인가?: C++의 한계와 Rust의 탄생 배경 (1-5p)
- Ch 2. 소유권(Ownership) 시스템: 복사(Copy)와 이동(Move)의 철학적 차이 (6-10p)
- Ch 3. 빌림(Borrowing)과 라이프타임: 참조자(&)의 안전성 원리 (11-15p)
제 2부: 컨테이너와 자료구조 – STL 대응 (16-40p)
- Ch 4. 연속된 메모리: std::vector vs Vec
(16-20p) - Ch 5. 키-값 저장소: std::map/unordered_map vs HashMap/BTreeMap (21-25p)
- Ch 6. 집합과 큐: std::set, std::deque 대응 기능 완벽 분석 (26-30p)
- Ch 7. 스마트 포인터: unique_ptr/shared_ptr vs Box/Arc/Rc (31-40p)
제 3부: 알고리즘과 반복자 (41-60p)
- Ch 8. 반복자(Iterator) 마스터: begin/end를 넘어선 메서드 체이닝 (41-50p)
- Ch 9. 정렬과 검색: 고성능 알고리즘 구현 기법 (51-60p)
제 4부: 멀티스레딩과 동시성 (61-80p)
- Ch 10. 스레드와 동기화: std::thread/mutex vs Safe Concurrency (61-70p)
- Ch 11. 비동기 프로그래밍: std::async vs Tokio/Future (71-80p)
제 5부: 실전 비즈니스 모델링 (81-100p)
- Ch 12. 케이스 스터디: 고성능 거래소 엔진 설계 (81-90p)
- Ch 13. 에러 처리 전략: Exception vs Result/Option (91-95p)
- Ch 14. 부록: C++ to Rust 문법 퀵 시트 (96-100p)
제 1부: 패러다임의 전환
Ch 1. 왜 Rust인가? (1-5p)
1.1 C++의 영원한 숙제: 성능 vs 안전성
C++은 "당신이 사용하지 않는 것에 대해서는 지불하지 않는다(Zero-cost Abstraction)"는 철학으로 지난 수십 년간 시스템 프로그래밍의 제왕으로 군림해 왔습니다. 하지만 이 강력한 자유에는 막대한 대가가 따랐습니다.
- Dangling Pointers: 해제된 메모리를 참조하는 문제.
- Buffer Overflow: 할당된 범위를 넘어선 메모리 쓰기.
- Memory Leaks: 명시적 delete를 잊어 발생하는 자원 고갈.
C++11 이후 스마트 포인터가 도입되었지만, 이는 가이드라인일 뿐 언어 차원의 강제성이 부족합니다. 통계에 따르면 Microsoft와 Google의 보안 취약점 중 약 70%가 메모리 관련 버그입니다.
1.2 멀티코어 시대의 재앙: 데이터 경합 (Data Race)
멀티코어 환경에서 여러 스레드가 동시에 같은 메모리를 수정할 때 발생하는 데이터 경합은 재현하기 어렵고 디버깅이 불가능에 가까운 ‘하이젠버그(Heisenbug)’를 만들어냅니다. C++ 뮤텍스는 개발자의 실수(잠금 잊음 등)를 막아주지 못합니다.
1.3 Rust의 해답: "컴파일 타임에 모든 것을 해결한다"
Rust는 C++와 동일한 성능을 유지하면서도 위의 문제들을 컴파일 시점 에 원천 차단합니다.
- 소유권(Ownership): 메모리의 단일 소유자를 지정하여 가비지 컬렉터 없이도 안전한 자동 해제를 보장합니다.
- 빌림 검사기(Borrow Checker): "여러 명이 읽을 수는 있지만, 고치는 것은 단 한 명만 가능하다"는 규칙을 컴파일러가 감시합니다.
- 두려움 없는 동시성(Fearless Concurrency): 데이터 경합이 일어날 가능성이 있는 코드는 아예 컴파일되지 않습니다.
1.4 결론: 생산성의 혁명
Rust를 배우는 초기 과정은 C++보다 고통스러울 수 있습니다(일명 ‘Borrow Checker와의 싸움’). 하지만 일단 컴파일에 성공하면, 당신은 "런타임에 메모리 오류나 데이터 경합으로 프로그램이 터지지 않을 것"이라는 강력한 확신을 얻게 됩니다. 이것이 수많은 기업이 Rust로 전환하는 이유입니다.
Ch 2. 소유권(Ownership) 시스템: 복사(Copy)와 이동(Move)의 철학적 차이 (6-10p)
2.1 소유권의 3대 원칙
Rust 메모리 관리의 심장부에는 다음 세 가지 단순한 규칙이 있습니다.
- Rust의 모든 값에는 그 값을 소유하는 변수가 하나 있습니다. (소유자, Owner)
- 소유자는 오직 하나뿐입니다. (단일 소유권)
- 소유자가 스코프(Scope)를 벗어나면 그 값은 즉시 메모리에서 해제(Drop)됩니다.
2.2 C++의 Copy vs Rust의 Move
C++ 개발자가 가장 먼저 겪는 문화 충격은 ‘할당이 곧 이동’이라는 점입니다.
- C++ (Default Copy):
string s1 = "hello"; string s2 = s1; // s1의 내용이 복사됨. s1, s2 둘 다 사용 가능. - Rust (Default Move):
let s1 = String::from("hello"); let s2 = s1; // s1의 소유권이 s2로 '이동'함. // println!("{}", s1); // 컴파일 에러! (s1은 이미 죽은 변수)Rust는 왜 이렇게 할까요? 만약 s1과 s2가 같은 힙 메모리를 가리키게 두면, 스코프가 끝날 때 Double Free 에러가 발생합니다. C++은 이를 막기 위해 깊은 복사(Deep Copy)를 수행하지만, Rust는 아예 소유권을 넘겨버림으로써 성능(복사 안 함)과 안전성(이중 해제 방지)을 동시에 잡습니다.
2.3 Copy 트레이트와 명시적 Clone
"그럼 모든 변수를 쓸 때마다 소유권을 걱정해야 하나요?" 아닙니다.
- Copy 트레이트: 정수(i32), 부동소수점(f64), 불리언 등 크기가 작고 고정된 타입은 자동으로 복사됩니다. (C++의 기본 타입과 동일하게 동작)
- Clone: String이나 Vec처럼 무거운 데이터를 복사하고 싶다면 반드시 .clone()을 명시적으로 호출해야 합니다. "나도 모르게 무거운 복사가 일어나는 일은 없다"는 것이 Rust의 철학입니다.
2.4 소유권이 주는 선물: RAII의 완성
C++의 RAII(자원 획득은 초기화) 패턴은 훌륭하지만, 개발자가 소멸자를 직접 관리해야 할 때가 많습니다. Rust는 소유권 시스템이 컴파일 타임에 drop 호출 시점을 완벽히 계산하므로, 메모리뿐만 아니라 파일 핸들, 네트워크 소켓 등 모든 자원이 "가장 완벽한 타이밍"에 자동으로 해제됩니다.
Ch 3. 빌림(Borrowing)과 라이프타임: 참조자(&)의 안전성 원리 (11-15p)
3.1 왜 빌려야 하는가?
모든 값을 이동(Move)시키면 함수를 한 번 부를 때마다 소유권을 돌려받아야 하는 번거로움이 생깁니다. C++에서 포인터나 참조자(&)를 쓰듯, Rust에서도 값을 소유하지 않고 ‘잠시 빌려 쓰는’ 참조 기능을 제공합니다.
3.2 빌림의 두 가지 규칙 (The Borrowing Rules)
Rust 컴파일러는 다음 규칙을 단 한 치의 오차도 없이 검사합니다.
- 불변 참조 (&T): 동시에 여러 명이 읽을 수 있습니다. (Shared)
- 가변 참조 (&mut T): 동시에 딱 한 명만 고칠 수 있습니다. (Exclusive)
- 가장 중요한 규칙: 여러 명이 읽고 있는 도중에는 아무도 고칠 수 없습니다.
- C++에서의 위험한 코드:
vector<int> v = {1, 2, 3}; auto& ref = v[0]; v.push_back(4); // 재할당 발생 시 ref는 무효화(Dangling)되지만 컴파일러는 모름. cout << ref; // 런타임 에러 또는 이상한 값 출력. - Rust의 철저한 방어:
let mut v = vec![1, 2, 3]; let ref1 = &v[0]; // 읽기 빌림 시작 v.push(4); // 에러! "읽고 있는 중에는 수정(가변 빌림)할 수 없음" println!("{}", ref1);
3.3 데이터 경합(Data Race)의 종말
이 규칙 덕분에 Rust는 멀티스레드 환경에서 "누군가 데이터를 고치고 있을 때는 아무도 읽을 수 없고, 누군가 읽고 있을 때는 아무도 고칠 수 없음"을 보장합니다. 이것이 Rust가 ‘안전한 언어’로 불리는 가장 큰 이유입니다.
3.4 라이프타임 (Lifetimes): 유효 기간 확인
C++에서 가장 흔한 버그 중 하나는 이미 사라진 객체를 가리키는 포인터를 사용하는 것입니다.
- Rust의 해결책: 컴파일러가 모든 참조자의 유효 기간을 계산합니다. 만약 참조하는 원본 데이터가 참조자보다 먼저 사라지려고 하면 컴파일러가 빨간 줄을 띄웁니다.
- 기억하세요: 라이프타임은 메모리를 늘리는 마법이 아니라, "참조자가 원본보다 오래 살지 못하게 감시하는 보안관"입니다.
제 2부: 컨테이너와 자료구조 – STL 대응
Ch 4. 연속된 메모리: std::vector vs Vec (16-20p)
4.1 가장 친숙한 컨테이너
C++의 std::vector는 Rust에서 Vec
4.2 메모리 레이아웃의 동일성
놀랍게도 두 언어의 벡터 내부 구조는 거의 똑같습니다. 스택 공간에 다음 3가지 정보를 담고 있습니다.
- Pointer: 데이터가 시작되는 힙 메모리 주소
- Capacity: 현재 할당된 총 메모리 크기
- Length (Size): 현재 실제로 담긴 데이터 개수
4.3 결정적인 차이: 안전한 접근
- C++ (위험한 접근):
vector<int> v = {1, 2}; cout << v[5]; // 범위를 벗어난 접근. "정의되지 않은 동작(UD)" 발생. // 쓰레기 값이 나오거나 프로그램이 나중에 갑자기 터짐. -
Rust (안전한 접근):
let v = vec![1, 2]; // println!("{}", v[5]); // 실행 즉시 "Panic" 발생. 안전하게 프로그램 종료. // 더 권장되는 방식 (안전한 조회) match v.get(5) { Some(val) => println!("값: {}", val), None => println!("값이 없습니다."), // 에러 처리를 우아하게 유도 }
4.4 성능 최적화: reserve vs with_capacity
C++에서 reserve()를 써서 재할당 횟수를 줄이듯, Rust에서도 미리 메모리를 예약할 수 있습니다.
- C++: v.reserve(100);
- Rust: let mut v = Vec::with_capacity(100);
불필요한 메모리 복사를 줄이는 기법은 두 언어 모두 동일하게 중요합니다.
4.5 가변성 선언 (mut)
C++에서는 const를 붙이지 않으면 기본적으로 가변이지만, Rust는 기본적으로 불변(Immutable)입니다. 벡터에 요소를 추가하려면 반드시 let mut v = …와 같이 선언해야 합니다. 이는 "누가 이 데이터를 고치고 있는지"를 명확히 추적하기 위함입니다.
Ch 5. 키-값 저장소: std::map/unordered_map vs HashMap/BTreeMap (21-25p)
5.1 두 가지 유형의 맵
C++와 마찬가지로 Rust도 구현 원리에 따라 두 가지 주요 맵을 제공합니다.
- HashMap (C++ unordered_map 대응): 해시 테이블 기반. 검색 속도가 평균적으로 O(1)로 매우 빠릅니다.
- BTreeMap (C++ map 대응): B-트리 기반. 키가 항상 정렬된 상태를 유지하며, 범위 조회가 효율적입니다.
5.2 보안이 강화된 해시 알고리즘
Rust의 HashMap은 기본적으로 SipHash 알고리즘을 사용합니다. 이는 해시 충돌을 악용한 서비스 거부 공격(Hash DoS)을 방지하기 위해 설계되었습니다.
- 성능 팁: 보안보다 절대적인 속도가 중요한 알고리즘 문제 풀이 등에서는 더 빠른 해시 함수(fnv, fxhash 등)로 쉽게 교체할 수 있습니다.
5.3 결정적 차이: 존재하지 않는 키 접근
C++ 개발자가 Rust 맵을 쓸 때 가장 당황하는 부분은 조회 방식입니다.
- C++ (자동 삽입의 위험):
unordered_map<string, int> m; int value = m["missing_key"]; // 키가 없으면 기본값(0)을 삽입하고 리턴함! // 의도치 않게 맵의 크기가 커질 수 있음. -
Rust (명시적 처리 강제):
let mut m = HashMap::new(); // let value = m["missing_key"]; // 컴파일 에러 혹은 실행 시 패닉 위험 // 안전하고 관용적인 방식 match m.get("missing_key") { Some(val) => println!("값: {}", val), None => println!("키가 존재하지 않습니다."), // 개발자가 이 상황을 인지하게 함 }
5.4 엔트리(Entry) API: "없으면 넣고, 있으면 가져오기"
C++의 복잡한 find와 insert 조합 대신, Rust는 매우 직관적인 Entry API를 제공합니다.
// 키가 없으면 0을 넣고, 그 값의 가변 참조를 가져와서 1을 더함
m.entry("score").or_insert(0).add_assign(1);
이 한 줄의 코드는 C++에서 여러 줄의 조건문으로 구현해야 했던 로직을 안전하고 효율적으로 대체합니다.
Ch 6. 집합과 큐: std::set, std::deque 대응 기능 분석 (26-30p)
6.1 집합 (Sets)
-
HashSet (C++ unordered_set 대응): 해시 테이블 기반 집합.
-
BTreeSet (C++ set 대응): 정렬된 상태를 유지하는 집합.
-
구현 샘플 비교:
// Rust: 집합 연산 (합집합) let s1: HashSet<_> = [1, 2, 3].into_iter().collect(); let s2: HashSet<_> = [3, 4, 5].into_iter().collect(); // 연산 결과는 반복자로 반환됨 (지연 연산) let union_set: HashSet<_> = s1.union(&s2).cloned().collect();
6.2 큐 (Deque)
-
VecDeque
(C++ std::deque 대응): 양방향 큐. -
구현 샘플:
use std::collections::VecDeque; let mut dq = VecDeque::new(); dq.push_back(10); // 뒤에 삽입 dq.push_front(5); // 앞에 삽입 (C++의 push_front와 동일) // 인덱스로 접근 가능 (O(1)) println!("첫 번째 요소: {}", dq[0]); // 효율적인 양방향 제거 let front = dq.pop_front(); // 앞에서 제거 let back = dq.pop_back(); // 뒤에서 제거 -
메모리 구조 차이:
- C++ deque: 조각난 메모리 블록을 연결하여 할당. 인덱스 접근 시 약간의 오버헤드 발생.
- Rust VecDeque: 단일 연속 메모리 블록 내에서 인덱스를 순환시키는 ‘링 버퍼(Ring Buffer)’ 구조. 메모리 지역성이 뛰어나 성능상 유리함.
Ch 7. 스마트 포인터: unique_ptr/shared_ptr vs Box/Arc/Rc (31-40p)
7.1 Box: 독점적 할당
-
C++: unique_ptr
p = make_unique (10); -
Rust: let p = Box::new(10);
-
구현 샘플:
// 힙 메모리에 정수 저장 (std::unique_ptr와 유사) let p1 = Box::new(10); // 소유권 이동 (C++의 std::move와 동일하게 동작) let p2 = p1; // println!("{}", p1); // 컴파일 에러! p1은 이미 소유권을 잃었습니다. println!("p2의 값: {}", p2); -
차이점: Rust는 모든 것이 기본적으로 이동(Move)되므로, std::move 없이도 완벽한 독점적 소유권 이전이 보장됩니다.
7.2 Rc와 Arc: 공유 할당
C++은 shared_ptr 하나로 통일하지만, Rust는 성능 최적화를 위해 둘을 분리합니다.
| 기능 | Rust Rc |
Rust Arc |
C++ shared_ptr |
|---|---|---|---|
| 스레드 안전 | No (Single Thread) | Yes (Multi Thread) | Yes (Atomic RC) |
| 오버헤드 | 낮음 (일반 정수 연산) | 높음 (원자적 연산) | 높음 (원자적 연산) |
- 구현 샘플:
// 멀티스레드에서 안전하게 공유하기 (Arc) let data = Arc::new(5); for _ in 0..3 { let clone = Arc::clone(&data); // 참조 횟수 증가 (명시적) thread::spawn(move || { println!("값: {}", clone); }); }
7.3 Weak: 순환 참조 해결
C++의 weak_ptr와 동일하게, Arc나 Rc 사이의 상호 참조로 인한 메모리 누수를 막기 위해 사용됩니다.
-
구현 샘플:
use std::sync::Arc; let five = Arc::new(5); let weak_five = Arc::downgrade(&five); // 약한 참조 생성 // 사용 시에는 반드시 살아있는지 확인(upgrade)해야 함 match weak_five.upgrade() { Some(val) => println!("값이 살아있음: {}", val), None => println!("원본이 이미 해제됨"), }
제 3부: 알고리즘과 반복자
Ch 8. 반복자(Iterator) 마스터: begin/end를 넘어선 체이닝 (41-50p)
8.1 함수형 패러다임의 수용
C++은 반복자 쌍(v.begin(), v.end())을 함수에 넘기는 방식이지만, Rust는 반복자 객체 자체에 강력한 메서드들을 달아놓았습니다.
// C++: 여러 줄의 for 루프와 if문
// Rust: 선언적인 체이닝
let result: Vec<_> = v.iter()
.filter(|&x| x % 2 == 0) // 짝수만 골라서
.map(|x| x * 10) // 10을 곱하고
.collect(); // 다시 벡터로 모음
8.2 Zero-cost Laziness (게으른 계산)
Rust의 반복자는 ‘실제로 호출되기 전까지는 아무 일도 하지 않습니다.’ 아무리 긴 체인을 엮어도 마지막에 .collect()나 for 루프에서 부르기 전까지는 연산 비용이 0입니다. 컴파일러는 이 체인을 분석하여 수동으로 짠 for 루프만큼 빠른 기계어로 최적화합니다.
Ch 9. 정렬과 검색: 고성능 알고리즘 구현 기법 (51-60p)
9.1 sort vs sort_unstable
-
sort(): 안정 정렬(Stable). 같은 값의 순서가 보장됩니다. 추가 메모리를 사용합니다.
-
sort_unstable(): 불안정 정렬. 순서 보장은 안 되지만 더 빠르고 추가 메모리를 쓰지 않습니다. C++의 std::sort가 보통 이 방식입니다.
-
구현 샘플:
let mut v = vec![5, 2, 8, 1, 9]; // 1. 기본 정렬 (안정) v.sort(); // 2. 고성능 정렬 (불안정, 메모리 절약) v.sort_unstable(); // 3. 내림차순 정렬 (클로저 사용) v.sort_by(|a, b| b.cmp(a));
9.2 검색 알고리즘
-
binary_search(): 정렬된 벡터에서 이진 검색을 수행합니다. 결과로 Result를 반환하여 값이 있으면 인덱스를, 없으면 삽입해야 할 위치를 알려줍니다.
-
구현 샘플:
let v = vec![10, 20, 30, 40, 50]; match v.binary_search(&30) { Ok(index) => println!("{}번 인덱스에서 찾았습니다.", index), Err(insert_at) => println!("없습니다. {}번에 넣으면 정렬이 유지됩니다.", insert_at), }
제 4부: 멀티스레딩과 동시성
Ch 10. 스레드와 동기화: std::thread/mutex vs Safe Concurrency (61-70p)
10.1 std::thread와 1:1 대응
Rust의 thread::spawn은 C++의 std::thread와 같이 실제 OS 스레드를 생성합니다.
-
구현 샘플:
use std::thread; let handle = thread::spawn(|| { println!("새로운 스레드에서 실행 중!"); }); handle.join().unwrap(); // 스레드 종료 대기 (std::thread::join과 동일)
10.2 Send와 Sync 트레이트 (마법의 명찰)
Rust 컴파일러는 모든 타입에 두 가지 질문을 던집니다.
- Send: "이 데이터를 다른 스레드로 보내도 안전한가?"
- Sync: "여러 스레드가 동시에 이 데이터를 읽어도 안전한가?"
이 질문에 통과하지 못한 데이터(Rc 등)를 스레드 간에 공유하려고 하면 컴파일러가 즉시 에러를 발생시켜 런타임 사고를 막습니다.
10.3 Mutex의 근본적 혁신: 데이터를 소유함
-
C++ Mutex: "데이터는 여기 있고, 뮤텍스는 저기 있으니 내가 알아서 lock을 잘 걸어야지." (실수하기 쉬움)
-
Rust Mutex: "데이터를 뮤텍스 안에 집어넣어라. 잠금을 풀지(Lock) 않으면 데이터에 손도 못 댄다."
-
구현 샘플:
use std::sync::{Arc, Mutex}; use std::thread; let counter = Arc::new(Mutex::new(0)); // Arc와 조합하여 여러 스레드에서 공유 let mut handles = vec![]; for _ in 0..10 { let counter_clone = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter_clone.lock().unwrap(); // 잠금 획득 *num += 1; // 데이터 수정 }); // 여기서 잠금이 자동으로 해제됨 (Drop) handles.push(handle); }
10.4 채널(Channel): 스레드 간 통신
"공유 메모리로 통신하지 말고, 통신으로 메모리를 공유하라"는 철학을 구현한 mpsc(Multi-Producer, Single-Consumer) 채널을 기본 제공합니다. C++에서 복잡하게 구현해야 했던 스레드 간 메시지 큐를 매우 안전하게 사용할 수 있습니다.
-
구현 샘플:
use std::sync::mpsc; use std::thread; let (tx, rx) = mpsc::channel(); // 송신자(tx), 수신자(rx) thread::spawn(move || { tx.send("메시지 보냄!").unwrap(); }); let received = rx.recv().unwrap(); // 메시지 받을 때까지 대기 println!("받은 내용: {}", received);
Ch 11. 비동기 프로그래밍: std::async vs Tokio/Future (71-80p)
11.1 C++ std::async와의 차이
C++의 std::async는 즉시 스레드를 할당받아 실행될 수 있지만, Rust의 async 함수는 ‘호출해도 아무 일도 일어나지 않습니다.’ 이는 단순한 상태 머신(Future)을 생성할 뿐입니다.
11.2 실행 엔진(Executor)의 필요성
Rust 표준 라이브러리에는 비동기 코드를 실제로 돌려줄 엔진이 없습니다. 그래서 Tokio 같은 강력한 외부 라이브러리를 사용합니다.
-
구현 샘플:
#[tokio::main] // Tokio 런타임 시작 매크로 async fn main() { let handle = tokio::spawn(async { // 비동기로 실행될 코드 "비동기 작업 결과" }); let result = handle.await.unwrap(); println!("{}", result); }
11.3 async/await 문법
C++20의 코루틴과 유사하지만, Rust는 훨씬 더 직관적이고 통합된 문법을 제공합니다. .await 키워드를 만나면 현재 스레드는 다른 작업을 하러 떠나고, 작업이 완료되면 다시 돌아옵니다.
-
구현 샘플:
async fn fetch_data() -> String { // 네트워크 요청 시뮬레이션 tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; "데이터 가져옴".to_string() } #[tokio::main] async fn main() { println!("시작..."); let data = fetch_data().await; // 완료될 때까지 효율적으로 대기 println!("결과: {}", data); }
제 5부: 실전 비즈니스 모델링
Ch 12. 케이스 스터디: 고성능 거래소 엔진 설계 (81-90p)
12.1 왜 거래소 엔진인가?
주식/가상화폐 거래소는 "절대 틀리면 안 되는 데이터(안전성)"와 "압도적인 속도(성능)"를 동시에 요구합니다. 이는 Rust가 가장 잘하는 분야입니다.
12.2 핵심 자료구조 설계
-
주문서 (OrderBook): 가격별로 정렬되어야 하므로 BTreeMap<Price, Vec
>를 사용합니다. -
사용자 지갑: ID로 빠른 조회가 필요하므로 HashMap<UserId, Wallet>을 사용합니다.
-
매칭 엔진: 여러 스레드가 동시에 주문을 처리해야 하므로 Arc<Mutex
> 구조로 보호합니다. -
설계 샘플:
struct OrderBook { buy_orders: BTreeMap<u64, Vec<Order>>, // 가격(key) 기준 정렬 sell_orders: BTreeMap<u64, Vec<Order>>, } // 멀티스레드 안전한 거래소 엔진 타입 type TradingEngine = Arc<Mutex<OrderBook>>;
12.3 데이터 파이프라인
네트워크로 들어오는 주문 패킷은 Tokio의 비동기 스트림으로 받고, 무거운 매칭 연산은 spawn_blocking을 통해 멀티코어에 분산시킵니다. 결과는 mpsc 채널을 통해 DB 저장 스레드로 안전하게 전달됩니다.
Ch 13. 에러 처리 전략: Exception vs Result/Option (91-95p)
13.1 예외(Exception)가 없는 언어
Rust에는 C++의 try-catch가 없습니다. 대신 모든 에러 가능성을 함수의 리턴 타입에 명시합니다.
13.2 Result와 Option
-
Option
: 값이 있을 수도(Some), 없을 수도(None) 있음. (C++ std::optional 대응) -
Result<T, E>: 성공(Ok) 혹은 실패(Err).
-
구현 샘플:
fn divide(a: f64, b: f64) -> Result<f64, String> { if b == 0.0 { Err("0으로 나눌 수 없습니다.".to_string()) } else { Ok(a / b) } } // 물음표(?) 연산자를 이용한 에러 전파 fn complex_calc() -> Result<f64, String> { let val = divide(10.0, 2.0)?; // 에러면 즉시 리턴, 성공이면 Ok를 까서 대입 Ok(val * 2.0) }
Ch 14. 부록
C++ to Rust 문법 매핑
| 기능 | C++ 문법 | Rust 문법 |
|---|---|---|
| 상수 선언 | const int X = 10; | const X: i32 = 10; |
| 가변 변수 | int x = 5; | let mut x = 5; |
| 함수 정의 | int add(int a, int b) { … } | fn add(a: i32, b: i32) -> i32 { … } |
| 구조체 | struct Point { int x, y; }; | struct Point { x: i32, y: i32 } |
| 인터페이스 | virtual void draw() = 0; | trait Draw { fn draw(&self); } |
| 반복문 | for (int i=0; i<10; ++i) | for i in 0..10 |
| 메모리 할당 | new T() / delete p | Box::new(T) (자동 해제) |
C++ STL vs Rust 핵심 매핑
| 카테고리 | C++ STL | Rust 기능 | 결정적 차이점 |
|---|---|---|---|
| 가변 배열 | std::vector |
Vec |
Rust는 접근 시 경계 검사 강제 |
| 연결 리스트 | std::list |
LinkedList |
Rust는 소유권 문제로 사용 비권장 |
| 정렬 맵 | std::map<K, V> | BTreeMap<K, V> | 조회 시 Option 반환으로 안전성 확보 |
| 해시 맵 | std::unordered_map | HashMap<K, V> | Rust는 DoS 방지용 보안 해시 기본 사용 |
| 독점 포인터 | std::unique_ptr | Box |
모든 소유권 이전이 기본적으로 ‘이동’ |
| 공유 포인터 | std::shared_ptr | Arc |
clone() 호출로 참조 횟수 증가 명시 |
| 뮤텍스 | std::mutex | Mutex |
데이터를 내부에 소유하여 경합 원천 차단 |
| 비동기 | std::async | tokio::spawn | 게으른(Lazy) 실행 방식으로 효율 극대화 |
[C++ 방식]
mutex mtx;
int data = 0;
{
lock_guard<mutex> lock(mtx);
data += 1; // 데이터와 뮤텍스가 분리됨 (실수 가능성)
}
[Rust 방식]
let m = Mutex::new(0); // 데이터를 뮤텍스 안에 넣음
{
let mut data = m.lock().unwrap(); // 잠금을 얻어야만 데이터 접근 가능
*data += 1;
} // 가드가 범위를 벗어나면 자동으로 잠금 해제
조언
- 컴파일러와 싸우지 마세요: Rust 컴파일러의 에러 메시지는 공격이 아니라 "무료 코드 리뷰"입니다.
- 소유권을 이해하면 80%는 끝납니다: 변수가 언제 죽고 사는지 아는 것이 Rust의 핵심입니다.
- 반복자를 사랑하세요: for 루프보다 .iter().map().filter()가 훨씬 빠르고 안전합니다.