메모리와 디스크 기본 개념
▪️ 메모리
▪️ 디스크
UserDefaults
, CoreData
를 활용해서 디스크에 데이터를 저장할 수 있다.💻 다음 주어진 상황들에서, 메모리를 활용하는게 좋을지 디스크를 활용하는게 좋을지 생각해봅시다. (추후 수정 예정)
1. 전화번호부 앱에서 친구의 이름, 전화번호 정보 데이터.
2. 카운터 앱 개발할 때 사용했던 number
변수.
3. 스테이지가 있는 게임 앱에서 유저가 몇 스테이지까지 클리어했는지 정보.
4. 유튜브나 인스타그램 같은 SNS 앱에서 추천 창에 뜬 이미지와 동영상 정보들.
💻 비유와 함께 개발할 때 메모리 관리가 왜 중요한지 이해해봅시다.
💻 Garbage Collector
Garbage Collector 는 직역하면 쓰레기 청소부. 메모리에서 필요없는 것들을 정리해주는 역할을 합니다. 사용하지 않는 데이터들이 메모리에 올라와 공간을 차지하고 있는 것은 매우 비효율적입니다. 따라서 좋은 개발자는 메모리 관리를 신경써서 잘 할 줄 알아야 합니다.
사용하지 않는 메모리가 쌓이고 쌓여서 메모리에 부담이 되는 상황을 메모리 누수 (Memory Leak) 이라고 합니다.
Java 에서는 개발자가 직접 명시적으로 메모리 관리를 하지 않더라도 기본적으로 메모리 관리를 돕는 GC 라는 시스템이 있습니다.
GC 가 동작하는 방식을 간단하게만 소개하면, 런타임에 메모리 영역을 슥 훑어보며 사용중인 것들을 표시 (Mark) 하고, 표 되지 않은 모든 것들을 정리해버리는 Mark-and-Sweep 방식을 사용합니다.
🤔 왜 갑자기 Java 의 메모리 관리 시스템을 공부하나요?
일반적인 메모리 관리 개념의 근간이 되는 시스템이며, 개발자라면 필수 교양으로 알아야 하는 내용이기 때문에 간단하게 공부해보았습니다.
Reference Counting
메모리를 할당 받은 객체를 인스턴스라고 합니다.
예를들어 아래 코드에서 myClass 는 인스턴스가 된 것이죠.
class MyClass {}
// 메모리를 할당받음. 인스턴스.
let myClass = MyClass()
인스턴스는 하나 이상의 참조자(소유자=owner) 가 있어야 메모리에 유지가 됩니다. 소유자가 없다면 즉시 메모리에서 제거가 됩니다. 이때 인스턴스를 참조하고 있는 소유자의 개수를 reference count
라고 합니다.
reference count > 0
이면 메모리에 살아있고, reference count = 0
이면 메모리에서 삭제됩니다.
그렇기 때문에, 더 이상 사용하지 않을 인스턴스의 reference coutn 가 0보다 크지 않도록 주의를 해야합니다.
class MyClass {
init() {
print("MyClass 생성")
}
deinit {
print("MyClass 소멸")
}
}
// RC = 1
var myClass: MyClass? = MyClass()
// RC = 2
var myClass2 = myClass
// RC = 2-1 = 1
myClass = nil
오늘의 스스로 숙제!
클래스의 deinit 소멸자 메서드는 메모리에서 해제될때 호출됩니다. 위 코드에서 deinit 의 프린트가 찍힐까? Reference Count 의 개념을 생각하며 결과에 대한 이유를 생각해보자
ARC 와 MRC
ARC = Automatic Reference Counting
MRC = Manual Reference Counting
ARC
MRC
🙋🏻♂️ 그렇다면 ARC 는 자동으로 RC 카운트를 해서 메모리 관리를 해주는 좋은 시스템이니, 개발자는 메모리 관리에 대해 신경쓰지 않아도 되나요?
→ 그렇지 않습니다. ARC 로 잡아내지 못하는 메모리 누수 상황이 발생할 수 있기 때문에, 개발자는 메모리 관리 방법을 반드시 알아야 합니다.
약참조와 강참조
클로저의 캡처링 개념
class Adam {
let mbti = "ENTJ"
init() {
print("클래스 생성")
}
deinit {
print("클래스 소멸")
}
}
// adam rc = 1
var adam: Adam? = Adam()
// 클로저 내부에서 adam 캡처. rc 1 증가. adam rc = 2
let printMbti: () -> () = { [adam] in
guard let adam else { return }
print("adam's mbti = \(adam.mbti)")
}
printMbti()
// adam rc = 2-1 = 1
adam = nil
printMbti
라는 클로저를 선언했고, 클로저 내부에서 클로저 외부의 adam
이라는 객체를 가져다 쓰고 싶으면 값을 캡처
해야합니다. 이때 [ ]
로 감싸면 값을 캡처링해서 클로저 내부에서 사용할 수 있게 됩니다.
🌟 클로저 내부에서 클래스의 값을 캡처하면, Reference Count 가 증가합니다.
위 예시를 따라해보면 adam
의 deinit
소멸자가 호출되지 않습니다. 클로저에서 값을 캡처해 rc 가 증가했기 때문입니다.
위 코드를 개선해서, 메모리 누수가 발생하지 않는 상황을 만들려면 다음과 같이 코드를 작성해야 합니다.
class Adam {
let mbti = "ENTJ"
init() {
print("클래스 생성")
}
deinit {
print("클래스 소멸")
}
}
// adam rc = 1
var adam: Adam? = Adam()
// 클로저 내부에서 adam 캡처.
// weak 참조 했으므로 rc 가 증가하지 않음. adam rc = 1
let printMbti: () -> () = { [weak adam] in
guard let adam else { return }
print("adam's mbti = \(adam.mbti)")
}
printMbti()
// adam rc = 1-1 = 0
adam = nil
위 코드를 따라서 실행해보면, 메모리가 해제되고 deinit
”클래스 소멸”
이 호출되는 것을 확인할 수 있습니다.
순환 참조 (Circular Reference)
A 가 B 를 참조하고 (A→B),
B 가 A 를 참조해서 (B→A), 서로가 서로를 참조하는 상황을 순환 참조라고합니다.
일반적으로 순환 참조는 메모리 누수를 발생시키는 대표적인 사례입니다.
아래 예시를 보고 순환 참조 개념을 이해해봅시다.
class Person {
var pet: Dog?
init() {
print("Person 클래스 생성")
}
deinit {
print("Person 클래스 소멸")
}
}
class Dog {
var owner: Person?
init() {
print("Dog 클래스 생성")
}
deinit {
print("Dog 클래스 소멸")
}
}
// person rc = 1
var person: Person? = Person()
// dog rc = 1
var dog: Dog? = Dog()
// dog rc = 2
person?.pet = dog
// person rc = 2
dog?.owner = person
// person rc = 1
person = nil
// dog rc = 1
dog = nil
→ 따라서, 개발자는 개발할 때 순환참조가 발생하는 상황이 아닌지 점검할 줄 알아야 합니다.
살아있나요?