일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
- camera access
- npm package
- adb pair
- Can't resolve
- html
- Failed to compiled
- react-native
- animation
- github 100mb
- vercel git lfs
- device in use
- ELECTRON
- react-native-dotenv
- github lfs
- ffi-napi
- github pdf
- rolldown
- 티스토리 성능
- adb connect
- electron-packager
- Each child in a list should have a unique "key" prop.
- camera permission
- custom printing
- Git
- augmentedDevice
- dvh
- nextjs
- Recoil
- silent printing
- 이미지 데이터 타입
- Today
- Total
Bleeding edge
C++의 약점, Rust가 채우는 방법: 메모리 안전성 코드로 비교하기 본문
C++는 강력한 성능과 유연성으로 수십 년간 소프트웨어 개발의 중추적인 역할을 해왔습니다. 하지만 이러한 강력함 뒤에는 개발자가 직접 메모리를 관리해야 하는 부담이 따르죠. 여기서 종종 메모리 안전성 문제가 발생합니다. 반면, 최근 주목받고 있는 Rust는 C++와 비슷한 성능을 제공하면서도, 메모리 안전성을 강력하게 보장하며 개발자들 사이에서 큰 반향을 일으키고 있습니다.
그렇다면 C++의 어떤 점이 Rust 대비 부족하다고 말할 수 있을까요? 바로 Rust의 소유권(Ownership) 시스템과 **빌림 검사기(Borrow Checker)**가 C++에서는 흔히 발생하는 메모리 관련 오류들을 컴파일 시점에 잡아낸다는 점입니다. 오늘은 댕글링 포인터(Dangling Pointer)와 이중 해제(Double Free)라는 두 가지 대표적인 예시를 통해 Rust가 어떻게 C++의 약점을 보완하는지 코드로 살펴보겠습니다.
1. 댕글링 포인터(Dangling Pointer): 유효하지 않은 메모리 참조
댕글링 포인터는 이미 해제되었거나 유효하지 않은 메모리 영역을 가리키는 포인터를 의미합니다. C++에서는 이러한 포인터를 잘못 사용할 경우 프로그램이 충돌하거나 예상치 못한 동작을 일으킬 수 있죠.
C++에서의 댕글링 포인터 위험 (코드 예시)
#include <iostream>
#include <vector>
std::vector<int>* create_and_return_vec() {
std::vector<int> local_vec = {1, 2, 3}; // 지역 변수
// 함수가 끝나면 local_vec은 메모리에서 해제됩니다.
// 하지만, 이 지역 변수의 주소를 반환하면 댕글링 포인터가 됩니다.
return &local_vec; // 경고: 지역 변수의 주소를 반환합니다.
}
int main() {
std::vector<int>* ptr = create_and_return_vec();
// 이 시점에서 ptr은 유효하지 않은 메모리를 가리킬 수 있습니다.
// 메모리는 이미 해제되었을 가능성이 높고,
// ptr->at(0)와 같이 접근하면 런타임 오류가 발생하거나 예측 불가능한 동작이 나타납니다.
std::cout << "C++: Dangling pointer potential." << std::endl;
return 0;
}
위 C++ 코드에서 create_and_return_vec 함수는 local_vec이라는 지역 변수의 주소를 반환합니다. 함수가 종료되면 local_vec은 스택에서 사라지고, ptr이 가리키는 메모리는 더 이상 유효하지 않게 됩니다. 이렇게 유효하지 않은 포인터를 사용하는 것은 **정의되지 않은 동작(Undefined Behavior)**이며, 디버깅하기 매우 어려운 런타임 버그로 이어질 수 있습니다.
Rust가 댕글링 포인터를 막는 방법 (컴파일 오류!)
fn create_and_return_vec() -> &Vec<i32> {
let local_vec = vec![1, 2, 3]; // 지역 변수
// Rust의 빌림 검사기는 지역 변수의 참조를 반환하는 것을 허용하지 않습니다.
// local_vec은 이 함수를 벗어나면 사라지기 때문에, 그 참조는 유효하지 않게 됩니다.
&local_vec // 컴파일 오류: `local_vec` does not live long enough
}
fn main() {
// let ptr = create_and_return_vec(); // 이 코드 줄에서 이미 컴파일 오류가 발생합니다!
println!("Rust: Dangling pointer prevented at compile time.");
}
Rust 컴파일러는 create_and_return_vec 함수에서 local_vec의 참조를 반환하려는 시도를 컴파일 시점에 즉시 오류로 감지합니다. Rust의 소유권 및 빌림 규칙에 따라, 지역 변수의 생명주기는 해당 함수 스코프 내로 제한됩니다. 따라서 함수 스코프를 벗어나는 참조를 반환하는 것은 안전하지 않다고 판단하고 오류를 발생시켜 개발자가 댕글링 포인터 문제를 런타임 이전에 해결하도록 강제합니다.
2. 이중 해제(Double Free): 이미 해제된 메모리를 또 해제?
이중 해제는 이미 한 번 해제된 메모리 영역을 다시 해제하려고 시도할 때 발생합니다. 이는 프로그램 충돌, 메모리 손상, 심지어 보안 취약점으로 이어질 수 있는 심각한 문제입니다.
C++에서의 이중 해제 위험 (코드 예시)
#include <iostream>
void process_data(int* data_ptr) {
if (data_ptr) {
// 일부 작업 수행...
delete data_ptr; // 여기서 메모리를 해제합니다.
}
}
int main() {
int* my_data = new int(10); // 힙에 정수 할당
process_data(my_data); // 첫 번째 해제 시도 (data_ptr이 해제)
// my_data는 이미 process_data에서 해제되었습니다.
// 다시 해제하려고 시도하면 '이중 해제'가 발생하여 런타임 오류로 이어질 수 있습니다.
// delete my_data; // 런타임 오류 (예: 이중 해제) 발생 가능성
std::cout << "C++: Double free potential." << std::endl;
return 0;
}
위 C++ 코드에서 my_data가 가리키는 메모리는 process_data 함수 내부에서 delete data_ptr;을 통해 해제됩니다. main 함수로 돌아와서 delete my_data;를 다시 호출하면 이미 해제된 메모리를 또다시 해제하려고 시도하게 되어 이중 해제가 발생합니다. C++에서는 std::unique_ptr이나 std::shared_ptr과 같은 스마트 포인터를 사용하여 이런 문제를 방지할 수 있지만, 일반 포인터를 사용할 경우 개발자의 주의가 필수적입니다.
Rust가 이중 해제를 막는 방법 (소유권 이동!)
fn process_data(data: Box<i32>) {
// Box<i32>는 힙에 할당된 데이터를 소유합니다.
// 함수 스코프를 벗어나면 Box가 가리키는 메모리가 자동으로 해제됩니다.
println!("Processing data: {}", *data);
// Rust에서는 명시적인 해제가 필요 없습니다.
} // data는 이 스코프를 벗어나면서 자동으로 Drop(해제)됩니다.
fn main() {
let my_data = Box::new(10); // Rust의 Box로 힙에 데이터 할당 및 소유
process_data(my_data); // my_data의 '소유권'이 process_data 함수로 '이동'합니다.
// my_data는 이제 유효하지 않습니다. (소유권이 이미 다른 곳으로 이동했기 때문)
// println!("My data: {:?}", my_data); // 컴파일 오류: borrow of moved value: `my_data`
// 따라서 명시적인 해제 또한 불가능하며, 이중 해제는 발생하지 않습니다.
println!("Rust: Double free prevented by ownership system.");
}
Rust는 소유권(Ownership) 시스템을 통해 이중 해제를 원천적으로 차단합니다. Box<i32>는 힙에 할당된 i32 값을 소유하는데, process_data(my_data)를 호출할 때 my_data의 소유권이 process_data 함수의 인자인 data로 이동(move)됩니다. 소유권이 이동한 후에는 main 함수 내의 my_data 변수는 더 이상 유효하지 않으므로, 접근하거나 다시 해제하려 하면 컴파일 오류가 발생합니다 (borrow of moved value).
process_data 함수가 종료될 때, data(Box<i32>)는 스코프를 벗어나면서 자동으로 Drop(해제)됩니다. Rust의 소유권 시스템은 각 메모리 조각에 대해 단 하나의 소유자만을 허용하며, 소유자가 스코프를 벗어나면 자동으로 메모리를 해제합니다. 이로 인해 이중 해제와 같은 문제를 컴파일 시점에 방지하거나, 안전한 디자인 패턴을 강제하여 런타임 오류의 가능성을 극적으로 줄여줍니다.
결론: 안전성과 성능, 두 마리 토끼를 잡는 Rust
C++는 저수준 제어와 최고 성능을 제공하지만, 개발자가 메모리 관리에 대한 전적인 책임을 져야 합니다. 이는 댕글링 포인터나 이중 해제와 같은 치명적인 메모리 오류로 이어질 수 있죠. 물론 C++11 이후 도입된 스마트 포인터(unique_ptr, shared_ptr) 등을 활용하면 이러한 위험을 상당 부분 줄일 수 있지만, 이는 개발자의 적극적인 노력과 정확한 이해를 요구합니다.
반면 Rust는 C++와 유사한 성능을 제공하면서도, 혁신적인 소유권 시스템과 빌림 검사기를 통해 컴파일 시점에 메모리 안전성 문제들을 적극적으로 탐지하고 방지합니다. 이는 "컴파일 시점에 더 많은 비용을 지불하여 런타임에 더 안전한 코드를 얻는다"는 Rust의 철학을 잘 보여줍니다.
결국 Rust는 C++의 강점인 저수준 제어 능력과 성능을 유지하면서도, 메모리 안전성과 동시성 안정성을 강력하게 보장하여 개발자가 런타임 오류 걱정 없이 더욱 견고하고 신뢰할 수 있는 소프트웨어를 만들 수 있도록 돕습니다. 여러분은 어떤 언어의 접근 방식이 더 매력적으로 느껴지시나요?