메모리 관리 모델 — 수동 vs GC vs 소유권
힙 메모리와 자원의 수명을 누가 어떻게 관리하는가. 수동 관리(C/C++)·가비지 컬렉션(Go/Java)·소유권(Rust)이라는 세 모델을 안전성·성능·예측성 축으로 비교하고, C++의 RAII와 스마트 포인터, Go의 GC, Rust의 소유권/빌림이 같은 문제를 어떻게 다르게 푸는지 정리한다.
난이도 중급 · 선행 스택/힙 개념
🗺️ 프로그래밍 언어 개념 로드맵의 한 편
한 줄 요약
프로그램이 동적으로 만든 객체와 자원의 수명을 누가 추적하고 언제 정리할지를 어떤 규칙으로 결정하느냐에 따라 메모리 관리 모델이 갈린다. 큰 흐름은 개발자가 직접 관리하는 방식, 런타임 GC가 도달 가능성을 추적하는 방식, 컴파일러가 소유권·빌림 규칙을 정적으로 검사하는 방식이다.
왜 이게 언어를 가르는 핵심인가
동적으로 만든 객체나 자원은 언젠가 정리해야 한다. 그런데:
- 안 풀면 → 메모리 누수(leak)
- 두 번 풀면 → double free
- 푼 걸 또 쓰면 → use-after-free
이 문제를 누가 추적하고 어떤 시점에 정리할 수 있게 하느냐가 언어의 성격을 크게 좌우한다. C/C++은 수명과 해제 시점을 개발자가 직접 다룰 수 있고, Go·Java 같은 GC 언어는 런타임이 도달 가능성을 추적해 회수한다. Rust는 런타임 GC 대신 컴파일 타임의 소유권·빌림 규칙으로 잘못된 수명 관계를 막고, 값이 수명을 다하면 생성된 Drop 코드가 런타임에 정리를 수행한다.
세 가지 모델
① 명시적 관리 — 개발자가 책임 (C, C++)
malloc/free, new/delete 같은 저수준 API에서는 개발자가 획득과 해제를 직접 맞춘다.
1
2
3
int* p = new int(42);
// ... 사용 ...
delete p; // 안 하면 누수, 두 번 하면 잘못된 해제
- 장점: 해제 시점과 자원 수명을 세밀하게 제어할 수 있다.
- 단점: 수명 관계를 잘못 관리하면 누수·double free·use-after-free가 생긴다.
다만 현대 C++ 전체를 new/delete 수동 관리 모델로만 보면 부정확하다. 실제 C++에서는 RAII와 값 타입, unique_ptr·shared_ptr 같은 소유권 타입으로 자원 수명을 객체 수명에 묶는 것이 기본적인 설계다.
② 가비지 컬렉션 — 런타임이 도달 가능성을 추적 (Go, Java, Python, JavaScript)
개발자는 일반적인 객체를 직접 free하지 않는다. 런타임의 GC가 루트에서 더 이상 도달할 수 없는 객체를 찾아 회수한다.
1
2
3
p := new(int)
*p = 42
// 명시적 delete 없음. 더 이상 도달할 수 없게 되면 GC 회수 대상이 된다.
- 장점: 일반적인 객체 수명에서 use-after-free·double free 위험을 크게 줄인다.
- 단점: 추적·회수에 CPU와 메모리가 필요하고, 구현에 따라 지연 시간에 영향을 줄 수 있다.
GC라고 해서 모든 자원이 자동으로 해결되는 것은 아니다. 파일·소켓·락처럼 메모리 외 자원은 여전히 명시적으로 닫거나 해제해야 한다.
③ 소유권 — 컴파일러가 수명 관계를 정적으로 검사 (Rust)
Rust는 런타임 GC 없이 소유권·이동·빌림·수명 규칙을 컴파일 타임에 검사한다. 잘못된 참조 관계는 실행 전에 거부된다.
1
2
3
4
let s = String::from("hi");
let t = s; // 소유권 이동
// println!("{}", s); // 컴파일 에러: 이동한 값 사용
// t가 스코프를 벗어나면 Drop이 실행되어 자원이 정리된다.
중요한 구분은 이렇다.
1
2
3
4
5
6
컴파일러
→ 소유권·빌림·수명 관계가 안전한지 검사
→ 필요한 drop 지점을 결정해 코드 생성
런타임
→ 실제 실행 중 해당 지점에서 Drop/해제 수행
즉 컴파일러가 메모리를 직접 해제하는 것이 아니라, 안전한 수명 규칙을 정적으로 강제하고 정리 코드를 생성한다.
- 장점: GC 없이도 많은 메모리 안전 오류를 컴파일 단계에서 막는다.
- 단점: 소유권·빌림·수명 관계를 타입과 코드 구조에 드러내야 하므로 학습 부담이 있다.
한눈에 비교
| C/C++의 명시적 관리 | GC 언어 | Rust 소유권 | |
|---|---|---|---|
| 수명 추적 주체 | 개발자·타입 설계 | 런타임 GC | 컴파일러의 정적 규칙 + 생성된 Drop 코드 |
| 일반 객체 회수 | 명시적 해제 또는 RAII | 도달 불가 시 GC | 소유자가 수명을 다할 때 Drop |
| use-after-free 방지 | 설계·RAII·스마트 포인터에 달림 | 일반 GC 객체에서는 구조적으로 방지 | 빌림 검사로 컴파일 단계에서 방지 |
| 런타임 GC | 없음 | 있음 | 없음 |
| 해제 시점 예측 | RAII/명시 해제는 높음 | GC 회수 시점은 구현 의존 | 소유자 수명 기준으로 결정적 |
RAII — C++과 Rust가 만나는 지점
C++의 RAII(Resource Acquisition Is Initialization)는 자원의 수명을 객체의 수명에 묶는다. 객체가 스코프를 벗어나면 소멸자가 자동으로 자원을 정리한다.
1
2
3
4
{
auto p = std::make_unique<int>(42);
// ... 사용 ...
} // unique_ptr 소멸자 실행 → delete
unique_ptr은 복사가 금지되고 이동만 허용되는 단일 소유 타입이다. 이 점은 Rust의 기본 소유권 모델과 매우 닮았다.
하지만 둘을 같은 것으로 보면 안 된다.
1
2
3
4
5
6
7
8
C++
- 소유권 표현을 타입과 설계로 선택한다.
- unique_ptr 자체의 단일 소유·복사 금지는 타입 시스템이 강제한다.
- raw pointer, shared_ptr, 값 타입 등 다른 모델도 함께 쓸 수 있다.
Rust
- 모든 일반 값에 소유권 규칙이 기본 적용된다.
- 빌림과 aliasing 규칙까지 언어 차원에서 정적으로 검사한다.
따라서 unique_ptr은 단순한 관례가 아니라 강제되는 소유권 타입이고, Rust의 차이는 그 원리를 언어 전반의 기본 규칙으로 확장했다는 데 있다.
언어별 정리
| 언어 | 기본 모델 | 핵심 수단 |
|---|---|---|
| C++ | RAII + 명시적 소유권 설계 | 값 타입, 소멸자, unique_ptr/shared_ptr, 이동 의미론 |
| Go | GC | 도달 가능성 기반 회수, escape analysis |
| Rust | 정적 소유권 | move, 빌림(&/&mut), 수명(lifetime), Drop |
- C++:
new/delete를 직접 맞추기보다 RAII와 소유권 타입으로 수명을 표현한다. → 이동 시맨틱과 스마트 포인터 · 클래스와 자원 관리(RAII) - Go: 메모리 해제는 GC가 담당하지만 파일·소켓 같은 외부 자원 수명은 별도로 관리한다. → Go 학습 로드맵
- Rust: 값은 소유자를 가지며, 다른 코드가 일시적으로 접근할 때는 빌림 규칙을 따른다. → 불변성 — const·mut·val
기억 흐름
1
2
3
4
5
6
7
8
9
10
11
동적 자원은 언젠가 정리해야 한다
↓
누가 수명 관계를 추적하는가?
↓
C/C++: 개발자와 타입 설계
GC 언어: 런타임
Rust: 컴파일러의 정적 소유권 규칙
↓
실제 정리는 결국 런타임에서 일어난다
↓
차이는 "안전한 정리 시점을 어떻게 결정·강제하느냐"
스스로 점검
1. Rust에서 컴파일러가 직접 메모리를 해제하는가?
답
아니다. 컴파일러는 소유권·빌림·수명 규칙을 검사하고 필요한 Drop 코드를 생성한다. 실제 정리는 프로그램이 실행될 때 해당 지점에서 수행된다.
2. C++ unique_ptr과 Rust 소유권은 무엇이 같고 무엇이 다른가?
답
둘 다 단일 소유와 이동을 표현한다는 점은 비슷하다. unique_ptr의 복사 금지는 C++ 타입 시스템이 강제한다. 다만 C++에서는 이 소유권 타입을 선택해서 사용하고, Rust에서는 소유권과 빌림 규칙이 언어 전반의 기본 의미론으로 적용된다.
3. GC가 있으면 자원 관리가 모두 자동인가?
답
아니다. 일반 객체 메모리는 GC가 회수하지만 파일·소켓·락처럼 외부 자원은 보통 명시적으로 닫거나 해제해야 한다.