← SEARCHABLE ARCHIVE
Computer Science · 2026-08-09

스택과 힙은 무엇이 다를까?

호출 프레임, 동적 할당, 객체 수명과 소유권 이해하기

INTERACTIVE READING MODE →

본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.

프로그램의 메모리를 설명할 때 스택과 힙이라는 말을 자주 사용한다. 둘의 핵심 차이는 단순히 빠르고 느린 위치가 아니라 메모리의 수명과 관리 방식에 있다.

스택에는 호출 기록이 쌓인다

함수를 호출하면 반환할 위치, 일부 매개변수, 지역 변수 등을 관리하는 호출 프레임이 만들어질 수 있다.

main 호출 프레임
└─ Update 호출 프레임
   └─ CalculateDamage 호출 프레임

안쪽 함수가 끝나면 그 프레임이 먼저 정리된다. 가장 나중에 들어온 것이 먼저 나가는 LIFO 구조이기 때문에 스택이라는 이름을 사용한다.

int CalculateDamage(int attack, int defense)
{
    int damage = attack - defense;
    return damage > 0 ? damage : 1;
}

damage는 일반적으로 함수 호출이 끝날 때 더 이상 사용할 수 없다. 하지만 모든 지역 변수가 반드시 메모리의 스택에 놓이는 것은 아니다. 컴파일러는 값을 레지스터에 두거나 최적화로 없앨 수 있으며 구체적인 호출 프레임 배치는 ABI와 최적화 옵션에 따라 달라진다.

힙은 수명을 직접 결정한다

함수 하나의 실행보다 오래 살아야 하거나 크기가 실행 중 결정되는 데이터는 동적 저장 영역에 둘 수 있다.

#include <memory>
#include <string>

struct Player
{
    std::string Name;
};

std::unique_ptr<Player> CreatePlayer()
{
    return std::make_unique<Player>(Player{"Rose"});
}

Player 객체의 수명은 CreatePlayer의 호출 프레임과 묶이지 않는다. 반환된 unique_ptr이 소유권을 가지고 마지막 소유자가 사라질 때 객체를 정리한다.

힙 할당이 일어날 때마다 운영체제에 직접 메모리를 요청하는 것은 아니다. 일반적인 메모리 할당기는 운영체제에서 큰 메모리 구역을 받은 뒤 작은 요청에 나눠 사용하고, 해제된 블록을 재사용한다.

포인터는 객체의 수명을 늘리지 않는다

int* Wrong()
{
    int value = 10;
    return &value; // 함수가 끝나면 value의 수명도 끝난다.
}

주소를 반환했다고 지역 변수의 수명이 연장되지는 않는다. 수명이 끝난 객체를 가리키는 포인터를 역참조하면 잘못된 메모리 접근이 된다.

반대로 힙 객체도 해제 책임이 불분명하면 누수가 생긴다. C++에서는 가능한 한 unique_ptr, shared_ptr, 표준 컨테이너처럼 소유권을 표현하는 타입을 사용하고 직접 new와 delete를 흩뿌리지 않는 편이 안전하다.

스택 오버플로와 힙 단편화

끝없이 이어지는 재귀 호출이나 지나치게 큰 지역 배열은 제한된 스택 공간을 소모해 스택 오버플로를 만들 수 있다.

void RecurseForever()
{
    RecurseForever();
}

힙에서는 크기가 다른 할당과 해제가 반복될 때 남은 공간이 잘게 나뉘는 단편화가 생길 수 있다. 메모리가 남아 있어도 원하는 크기의 연속 블록을 얻기 어려워지거나 캐시 효율이 낮아질 수 있다.

스택이 항상 빠르고 힙이 항상 느리다고 외우면 중요한 부분을 놓친다. 객체의 크기와 수명, 소유자, 할당 빈도, 접근 패턴을 먼저 정하고 실제 성능은 프로파일러로 확인해야 한다.