미루고 미루다 메모리 구조와 더불어 자료구조까지 공부 해보려한다.
메모리 구조는 프로그래밍 전반에 걸쳐 중요한 개념이라고 한다.
메모리 구조를 이해하면 코드의 효율성도 높아지고,
앱의 동작방식을 깊이 이해하는데도 큰 도움이 된다니 차근차근 공부해보자.
먼저 컴퓨터 메모리 구조는 크게 4가지 영역으로 나뉜다.
코드영역 (Code Segment) 은 실행할 프로그램의 명령어가 저장이 되고,
읽기 전용의 특징이 있다.
int main() {
return 0;
}
위의 main 함수 코드 자체는 코드 영역에 저장된다.
데이터 영역의 역할로 프로그램 시작과 동시에 할당되는 전역 변수와 정적(static) 변수들이 저장된다.
프로그램이 종료 될 때까지 메모리에 남아있는 특징이 있다.
var globalVar = 10 // 데이터 영역에 저장
globalVar 은 프로그램 실행 내내 메모리에 존재한다.
스택 영역은 함수 호출 시 생성되는 지역변수와 매개변수가 저장된다.
함수가 종료되면 메모리가 자동으로 해제 되며,
LIFO (Last In, First Out) 방식으로 작동한다.
스택 영역의 크기가 제한되어 있는 특징이 있는데,
이 말은 시스템에서 프로그램이 사용할 수 있는 스택 메모리의 크기가 고정되어 있다는 의미라고 보면 된다.
어떤 프로그램을 설계한다고 했을 때, 이 부분을 고려해서 재귀 호출이나 지역 변수 사용을 효율적으로 관리해야 하는데,
제한된 크기를 초과하게 되면 프로그램이 중단되거나,
스택 오버플로우가 발생할 수 있으니 주의가 필요할 수 있다고 한다.
func example() {
let localVar = 5 // 스택 영역에 저장
}
localVar는 함수가 종료되면 사라진다.
힙 영역은 프로그래머가 동적으로 할당하는 메모리가 저장되는 역할을 한다.
크기를 알 수 없는 데이터, 큰 배열이나 객체 등은 런타임에 힙 메모리에 저장된다고 한다.
힙 메모리는 메모리 공간을 비연속적으로 사용하기 때문에,
필요한 만큼 크기를 동적으로 요청할 수 있다.
그리고 swift에서는 자동 해제 지원이라고 해서 힙 메모리에 저장되는 데이터는 ARC를 통해 메모리 관리를 자동화 하고,
힙 메모리는 스택보다 크고 유연하지만 비효율적으로 관리될 경우 메모리 누수나 성능 문제가 발생할 수 있다.
이걸 예방하기 위해 강한 참조 순환 등을 피하는 설계가 필요하다고 한다.
class Example {
var name: String
init(name: String) {
self.name = name
}
}
let instance = Example(name: "Heap") // 힙 영역에 저장
Example 인스턴스는 힙 영역에 저장되고, instance는 스택에 있는 참조다.
Swift는 ARC(Automatic Reference Counting)를 사용하여 메모리를 관리한다.
메모리 구조와 Swift의 상호작용 방식을 한번 보자.
var a = 10
var b = a // b는 a의 값을 복사
b = 20
print(a) // 10
class Person {
var name: String
init(name: String) {
self.name = name
}
}
let personA = Person(name: "sonny")
let personB = personA
personB.name = "gyeom"
print(personA.name) // gyeom (같은 힙 메모리 참조)
ARC는 힙 메모리를 자동으로 관리하는 시스템인데,
메모리를 해제할 시점을 ARC가 참조 카운트를 기반으로 판단한다고 한다.
이 때 참조카운트는 객체가 힙 메모리에 남아있을지 해제될지를 결정하는 핵심 요소로 볼 수 있다.
참조 카운트라는 말이 낯설다. 뭘 세는걸까
참조카운트는 특정 객체를 참조하는 변수나 상수의 개수를 뜻한다고 한다.
참초 카운트가 0이 되면 객체는 더 이상 필요하지 않기 때문에
ARC가 메모리를 해제한다.
객체 생성 시 참조 카운트 증가
객체가 생성되면 참조 카운트는 1로 설정 됨.
참조가 추가될 때 증가
다른 변수나 상수가 같은 객체를 참조하면 참조 카운트가 1씩 증가함.
참조가 제거될 때 감소
객체를 참조하는 변수가 더 이상 사용되지 않으면 참조 카운트가 1씩 감소함.
참조 카운트가 0이 되면 메모리 해제
객체를 참조하는 변수가 없으면 ARC가 메모리를 자동으로 해제함.
.
.
.
class Person {
var name: String
init(name: String) {
self.name = name
print("\(name) 객체가 생성되었습니다.")
}
deinit {
print("\(name) 객체가 메모리에서 해제되었습니다.")
}
}
var person1: Person? = Person(name: "Alice") // 참조 카운트 1
var person2 = person1 // 참조 카운트 2
person1 = nil // 참조 카운트 1
person2 = nil // 참조 카운트 0 -> 메모리 해제
실행 결과는
Alice 객체가 생성되었습니다.
Alice 객체가 메모리에서 해제되었습니다.
person1
과 person2
는 같은 객체를 참조한다.person1
이 nil
이 되어도 person2
가 여전히 객체를 참조하고 있으므로 메모리는 해제되지 않는다.person2
도 nil
이 되면 참조 카운트가 0
이 되어 ARC
가 객체를 해제한다.기본적으로 swift의 객체 참조는 강한 참조인데,
만약 두 객체가 서로를 강하게 참조하게 된다면
참조 카운트가 0이 될 수 없어서 메모리가 해제되지 않는 상황이 된다.
결과적으로 메모리 누수가 발생하게 되는 것이다.
class Person {
var name: String
var friend: Person? // 다른 객체를 참조하는 변수
init(name: String) {
self.name = name
}
deinit {
print("\(name) 객체가 해제되었습니다.")
}
}
var alice: Person? = Person(name: "Alice")
var bob: Person? = Person(name: "Bob")
alice?.friend = bob // Alice가 Bob을 강하게 참조
bob?.friend = alice // Bob이 Alice를 강하게 참조
alice = nil // Alice를 nil로 설정해도
bob = nil // Bob을 nil로 설정해도
// 둘 다 메모리에서 해제되지 않는다! (강한 참조 순환)
alice
는 bob
을 참조하고, bob
은 다시 alice
를 참조한다.alice = nil
과 bob = nil
을 해도 참조 카운트가 1로 유지되고, 두 객체는 메모리에서 해제되지 않는다.강한 참조 순환을 방지하려면,
약한 참조(weak)나 비소유 참조(unownde)를 사용해야한다.
이들 참초는 카운트를 증가시키지 않기 때문에 객체가 해제될 수 있다.
class Person {
var name: String
weak var friend: Person? // 약한 참조로 설정
init(name: String) {
self.name = name
}
deinit {
print("\(name) 객체가 해제되었습니다.")
}
}
var alice: Person? = Person(name: "Alice")
var bob: Person? = Person(name: "Bob")
alice?.friend = bob // Alice가 Bob을 약하게 참조
bob?.friend = alice // Bob이 Alice를 약하게 참조
alice = nil // Alice를 nil로 설정
bob = nil // Bob을 nil로 설정 -> 메모리에서 해제됨!
alice
와 bob
모두 약한 참조로 설정되어 있기 때문에, 서로를 참조한다고 해도 참조 카운트가 증가하지 않는다.alice
와 bob
이 nil
로 설정되면 각각 참조 카운트가 0이 되어 메모리에서 정상적으로 해제된다.강한 참조 순환은 객체들이 서로를 강하게 참조할 때 발생한다.
이로 인해 객체들이 메모리에서 해제되지 않아서 메모리 누수가 발생해버리는 것인데,
이를 해결하려면 약한 참조(weak)나 비소유 참조(unowned)를 사용하여 참조 카운트를 증가시키지 않게 해야 한다는 걸 잊지말자.
이렇게 메모리 구조에 대해 알아보았다.
이제 자료구조와의 차이를 좀 공부해보고 싶다.
메모리 구조와 자료구조는 모두 프로그래밍과 컴퓨터 과학에서 중요한 개념이지만,
초점과 목적이 다르다고 한다.
메모리 구조는 컴퓨터 메모리가 데이터를 저장하고 관리하는 방식을 말하는데,
프로그램 실행 중 데이터가 어디에, 어떻게 저장되는지를 다룬다.
컴퓨터 메모리의 물리적/논리적 구성을 이해하는데 중점을 두고,
메모리의 영역과 데이터를 저장하는 방식이다.(스택,힙,코드,데이터 등)
프로그램의 메모리 효율성을 높이고 오류 (메모리 누수 등) 를 방지하며,
메모리 사용 방식 (할당, 해제)을 이해하고 최적화 하는 것이 목적이다.
자료구조는 데이터를 효율적으로 저장하고 조작하는 방법을 말한다.
어떤 데이터를 어떻게 구성하고 저장할지에 대한 설계 방식이라 보면 된다.
데이터의 구조와 관계를 정의하고,
데이터를 저장, 검색, 삽입, 삭제 등의 연산을 효율적으로 수행하는 방법에 초점을 둔다.
데이터의 효율적 관리와 알고리즘 성능의 최적화,
그리고 대량의 데이터를 다룰 때 연산 속도를 개선하는 것이 목적이다.
자료구조는 메모리구조를 활용해서 구현이 된다고 한다.
예로 들면, 배열은 메모리에서 연속된 공간에 데이터를 저장하고,
연결 리스트는 메모리의 비연속적인 공간을 포인터로 연결한다.
그리고 메모리구조는 자료구조의 성능에 영향을 미치는데,
예로 들면 스택과 큐는 주로 스택 영역을 사용하고,
동적 자료 구조는 힙 영역을 활용하는 것이다.
특징 | 메모리 구조 | 자료 구조 |
---|---|---|
초점 | 데이터가 메모리에 어떻게 저장되고 관리되는지 | 데이터를 어떻게 구성하고 조작할지 |
관점 | 컴퓨터 시스템 관점 | 알고리즘 및 문제 해결 관점 |
단위 | 코드, 데이터, 스택, 힙 | 배열, 리스트, 트리, 그래프, 해시 등 |
목적 | 메모리 최적화 및 관리 | 데이터 처리 및 알고리즘 성능 개선 |
사용되는 대상 | 컴퓨터 메모리 | 데이터 |
메모리 구조는 프로그램의 성능과 안정성에 큰 영향을 미친다는 것을 깨달은 것 같다.
이제 메모리 관리가 잘못되면 프로그램이 예기치 않게 크래시가 나거나, 메모리 누수로 인해 성능 저하가 발생할 수 있다는 것도 알게 되었고,
자료 구조와 메모리 구조는 서로 밀접하게 연관되어 있어서
예를 들어, 배열은 연속된 메모리에 데이터를 저장하고,
연결 리스트는 비연속적인 메모리를 사용한다는 점에서 메모리 관리 방식이 다르다는 것도 공부했다.
그리고 애증의 ARC에 대해 그나마 자세히 알게 된 시간이었는데,
강한 참조 순환을 피하기 위한 방법도 어느정도 알 것 같다.
약한 참조나 비소유 참조를 사용하면 메모리 누수를 방지할 수 있다는 점이 중요하다는 걸 알았으니 이 정도면 공부 잘 한 거겠지..?!