[Swift] Memory Structure

o_jooon_·2024년 4월 23일
0

swift

목록 보기
8/8
post-thumbnail

이번 포스팅에서 알아 볼 내용은 iOS의 메모리 구조에 관한 내용입니다.
메모리는 일반적으로 힙(Heap), 스택(Stack), 코드(Code), 데이터(Data) 영역으로 나뉘어지는데,
iOS에서 화면을 보여줄 때 어떤 식으로 메모리에 할당되는지 알아보겠습니다.
메모리 관련 포스팅은 현재 포스팅이 메모리 구조, 다음 포스팅은 메모리 관리 방법에 대해 알아 볼 예정입니다.


메모리의 구조

먼저 메모리가 어떤 구조로 이루어져 있는지 알아보도록 합시다.
iOS 뿐만 아니라 대부분의 OS에서 메모리는 다음과 같이 이루어져 있습니다.

일반적인 메모리의 구조는 다음과 같습니다.
코드 영역, 데이터 영역, 힙 영역, 스택 영역으로 이루어져 있으며, 각 영역은 낮은 주소부터 높은 주소로 차례대로 할당이 됩니다.

컴파일 타임과 런 타임

컴파일 타임(Compiletime)과 런타임(Runtime)은 소프트웨어 프로그램개발의 서로 다른 두 계층의 차이를 설명하기 위한 용어입니다.

컴파일 타임(Compiletime)

컴파일 타임은 소스 코드를 기계어로 번역하여 실행 가능한 형태로 만드는 과정입니다.
주로 개발 단계에서 발생하며, 해당 오류들을 미리 발견할 수 있습니다.

  • Syntax Error
  • 타입 체크 오류
  • 파일 참조 오류

Xcode와 같은 개발 툴에서 문법 오류가 발생한 경우, 경고 및 알림을 통해 알려주는 것은 컴파일 타임에서 발생할 오류를 미리 알려주는 것이죠.
Swift에서는 소스 코드를 컴파일하면, Swift 코드가 LLVM 컴파일러를 통해 중간 언어로 변환됩니다.

런타임(Runtime)

런 타임은 프로그램이 실제로 실행되어 동작하는 시점입니다.
사용자의 입력에 따라 프로그램이 반응하고, 데이터를 처리하며 작동합니다.
그렇기 때문에, 프로그램 실행 중 다음과 같은 오류가 발생하여 프로그램이 종료되거나 원하지 않은 결과를 얻을 수 있습니다.

  • 0 나누기 오류
  • Null 참조 오류
  • 메모리 부족 오류

예시 코드

// ViewController
let button = UIButton()
button.addTarget(self, #selector(didTapButton(_:)), .touchUpInside)

@objc func didTapButton(_ sender: UIButton) {
	let message = "Tap Button!"
	print(message)
}

Xcode는 위의 코드들을 앱 실행 전 오류들이 있는지 알려주고 없다면 정상적으로 실행됩니다.
해당 과정을 컴파일 타임이라고 하는 것이죠. -> Xcode의 컴파일러가 해당 코드를 기계어로 번역하는 과정

버튼을 탭하면 didTapButton() 메서드가 실행되며 message를 메모리에 할당하고 출력하는 것처럼, 앱이 실행 중인 경우 발생하는 이벤트 등을 처리하여 관련 동작을 수행합니다.
해당 과정을 런타임이라고 합니다. -> iOS 운영 체제가 앱 실행 도중 상황에 맞는 코드를 실행하는 과정

할당되는 주소가 다른 이유

영역마다 할당되는 주소의 차이가 있는 이유는 주로 하드웨어의 동작 방식과 관련되어 있습니다.

코드 영역이 낮은 주소에 할당되는 이유는, 하드웨어 중 CPU의 동작과 관련이 있습니다.
CPU는 내부의 프로그램 카운터(Program Counter, PC)를 통해 실행할 명령어의 주소를 저장하고 이동하여 다음 명령어를 실행시킵니다. 낮은 주소부터 시작하기 때문에, 명령어들의 집합이 있는 코드 영역이 낮은 주소에 할당되는 것입니다.

  • 프로그램 카운터란?
    CPU 내부 레지스터 중 하나로, 다음에 실행할 명령어의 주소를 저장하고 해당 주소로 이동하여 명령어를 실행하는 역할을 합니다.
    즉, CPU는 프로그램 카운터에 저장된 주소로 이동하여 해당 주소에 있는 명령어를 실행하고, 실행하고 나면 다음 명령어의 주소를 가리키도록 업데이트가 됩니다. 이 과정을 순차적으로 진행하면서 명령어들을 계속해서 실행시키는 것이죠.

힙 영역은 동적 할당의 불확실성과 데이터 크기의 변동성 때문에 높은 주소에서 낮은 주소로 데이터가 쌓이고,
스택 영역은 말 그대로 후입선출(LIFO) 구조를 가진 스택의 구조로 이루어져 있기 때문에 지연 변수 관리를 위해 높은 주소부터 할당이 됩니다.

그럼 각 영역의 특징에 대해 자세하게 알아보도록 합시다.
일반적인 특징과, iOS 관점에서의 특징 및 어떻게 메모리에 각 영역에 코드가 할당되는지 봅시다.

코드(Code) 영역

일반적 특징

코드 영역은 컴파일 타임에 크기가 결정됩니다.
프로그램의 기계어 명령어가 저장되는 영역으로, 프로그램의 실행 코드가 저장됩니다.
읽기 전용(Read-Only)으로 처리되며, 프로그램이 실행될 때 메모리에 로드되고 종료 시 메모리에서 제거됩니다.

iOS

앱의 실행 코드, 클래스 정의, 함수 정의 등이 컴파일 시에 실행 코드가 되며, 코드 영역에 저장됩니다.
앱이 실행되면 코드 영역에 있는 앱의 실행 코드가 메모리에 로드됩니다.

class MyViewController: UIViewController {}
struct MyStruct {}
enum MyEnum {}
func myFunction() {}
var myVariable: Int = 0

해당 코드와 같이 구조체나 클래스 등과 같이 정의하는 코드 자체는 코드 영역에 할당이 됩니다.
앱이 실행되면 코드 영역의 코드들이 메모리에 로드되고, 해당 코드에 따라 정의된 각각의 요소들은 어떤 요소냐에 따라 상황에 맞게 데이터, 힙, 스택 영역에 할당되게 됩니다.

데이터(Data) 영역

일반적 특징

데이터 영역은 컴파일 타임에 크기가 결정됩니다.
전역 변수(Global Variables)와 정적 변수(Static Variables)가 저장되는 영역입니다.
코드 영역과 마찬가지로 프로그램 시작 시부터 종료 시까지 메모리에 상주하며, 프로그램의 정적 데이터를 저장합니다.

iOS

전역 변수와 정적 변수, class 또는 struct에 포함된 해당 프로퍼티들 또한 데이터 영역에 저장됩니다.
앱이 실행될 때 초기화되고 메모리에 남아있게 되는 것이죠.

class MyClass {
	var classVariable: Int = 1
	static var classStaticVariable: Int = 2
}

struct MyStruct {
	var structVariable: Int = 3
    static var structStaticVariable: Int = 4
}

var globalVariable: Int = 5
static var globalStaticVariable: Int = 6

해당 코드에서 var, static var 키워드가 붙은 모든 변수들은 데이터 영역에 할당됩니다.
하지만 다른 점은 있습니다.

globalVariableglobalStaticVariable은 모두 전역 변수이기 때문에, 프로그램이 실행될 때 메모리에 할당됩니다.
classStaticVariablestructStaticVariable은 특정 요소 내부의 정적 변수로, 클래스나 구조체의 인스턴스와 상관 없이 해당 요소 내에 속하기 때문에 위 두 변수와 마찬가지로 프로그램이 실행될 때 메모리에 할당됩니다.
추가로, 모든 곳에서 MyClass.classStaticVariable과 같이 동일한 정적 변수에 접근할 수 있기 때문에 하나의 메로리 공간을 공유합니다.
classVariablestructVariable은 인스턴스 생성 시마다 해당 메모리 공간을 추가로 할당받게 됩니다.

힙(Heap) 영역

일반적 특징

힙 영역은 런타임크기가 계속해서 변경됩니다.
동적으로 할당되는 데이터(클래스 인스턴스)가 저장되는 영역입니다.
프로그래머가 직접 메모리를 할당하고 해제할 수 있는 영역으로, 메모리 관리가 필요합니다.
프로그램 실행 중(런타임)에 동적으로 할당되는 데이터의 크기와 수명에 따라 크기가 계속해서 변화합니다.

iOS

iOS 앱은 OOP(객체 지향 프로그래밍) 방식을 사용하므로, 클래스의 인스턴스(객체), 배열과 같은 데이터 구조는 힙에 할당됩니다.
UIViewController, UIView와 같은 UI 요소들이 힙에 할당됩니다.
메모리 관리는 보통 ARC(Automatic Reference Counting)가 메모리 관리를 해줍니다.
더 효율적이고 좋은 앱을 위해서는 개발자도 메모리 관리를 신경써주어야 하죠.

class MyClass {
    var property: Int
    
    init(value: Int) {
        property = value
    }
}

// 힙에 MyClass의 인스턴스 할당
let instance1 = MyClass(value: 10)

// 다른 인스턴스를 생성하면 새로운 메모리 공간에 할당
let instance2 = MyClass(value: 20)

해당 코드와 같이 class의 인스턴스를 생성하면서, 힙에 메모리가 동적으로 할당되고 사용할 수 있는 것이죠.
반대로 인스턴스를 제거하며 메모리를 동적으로 줄일 수도 있습니다.

스택(Stack) 영역

일반적 특징

스택 영역은 일반적으로 컴파일 타임에 크기가 결정됩니다.
함수 호출 시마다 스택 프레임이 생성되고 제거됩니다.
스택 프레임의 크기는 함수의 지역 변수(Local Variables)와 함수 매개변수에 따라 정해지며, 대부분의 경우 컴파일러가 미리 예상하여 할당합니다.
특정 상황(e.g 재귀 함수 호출)에서는 런타임에 동적으로 크기가 변경될 수 있습니다.

  • "일반적으로 컴파일 타임"이라고 쓴 이유는, 특정 상황에서는 런타임에 동적으로 크기가 변경될 수 있기 때문입니다.
    재귀 함수를 호출하게 되면 스택 프레임이 계속해서 쌓이기 때문에 컴파일러가 예상하지 못한만큼 크기가 커지면 런타임에 동적으로 변할 수도 있다는 말입니다.

iOS

struct의 인스턴스(객체)가 생성되거나, 함수 호출이 발생하면 필요한 데이터들이 스택 영역에 저장됩니다.
인스턴스가 제거되거나 함수가 종료되면 스택 영역에서 사라지며 메모리 공간이 해제되죠.

재귀 함수의 경우, 재귀할 때마다 새로운 스택 프레임이 생성됩니다.
재귀 호출이 깊어질수록 계속해서 스택 프레임이 쌓이며, 호출이 종료되면 스택 프레임이 역순으로 해제됩니다.
-> 재귀 깊이 제한을 걸어줘야 하는 이유입니다. 수십만 번 호출하면 스택 프레임이 수십만개 쌓이고 메모리를 엄청나게 차지할 수 있으니까요.

struct Point {
	var x: Int
    var y: Int
}

func modifyPoint() {
	var myPoint = Point(x: 10, y: 20) // 스택에 할당
    
    myPoint.x += 5
    myPoint.y -= 10
}

modifyPoint()

해당 코드는 modifyPoint() 가 호출되면, 함수의 스택 프레임이 생성되고 스택 영역에 해당 프레임이 할당됩니다. 프레임 내부에 struct의 정보가 담겨지게 되고, 함수가 종료되면 struct 정보와 함께 스택 프레임이 사라집니다.

여기서, struct는 보통 스택 영역에 할당되지만, 힙 영역에 할당되기도 한다고 하여 이것저것 찾아봤습니다.
정리해서 말씀드리자면 이렇습니다.

  • 작은 크기의 struct는 일반적으로 스택 영역에 할당됩니다.
    -> struct는 값 타입(Value Type)이기 때문에, 스택에 직접 할당되고 메모리 효율성을 높일 수 있습니다.
  • 큰 크기의 struct는 일반적으로 힙 영역에 할당됩니다.
    -> struct 내부 프로퍼티에 10만개의 Int값을 가진 배열이 있는 경우, 크기가 매우 크다고 할 수 있겠죠?
    이런 경우는 컴파일러가 힙 영역에 할당하여 메모리 효율성을 높일 수 있다고 합니다.

결국 일반적으로 사용하는 크기가 작은 struct는 스택 영역에 할당되는 것이 많고,
크기가 큰 struct를 사용해야 하는 경우엔 컴파일러가 판단하여 힙 영역에 할당될 수도 있다는 것 같네요.

profile
iOS개발 공부 중입니다.

0개의 댓글