스파르타 코딩 클럽 - iOS 스타터 6기/본 캠프

18. 스파르타 코딩 클럽 - 메모리 구조

seongpil Heo 2025. 3. 18. 21:51

 📚 메모리 구조

프로세스

  • 실행중인 프로그램의 인스턴스를 의미합니다.
  • iOS에서는 실행되는 앱을 프로세스라고 이해하시면 됩니다.
  • 앱이 실행되면, 운영 체제는 앱의 실행을 관리하기 위해서 프로세스를 생성합니다.
  • 프로세스는 자신만의 메모리 영역을 할당받고, 앱의 실행상태를 관리합니다.

메모리 구조

  • 앱을 실행하면 운영체제(iOS)가 메모리에 영역을 설정해 줍니다.
  • 메모리영역에는 크게 4가지 영역이 존재하며 Code, Data, Heap, Stack 영역이 존재합니다.

Code 영역

  • 우리가 작성한 코드는 컴파일 과정을 거쳐 기계어 형태(컴퓨터가 읽을 수 있는 0과 1로 이루어진 형태)로 변환되어 이 영역에 저장됩니다.
  • CPU는 이 영역에 저장된 코드를 읽고 해당 작업을 처리합니다.
  • 프로그램 실행과 동시에 메모리에 할당되며, 프로그램이 종료되면 메모리에서 해제됩니다.
  • 이 영역은 Read-Only(읽기 전용) 으로 실행중에는 변경할 수 없습니다.

Data 영역

  • 전역변수정적(static)변수가 저장되는 메모리 영역입니다.
  • 프로그램 실행 시 메모리에 할당되며, 프로그램 종료 시 메모리에서 해제됩니다.
  • 프로그램 실행 중에 변수 값이 변경될 수 있으므로 Read-Write(읽기-쓰기가 가능) 입니다.

Heap 영역

  • class 인스턴스나 클로저와 같은 참조 타입(reference type)의 데이터가 할당되는 동적 메모리 영역입니다.
  • 이 영역의 메모리 크기는 런타임에 결정되며, 확정되지 않은 크기의 데이터가 저장됩니다.
  • ARC를 통해서 메모리 관리가 자동으로 이루어집니다.
    • 개발자는 메모리 누수에 주의해야 합니다.
    • 자동으로 관리하지만 관리를 해야합니다.
  • 메모리에 할당되고 해제되는 과정이 상대적으로 느립니다.

Stack 영역

  • 함수 호출 시 생성되는 지역변수, 매개변수, 리턴값 등이 저장됩니다.
  • 일반적으로 값 타입(value type)의 데이터가 저장됩니다.
  • 함수가 종료되면 자동으로 메모리에서 해제됩니다.
  • 메모리 할당 및 해제의 속도가 Heap 영역보다 빠릅니다.
  • 컴파일 시점에 해당 영역의 메모리 크기가 결정됩니다.
  • 스택 메모리는 크기가 제한되어 있어, 이를 초과하면 스택 오버플로(Stack Overflow) 오류가 발생하여 프로그램이 종료될 수 있습니다.

 💡 class의 인스턴스가 let인데 프로퍼티를 변경할 수 있었던 이유!

class Person {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

let person = Person(name: "Brody", age: 30)
print(person.name) // 출력 : Brody
person.name = "Change Name"
print(person.name) // Change Name

// let person인데 이름을 변경할 수 있는 이유!?
// class의 인스턴스는 reference type이여서 메모리의 주소값을 저장하고 있죠?
// person에는 인스턴스의 실제 주소값이 있을거에요 (ex 0x0016) 
// 이름을 변경하면 새로운 인스턴스를 생성하지 않기 때문에 새로운 주소값으로 변하지 않아요!
// 그래서 참조타입의 인스턴스의 값은 let이여도 바꿀수 있었던거죠!
// class는 주소값을 저장하기 때문에 한곳에서 변경해도 모두 변경됩니다.
class Person {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

let person = Person(name: "Brody", age: 30) // person : 주소값 0x0001을 저장
let person2 = person                        // person2 : 주소값 0x0001을 저장

person2.name = "John"                       // person2를 변경합니다. 주소값 0x0001의 name을 변경합니다.

print(person.name)    // John
print(person2.name)   // John


func nameChange(person: Person) {
    person.name = "바뀐이름"
}

nameChange(person: person) // person의 주소값 전달!

print(person.name)   // "바뀐이름"
print(person2.name)  // "바뀐이름"
// struct은 value type이기 때문에 값을 복사하여 전달합니다.
// 전달받은 값을 변경해도 원본이 변경되지 않습니다.
struct Animal {
    var name: String
    var age: Int
}

var animal = Animal(name: "Dog", age: 3)
var animal2 = animal

animal2.name = "Cat"

print(animal.name) // "Dog" 원본의 값은 변경되지 않아요.
print(animal2.name) // "Cat"

func changeName(animal: Animal) {
    var animal = animal
    animal.name = "Bird"
}

changeName(animal: animal)

print(animal.name) // "Dog" 원본의 값은 변경되지 않아요.
print(animal2.name) // "Cat"

 🛠️ Copy On Write 알아보기

📚 메모리 효율을 위해서 value type 의 인스턴스를 다른 값에 할당하거나 파라미터로 전달할 때 값이 변경되기 전까지는 실제로 복사하지 않는 것을 의미합니다.
// 메모리를 확인하는 함수!

/*
func address(of object: UnsafeRawPointer) -> String {
    let address = Int(bitPattern: object)
    return String(format: "%p", address)
}
*/

func address(of object: UnsafeRawPointer) -> String {
    return String(describing: object) // 포인터 주소를 문자열로 출력
}


var a = [1,2,3]
var b = a

print(address(of: &a)) // 0x60000171d6a0
print(address(of: &b)) // 0x60000171d6a0

// value type인 배열 a를 b에 할당하고 주소값을 확인했는데 똑같은 주소값이네요!
// 값을 작성하기 전까지는 동일한 메모리를 사용하는것을 의미합니다!

b.append(100)
print(address(of: &a)) // 0x60000171d6a0
print(address(of: &b)) // 0x6000021014c0

// b 배열에 값을 작성하면 그때 메모리를 복사합니다.
// 이렇게 하는 이유는 메모리 효율을 높이기 위해서입니다! 
// WRITE를 하기전까지는 쓸모없는 메모리를 사용하지 않죠!

var c = a
c = [1,2,3] 

print(address(of: &a)) // 0x60000171d6a0
print(address(of: &c)) // 0x60000170d920  //동일한 값을 넣었어도 값을 수정한거기 때문에 새로운 주소값이 나옵니다!