메모리 관리 모델 — 수동 vs GC vs 소유권
힙 메모리를 언제 잡고 언제 푸느냐를 누가 결정하는가. 수동 관리(C/C++)·가비지 컬렉션(Go/Java)·소유권(Rust)이라는 세 모델을 안전성·성능·예측성 축으로 비교하고, C++의 RAII와 스마트 포인터, Go의 GC, Rust의 소유권/빌림이 같은 문제를 어떻게 다르게 푸는지 정리한다.
난이도 중급 · 선행 스택/힙 개념
한 줄 요약
프로그램이 힙 메모리를 언제 잡고 언제 푸는지를 누가 결정하느냐로 언어가 갈린다. 답은 세 가지다 — 개발자(수동), 런타임(GC), 컴파일러(소유권). C++·Go·Rust의 성격 차이 대부분이 여기서 나온다.
왜 이게 언어를 가르는 핵심인가
힙에 할당한 메모리는 언젠가 풀어야 한다. 그런데:
- 안 풀면 → 메모리 누수(leak)
- 두 번 풀면 → double free (크래시/보안 취약점)
- 푼 걸 또 쓰면 → use-after-free = 댕글링 포인터 (UB, 보안 취약점의 단골)
이 셋을 누가, 언제 책임지느냐가 언어 철학의 뿌리다. C++이 자유와 성능을 주는 대신 책임을 개발자에게 지운다면, Go는 편의를 위해 런타임에 맡기고, Rust는 그 책임을 컴파일 타임 규칙으로 옮겨 “안전 + 무런타임비용”을 동시에 노린다.
세 가지 모델
① 수동 관리 — 개발자가 책임 (C, C++)
new/delete(또는 malloc/free)로 개발자가 직접 잡고 푼다.
1
2
3
int* p = new int(42);
// ... 사용 ...
delete p; // ★ 안 하면 누수, 두 번 하면 double free
- 장점: 최고의 성능과 예측 가능성. 언제 할당·해제되는지 정확히 안다. 런타임 오버헤드 0.
- 단점: 실수하면 곧장 UB. 큰 코드베이스에서 수명 추적은 극히 어렵다.
그래서 현대 C++은 이걸 손으로 하지 않는다. RAII로 자동화한다(아래).
② 가비지 컬렉션 — 런타임이 책임 (Go, Java, Python, JS)
개발자는 할당만 하고, 해제는 신경 쓰지 않는다. 런타임의 GC가 “더 이상 아무도 참조하지 않는” 객체를 주기적으로 찾아 회수한다.
1
2
3
p := new(int) // 또는 &SomeStruct{}
*p = 42
// delete 없음. 아무도 p를 참조 안 하게 되면 GC가 알아서 회수
- 장점: use-after-free·double free가 원천 불가능. 개발 생산성이 높다.
- 단점: GC 일시정지(pause) — 회수 작업 동안 지연이 생길 수 있어 예측성이 떨어진다. 런타임·메모리 오버헤드가 있다.
③ 소유권 — 컴파일러가 책임 (Rust)
런타임 GC 없이, 컴파일 타임에 소유권/빌림 규칙으로 안전을 검증한다. 규칙을 어기면 컴파일이 안 된다.
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
let s = String::from("hi"); // s가 이 String의 소유자
let t = s; // 소유권이 t로 이동(move). s는 이제 무효
// println!("{}", s); // ★ 컴파일 에러 — 이미 이동된 값
``` // t가 스코프를 벗어나면 자동 해제 (GC 없이)
- **장점**: GC 없이(런타임 비용 0) 메모리 안전을 **보장**. C++ 수준 성능 + 안전.
- **단점**: 학습 곡선이 가파르다. 초반엔 "빌림 검사기(borrow checker)와 싸운다"는 말이 나온다.
## 한눈에 비교
| | 수동 (C++) | GC (Go/Java) | 소유권 (Rust) |
|---|---|---|---|
| 누가 해제 결정 | 개발자 | 런타임 | 컴파일러(규칙) |
| 안전성 | 실수 시 UB | 안전 | 안전(컴파일 보장) |
| 런타임 비용 | 없음 | GC pause·메모리 | 없음 |
| 예측 가능성 | 높음 | 낮음(GC 시점) | 높음 |
| 개발 부담 | 높음 | 낮음 | 중간(초반 높음) |
| 대표 실수 | 누수·댕글링 | (거의 없음) | 컴파일 통과 고생 |
> "정답"은 없다. 실시간·임베디드·게임 엔진은 예측성 때문에 수동/소유권을, 서버·앱 개발은 생산성 때문에 GC를 택하는 식으로 **트레이드오프**다.
## RAII — 수동과 소유권을 잇는 다리
C++이 수동 관리의 위험을 길들인 방법이 **RAII(Resource Acquisition Is Initialization)**다. 핵심은 **자원의 수명을 객체의 수명에 묶는 것**이다. 객체가 스코프를 벗어나면 소멸자가 **자동으로** 자원을 해제한다.
```cpp
{
auto p = std::make_unique<int>(42); // 획득
// ... 사용 ...
} // ★ 스코프 끝 → unique_ptr 소멸자가 자동 delete. 누수 불가능
unique_ptr— 소유자가 하나. 복사 불가, 이동만 가능. Rust 소유권과 발상이 같다.shared_ptr— 참조 카운팅으로 공유 소유. 마지막 참조가 사라지면 해제.
여기서 중요한 연결: Rust의 소유권은 RAII를 언어 차원에서 강제하고, 컴파일러가 검증까지 하는 것이다. C++의 unique_ptr은 “규율로 지키는 관례”, Rust의 소유권은 “컴파일러가 강제하는 규칙”이라는 차이다.
언어별 정리
| 언어 | 모델 | 핵심 도구 |
|---|---|---|
| C++ | 수동 + RAII | unique_ptr/shared_ptr, 소멸자, 이동 시맨틱 |
| Go | GC | escape analysis로 스택/힙 자동 결정, new/& |
| Rust | 소유권 | move 기본, 빌림(&/&mut), 수명(lifetime), Drop |
- C++:
new/delete를 직접 쓰지 말고 스마트 포인터로. → 이동 시맨틱과 스마트 포인터 · 클래스와 자원 관리(RAII) - Go: 포인터를 쓰되 해제는 GC가. 값 의미론이 기본이라 복사가 잦다. → Go 학습 로드맵
- Rust: 값은 기본적으로 move되고, 빌려 쓸 땐
&로 참조. 소유권·수명이 언어의 중심축.
C++
unique_ptr을 이해했다면 Rust 소유권의 절반은 이미 아는 셈이다. “소유자는 하나, 이동하면 원본 무효”가 정확히 같은 개념이다.
스스로 점검
1. use-after-free를 세 모델은 각각 어떻게 막는가?
답
- 수동(C++): 못 막는다. 개발자가 규율로 피해야 하며, RAII/스마트 포인터로 위험을 줄인다.
- GC(Go): 참조가 남아있는 한 회수하지 않으므로 원천 불가능.
- 소유권(Rust): 이동된/해제될 값을 다시 쓰면 컴파일 에러. 실행 전에 차단.
2. GC의 대가(단점)는 무엇인가?
답
예측 불가능성과 런타임 비용. GC가 도는 시점에 일시정지(pause)가 생겨 지연이 튈 수 있고, 추적·회수에 CPU와 메모리를 쓴다. 실시간성이 중요한 영역에서 GC 언어를 꺼리는 이유다.
3. C++ unique_ptr과 Rust 소유권의 관계는?
답
발상이 같다 — 소유자는 하나, 복사 불가, 이동만 가능, 소유자가 사라지면 해제. 차이는 강제력이다. C++은 개발자가 unique_ptr을 “선택해서” 쓰는 관례이고, Rust는 컴파일러가 소유권 규칙을 강제·검증한다.