포스트

메모리 관리 모델 — 수동 vs GC vs 소유권

힙 메모리를 언제 잡고 언제 푸느냐를 누가 결정하는가. 수동 관리(C/C++)·가비지 컬렉션(Go/Java)·소유권(Rust)이라는 세 모델을 안전성·성능·예측성 축으로 비교하고, C++의 RAII와 스마트 포인터, Go의 GC, Rust의 소유권/빌림이 같은 문제를 어떻게 다르게 푸는지 정리한다.

메모리 관리 모델 — 수동 vs GC vs 소유권

난이도 중급 · 선행 스택/힙 개념

한 줄 요약

프로그램이 힙 메모리를 언제 잡고 언제 푸는지를 누가 결정하느냐로 언어가 갈린다. 답은 세 가지다 — 개발자(수동), 런타임(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++수동 + RAIIunique_ptr/shared_ptr, 소멸자, 이동 시맨틱
GoGCescape analysis로 스택/힙 자동 결정, new/&
Rust소유권move 기본, 빌림(&/&mut), 수명(lifetime), Drop

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는 컴파일러가 소유권 규칙을 강제·검증한다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.