스택과 힙 비교

프로그램이 실행될 때 데이터는 메모리에 적재되어야 합니다. 데이터가 차지하는 공간과 저장 위치는 데이터 타입에 따라 결정됩니다. 프로그램 구동 중 데이터를 보관하기 위해 두 가지 메모리 영역이 사용됩니다: 스택(Stack), 힙(Heap).

스택(Stack)

스택은 연속된 메모리 배열로, 후입선출(LIFO) 데이터 구조 규칙을 따르며 총 세 가지 종류의 데이터를 저장합니다.

  • 일부 타입 변수에 담긴 값
  • 프로그램의 현재 실행 문맥
  • 메서드에 전달되는 매개변수
스택은 동전 통과 유사한 구조로, 한쪽 끝에서만 데이터를 넣고 꺼낼 수 있습니다.

스택의 메모리 할당과 해제는 시스템이 자동으로 처리하므로 개발자가 직접 관리할 필요가 없습니다. 스택의 내부 동작 원리를 이해하면 프로그램 실행 흐름을 파악하고 C# 공식 문서를 읽기 훨씬 수월해집니다.

스택 핵심 특징

  • 데이터 추가 및 삭제는 오직 스택 최상단에서만 가능
  • 스택 최상단에 데이터를 저장하는 행위: 푸시(Push)
  • 스택 최상단에서 데이터를 꺼내는 행위: 팝(Pop)

스택 메모리 특성

  • 메모리 할당·해제 속도가 매우 빠르며 연속된 메모리 공간을 사용
  • 생명 주기가 메서드 호출에 묶여있고, 메서드 실행이 끝나면 스택 프레임이 자동으로 소멸
  • 저장 대상: 값 타입 변수, 메서드 매개변수, 지역 변수, 실행 문맥

힙의 작동 방식을 더 잘 이해하기 위해 간단한 예시 코드를 살펴보겠습니다.

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        int b = 20;
        int sum = Add(a, b);
    }

    static int Add(int x, int y)
    {
        return x + y;
    }
}
Code language: JavaScript (javascript)

이 예시는 로직이 아주 단순합니다. 프로그램 진입점인 Main 함수 첫 줄에서 int 타입 변수 a를 만들고, 두 번째 줄에 변수 b를 선언합니다. 세 번째 줄에서는 Add 함수를 호출해 a와 b의 값을 전달하고 함수 내부로 진입합니다. x에는 a의 값이, y에는 b의 값이 전달되며 함수는 x+y 연산 결과인 30을 호출한 지점으로 반환해 sum에 대입합니다. Add 함수 실행이 완료되면 x, y가 팝 처리되고 sum의 값은 30으로 바뀝니다. Main 함수가 끝나면 프로그램이 종료됩니다. 아래에서 스택에서 이 과정이 진행되는 순서를 단계별로 분석해보겠습니다.

프로그램 실행 흐름과 스택 푸시·팝 과정

변수 a를 생성할 때 내장 기본 타입은 스택에 직접 할당되며 푸시 작업이 진행됩니다.
두 번째 코드를 실행해 값이 20인 int 변수 b를 생성합니다. 먼저 푸시된 데이터는 스택 바닥에 위치하고 나중에 들어온 데이터는 스택 최상단에 쌓입니다.
세 번째 코드에서는 등호 우측의 Add(a,b)부터 먼저 실행해 함수를 호출합니다. a와 b의 값만 x, y에 복사될 뿐 a와 x는 서로 완전히 독립된 변수로 서로 영향을 주지 않습니다.
이 단계에서는 x+y 연산만 수행하고 결과를 반환할 뿐 스택에 영구 저장하지 않습니다. 연산 결과 30은 호출 지점으로 전달돼 int sum = 30과 같은 형태로 할당됩니다. return x+y가 실행되고 Add 함수가 종료되면 함수 내 모든 지역 변수는 팝 처리되며 해당 변수가 차지하던 메모리 공간이 초기화되어 이후 재사용할 수 있습니다.
스택의 가장 위쪽 지점을 스택 탑(Top)이라고 부르며 팝 작업을 거치면 탑 위치가 아래로 이동합니다.
Add 함수 실행이 끝나면 x, y는 함수 내 지역 매개변수이므로 순서대로 팝되어 스택 가용 공간을 확보하고 시스템 구동을 원활하게 유지합니다.
팝 처리된 x, y는 메모리에서 사라지고 Add 함수가 반환한 값 30이 호출 지점으로 전달돼 sum에 저장됩니다.
int sum = 30 구문을 실행하면 다시 푸시 작업이 발생하며 값 30의 sum 변수가 스택 최상단에 적재됩니다. 이 구문이 끝나면 Main 함수가 종료될 준비가 되고 sum, b, a 등 Main 소속 지역 변수들은 차례로 팝되어 소멸합니다.
팝은 언제나 스택 최상단부터 진행됩니다. 스택은 입출구가 단 하나뿐인 통로이므로 데이터를 넣고 꺼낼 때 같은 한쪽 끝을 이용해야 합니다. 그래서 Main 함수 내 변수가 소멸되는 순서는 생성 순서와 반대로 sum → b → a 순서로 처리됩니다.
변수 b가 팝되어 소멸되고 스택 탑 포인터가 자동으로 아래로 이동합니다.
모든 변수가 팝 처리되고 마지막 변수 a까지 메모리에서 제거되었습니다. 엄밀히 말하면 Main 함수에는 또 다른 지역 변수가 존재하는데 Main(string[] args)의 문자열 배열 args로, 콘솔 실행 시 입력하는 명령줄 인자를 받는 역할입니다. 이 배열은 메모리 상 두 구역으로 나뉘어 저장되며 이후 다룰 힙과 연관된 내용이므로 본 예시에서는 별도로 설명하지 않겠습니다.

독자분들은 한 가지를 기억해두시면 좋습니다. 함수가 실행될 때 변수는 위에서 아래 순서로 푸시되므로 진행 순서는 a → b → Add 함수 진입 → x → y 입니다. Add 함수가 종료되면 x, y가 팝되고 sum이 푸시되며 Main 함수가 끝날 때는 생성 순서와 반대로 sum, b, a 순서로 팝되어 사라집니다.


힙(Heap)

힙은 넓은 메모리 영역으로 객체를 저장할 메모리 블록을 동적으로 할당하는 공간입니다. 스택과 달리 힙에 저장된 데이터는 아무 순서로나 추가하거나 삭제할 수 있습니다.

프로그램에서 힙 내 객체를 직접 삭제할 수 없으며 CLR(공통 언어 런타임)의 가비지 컬렉터 GC가 참조가 끊겨 접근할 수 없는 고립된 객체를 자동으로 정리합니다. 이 덕분에 다른 언어에서 필수로 진행하는 수동 메모리 해제로 인한 각종 오류를 피할 수 있습니다.

힙은 넓은 책상과 같아서 아무 곳에나 물체를 놓을 수 있습니다. 반면 앞서 설명한 스택은 동전 통처럼 한쪽 끝에서만 데이터를 주고받을 수 있습니다.

힙은 넓은 책상으로 물체 배치에 제약이 없습니다. 개발자가 힙 객체를 직접 지울 필요 없이 GC(가비지 컬렉터)가 관리합니다. 객체를 참조하는 코드가 전혀 없어지면 GC가 주기적으로 객체를 파괴하고 메모리를 회수합니다. 이는 개발자의 업무 부담을 크게 줄이고 메모리 해제 누락으로 인한 프로그램 다운 현상을 막아줍니다. C++ 같은 언어는 개발자가 직접 힙 메모리를 해제해야 합니다.

GC 회수 순서

  1. 프로그램이 힙에 객체 3개를 생성하고 유효한 참조를 유지
  2. 프로그램이 한 객체 사용을 중단하며 해당 객체의 참조가 사라짐
  3. GC가 참조가 없는 고립 객체를 감지하고 차지하던 메모리를 해제
  4. 회수가 완료되면 해제된 메모리는 새 객체 할당에 재사용 가능

힙 메모리 특성

  • 메모리 공간이 비연속적이며 할당 속도가 스택보다 느림
  • 생명 주기가 메서드 범위에 구속되지 않고 GC가 통합 관리
  • 저장 대상: 참조 타입 인스턴스(클래스, 배열, 델리게이트 등). 스택에는 오직 객체의 메모리 주소만 저장

더 직관적으로 이해하기 위해 실제 예시를 살펴보겠습니다. 앞서 설명했듯 지역 기본 타입 변수는 스택에 저장되지만 사용자가 직접 정의한 타입은 대부분 힙에 할당됩니다. 아래 코드를 참고하세요.

class Program
{
    static void Main(string[] args)
    {
        int a = 10;
        int b = 20;
        Student tom = new Student("Tom", 12);
        Student lucy = new Student("Lucy", 12);
    }

}

class Student
{
    //멤버 변수
    private string name;

    private int age;

    //생성자, 뒤에서 자세히 설명할 예정
    public Student(string name, int age)
    {
        this.name = name;
        this.age = age;
    }

    //멤버 메서드
    public void DisplayInfo()
    {
        Console.WriteLine($"Name: {name}, Age: {age}");
    }
}   
Code language: JavaScript (javascript)

아래 그림은 Main 함수 내 네 개 변수 a, b, tom, lucy의 메모리 저장 구조를 보여줍니다. 스택 설명과 동일하게 위에서 아래로 코드가 실행되며 a가 먼저 푸시되고 그 다음 b가 푸시됩니다. a와 b의 값은 스택에 직접 기록됩니다.

기본 타입 변수는 스택에 직접 저장됩니다.
직접 정의한 Student 타입의 실제 데이터는 힙에 저장되고, 스택에는 힙 내 데이터의 메모리 주소만 기록됩니다.
예를 들어 tom의 데이터는 힙의 00001 영역에 위치하며 스택에는 00001이라는 주소를 저장해 나중에 데이터를 찾을 수 있게 합니다.

Main 함수 실행이 완료되면 스택 내 lucy: 01010 주소 정보가 팝되어 소멸합니다. 다만 삭제되는 건 주소값일 뿐 힙에 존재하는 실제 객체 데이터는 그대로 유지됩니다. 이 시점에서 힙의 객체는 스택 변수에 기록된 모든 주소 참조를 잃게 됩니다.

힙의 01010 영역에 저장된 Lucy 객체는 스택에 해당 주소를 기록하는 변수가 사라져 참조가 없는 고립 객체가 됩니다. 가비지 컬렉터 GC는 적절한 시점에 이 객체를 파괴합니다.
스택에 저장된 참조 주소가 팝되면서 힙의 Lucy 객체는 어디에서도 참조할 수 없는 상태가 됩니다. GC는 임의의 시점에 해당 객체의 메모리를 정리하고 공간을 재사용 가능하게 만듭니다. 아직 참조하는 곳이 하나라도 남아있다면 GC는 객체를 파괴하지 않습니다.

메모리 영역 설명

스택과 힙은 소프트웨어 단위로 구분한 메모리 영역으로 물리적인 하드웨어 분할이 아닙니다.

스택, 힙은 소프트웨어에서 논리적으로 나눈 메모리 구역일 뿐 물리 RAM 하드웨어에 ‘스택 영역 / 힙 영역’이라고 표시된 전용 모듈은 존재하지 않습니다.

물리 RAM 메모리는 단일 메모리 칩으로 연속된 물리 주소만 가질 뿐, 스택과 힙을 구분하는 표식 자체가 없습니다

운영체제는 실행 중인 각 프로세스에 독립적인 가상 메모리 공간을 할당하고, 이 가상 메모리를 미리 네 가지 논리 세그먼트로 분할합니다.

코드 세그먼트: 컴파일된 프로그램 명령어를 저장

전역 정적 세그먼트: 정적 변수, 상수 저장

스택 세그먼트: 프로그램 메인 스레드, 서브 스레드가 각자 독립적인 스택 메모리를 보유

힙 세그먼트: 동적 메모리 할당에 사용하는 거대한 공유 메모리 풀

여기서 말하는 스택과 힙은 운영체제가 프로세스마다 할당한 논리 영역일 뿐 하드웨어 레벨에서 격리되지 않습니다.

스택과 힙 비교

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다