포스트

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

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

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

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

🗺️ 프로그래밍 언어 개념 로드맵의 한 편

한 줄 요약

프로그램이 동적으로 만든 객체와 자원의 수명을 누가 추적하고 언제 정리할지를 어떤 규칙으로 결정하느냐에 따라 메모리 관리 모델이 갈린다. 큰 흐름은 개발자가 직접 관리하는 방식, 런타임 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, 이동 의미론
GoGC도달 가능성 기반 회수, escape analysis
Rust정적 소유권move, 빌림(&/&mut), 수명(lifetime), Drop

기억 흐름

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가 회수하지만 파일·소켓·락처럼 외부 자원은 보통 명시적으로 닫거나 해제해야 한다.

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