iOS & Swift

스택(Stack) vs 힙(Heap), 변수가 저장되는 곳 완벽 정리 (Swift 값 타입까지)

변수를 선언하면 그 값은 어디에 저장될까요.

이석우iOS Developer4분 읽기
스택(Stack) vs 힙(Heap), 변수가 저장되는 곳 완벽 정리 (Swift 값 타입까지) 대표 이미지

변수를 선언하면 그 값은 어디에 저장될까요.

“메모리요”라고 답하면 반은 맞고 반은 부족합니다. 메모리 안에서도 스택에 놓이는 값과 힙에 놓이는 값의 운명이 완전히 다르거든요.

스택 오버플로라는 에러 이름, 클래스와 구조체의 성능 차이, ARC가 왜 필요한지까지 — 전부 이 두 영역을 알아야 설명이 됩니다.

이 글에서는 스택과 힙이 각각 어떻게 동작하는지, 왜 속도 차이가 나는지, Swift에서는 뭐가 어디로 가는지 정리합니다.

스택은 접시 쌓기, 힙은 창고 관리. 속도 차이가 여기서 나옵니다
스택은 접시 쌓기, 힙은 창고 관리. 속도 차이가 여기서 나옵니다

핵심 요약부터 볼게요.

  1. 스택: 함수 호출과 함께 쌓이고 리턴과 함께 사라지는 자동 관리 영역. 빠르다
  2. 힙: 런타임에 원하는 크기로 빌려 쓰는 동적 영역. 유연하지만 관리 비용이 든다
  3. 속도 차이의 본질은 “할당·해제 방식”과 “정리를 누가 하느냐”
  4. Swift에서 구조체는 대체로 스택, 클래스 인스턴스는 힙에 산다

스택: 포개 놓은 접시

스택은 함수 호출 정보를 쌓는 영역입니다. 이름 그대로 접시 쌓기예요. 마지막에 올린 접시를 가장 먼저 내리는 LIFO(후입선출) 구조입니다.

함수를 호출하면 그 함수의 지역 변수, 매개변수, 리턴 주소가 한 덩어리(스택 프레임)로 쌓입니다. 함수가 리턴하면 그 프레임이 통째로 사라져요.

여기서 스택의 강점이 나옵니다.

할당이 빠릅니다. 스택 포인터를 위로 옮기면 끝이에요. 빈자리를 찾아다닐 필요가 없습니다.

해제도 공짜입니다. 함수가 끝나면 포인터를 되돌리기만 하면 됩니다. 누가 치워야 할지 고민할 필요가 없어요.

대신 제약이 있습니다. 크기가 컴파일 타임에 정해져야 하고, 함수가 끝나면 무조건 사라지며, 전체 크기 한도가 작습니다(보통 수 MB). 재귀 함수가 종료 조건 없이 자기를 계속 호출하면 프레임이 한도를 넘어버리는데, 그 에러가 바로 스택 오버플로입니다.


힙: 넓지만 관리가 필요한 창고

힙은 런타임에 필요한 만큼 빌려 쓰는 영역입니다. 크기를 미리 몰라도 되고 함수가 끝나도 살아남으며 공간도 훨씬 넓어요.

대신 관리 비용이 따라옵니다.

할당이 상대적으로 느립니다. 요청한 크기에 맞는 빈 공간을 찾아야 하고 여러 스레드가 동시에 할당을 요청하면 조율도 필요합니다.

해제를 누군가 책임져야 합니다. 함수가 끝나도 안 사라지니까, “이제 안 쓴다”를 누군가 판단해야 해요. C는 프로그래머가 직접 free를 호출하고 Java는 가비지 컬렉터가 주기적으로 청소하고 Swift는 ARC가 참조 카운트를 세다가 0이 되는 순간 해제합니다.

이 판단을 잘못하면 두 가지 사고가 납니다. 너무 늦게 해제하면 메모리 누수, 너무 일찍 해제하면 해제된 메모리 접근(댕글링 포인터)이죠.

스택 프레임의 참조가 힙의 실제 객체를 가리키는 구조예요
스택 프레임의 참조가 힙의 실제 객체를 가리키는 구조예요

Swift에서는 뭐가 어디로 갈까

Swift 공식 문서와 WWDC 세션에서 반복해서 강조하는 구도가 있습니다. 값 타입(구조체·열거형)은 스택에, 참조 타입(클래스)은 힙에 할당되는 게 기본이라는 거예요.

struct PointStruct { var x, y: Double }
class PointClass { var x = 0.0, y = 0.0 }

func run() {
    let a = PointStruct(x: 1, y: 2) // 스택 프레임 안에 통째로
    let b = PointClass()            // 힙에 할당, 스택엔 참조만
}

구조체 a는 스택 프레임에 값이 통째로 들어갑니다. 함수가 끝나면 프레임과 함께 사라지니 참조 카운팅도 필요 없어요.

클래스 인스턴스 b는 힙에 자리 잡고 스택에는 그 주소만 놓입니다. 여러 곳에서 참조할 수 있으니 ARC가 카운트를 세야 하고요.

애플이 “구조체를 우선 고려하라”고 권하는 성능 쪽 근거가 이겁니다. 힙 할당, 참조 카운팅, 잠금 비용이 통째로 빠지거든요.

다만 “구조체 = 무조건 스택”은 아닙니다. 클래스의 프로퍼티로 들어간 구조체는 그 클래스와 함께 힙에 살고 String이나 Array 같은 타입은 겉은 구조체지만 실제 데이터 버퍼를 힙에 두고 있어요. “구조체는 스택에 놓일 수 있는 조건을 갖췄다” 정도로 이해하는 게 정확합니다.

구조체는 스택에 통째로, 클래스는 힙에 본체를 두고 참조만 스택에 놓입니다
구조체는 스택에 통째로, 클래스는 힙에 본체를 두고 참조만 스택에 놓입니다

면접 단골 꼬리 질문

“왜 스택이 힙보다 빠른가요?” — 스택은 포인터 이동만으로 할당·해제가 끝나지만, 힙은 빈 공간 탐색과 해제 관리(참조 카운팅·GC)가 필요하기 때문입니다.

“스택 오버플로는 왜 나나요?” — 스택 크기 한도는 작은데, 깊은 재귀나 거대한 지역 변수로 프레임이 한도를 넘으면 발생합니다.

“지역 변수는 무조건 스택인가요?” — 아니요. 지역 변수라도 참조 타입이면 본체는 힙에 있습니다. 스택에 놓이는 건 참조뿐이에요.


정리

  • 스택은 함수 호출 단위로 자동으로 쌓이고 사라지는 영역. 포인터 이동만으로 관리되어 빠르다
  • 힙은 런타임에 크기를 정해 빌리는 영역. 유연하지만 할당 탐색과 해제 관리 비용이 든다
  • 해제 책임: 스택은 자동, 힙은 언어마다 다르다 (C는 수동, Java는 GC, Swift는 ARC)
  • 스택 오버플로는 깊은 재귀 등으로 스택 한도를 넘을 때 나는 에러
  • Swift에서 구조체는 대체로 스택, 클래스는 힙. 이게 값 타입 우선 권장의 성능 근거
  • 단, 클래스 안의 구조체는 힙에 살고, String·Array는 내부 버퍼가 힙에 있다