카테고리 없음

swift 메모리 관리에 대해 공부해보자

kingarthur 2024. 7. 16. 17:23

오늘 데이터 메모리에 대해 간략하게 공부해보았다. 

 

우리가 잘 아는 램이랑 하드 디스크 차이에 대해 공부했다고 생각하면 될 것 같다. 

 

메모리는 머고 디스크 기본 개념은 뭘까? 

 

메모리는 - 휘발성 기억장치 ~ 데이터가 필요없으면 지워버리고 필요하면 올리고 하는장치라고 생각하면 되고

 

디스크는 - 저장장치라고 생각하면 된다. 지금 당장은 안쓰지면 지워지면 안되는것 전화번호목록이던지 친구목록 등 등 내가 계속 가지고 있어야 될 정보라고 생각하면 될것이다. 

 

자세히 설명하면 

메모리는 

▪️ 메모리

  • 일반적으로 RAM 을 말하는 경우가 많다.
  • 맥북에서도 몇 GB 짜리 RAM 을 사용하는지 볼 수 있다.
  • RAM 은 휘발성 메모리이다. 즉, 데이터를 영구적으로 저장하지 않는다. 일시적인 저장에 사용한다.→ 앱도 결국 데이터 덩어리이기 때문에, 실행을 시키면 메모리에 올라간다.→ RAM 의 용량이 클 수록, 동시에 실행시킬 수 있는 앱의 총량이 높아진다고 생각할 수 있다.
  • → 그렇기 때문에 메모리에 저장된 데이터는 앱이 메모리에서 내려올 때 같이 내려오게 되는 것.
  • → 앱 실행중에 메모리에 저장된 데이터들은 앱을 종료하면 함께 삭제된다. (휘발된다)
  • 디스크보다 속도가 빠르다. (CPU 가 디스크보다 메모리에 더 빨리 접근할 수 있다.)
  • 디스크에 비해 용량이 작다. (보통 8GB, 16GB, 32GB)
  • EEPROM 과 같은 비휘발성 메모리도 있다. 아이폰은 이곳에 장치의 일련번호 및 하드웨어 정보를 저장한다. 

 

▪️ 디스크

  • 영구적인 데이터를 저장하는 곳. 비휘발성 장치.
  • → 앱 실행중에 디스크에 저장된 데이터들은 앱을 종료해도 디스크에 남는다.
  • 파일, 문서, 프로그램 등 상대적으로 용량이 큰 정보들을 담을 수 있다.
  • 메모리에 비해 속도가 느리다.
  • UserDefaults, CoreData 를 활용해서 디스크에 데이터를 저장할 수 있다.

아무튼 이렇다고 생각하면 될 듯하다. 

 

Garbage Collector 는 메모리 관리를 돕는 시스템 중 하나. 대표적으로 Java 에서 GC 를 사용한다.

 

그러면 스위프트에서는 무엇을 사용하나??? ARC 사용한다. 

 

아무튼 비교하자면 

  • 일류 요리사가 있습니다.
  • 조리대에 수많은 식재료들이 있고, 열심히 요리를 하고 있습니다.
  • 조리대의 크기가 클수록 한꺼번에 올려놓을 수 있는 식재료의 양이 많아지겠죠.
  • 조리대의 크기는 메모리 (RAM) 의 크기에 비유해서 생각할 수 있습니다. RAM 의 크기가 클 수록 한꺼번에 올려놓고 처리할 수 있는 데이터의 양이 많아지는 것과 같습니다. 요리사는 CPU 입니다.
  • 매우 넓은 조리대가 있고, 요리사는 열심히 요리를 하느라 정신이 없어서, 더 이상 사용하지 않는 식재료를 정리하면서 동시에 요리하기가 힘듭니다.
  • 하지만 더 이상 필요하지 않은 식재료가 조리대의 자리를 차지하면 정작 필요한 재료들을 새로 올려놓을 때 좋지 않겠죠.
  • 이때 센스있는 조교가 나타납니다. 이 조교는 조리대를 슥 훑어보고 “지금부터 양배추와 토마토는 더 이상 사용하지 않는군.” 하고 알아서 조리대에서 양배추와 토마토를 정리해줍니다.
  • 그렇게 함으로써 요리사는 조리대를 더 효율적으로 사용할 수 있게 됩니다.
  • 이 조교는 Garbage Collector (가비지 컬렉터) 입니다. 

요렇다고 보면 된다. 

 

갈베지 컬렉션은 왜 써야하는가?? 

 

Garbage Collector 는 직역하면 쓰레기 청소부. 메모리에서 필요없는 것들을 정리해주는 역할을 합니다. 사용하지 않는 데이터들이 메모리에 올라와 공간을 차지하고 있는 것은 매우 비효율적입니다. 따라서 좋은 개발자는 메모리 관리를 신경써서 잘 할 줄 알아야 합니다.

 

사용하지 않는 메모리가 쌓이고 쌓여서 메모리에 부담이 되는 상황을 메모리 누수 (Memory Leak) 이라고 합니다.

Java 에서는 개발자가 직접 명시적으로 메모리 관리를 하지 않더라도 기본적으로 메모리 관리를 돕는 GC 라는 시스템이 있습니다.

 

GC 가 동작하는 방식을 간단하게만 소개하면, 런타임에 메모리 영역을 슥 훑어보며 사용중인 것들을 표시 (Mark) 하고, 표 되지 않은 모든 것들을 정리해버리는 Mark-and-Sweep 방식을 사용합니다.

 

🤔 왜 갑자기 Java 의 메모리 관리 시스템을 공부하나요?

일반적인 메모리 관리 개념의 근간이 되는 시스템이니 알아두는게 좋다 ~ 메모리를 잘 써야지 데이터 비용도 아끼겠지 ~

 

Swift 의 메모리 관리 시스템의 핵심이 되는 개념인 Reference Counting(RC) 에 대해 공부합니다.

 

메모리를 할당 받은 객체를 인스턴스라고 합니다.

예를들어 아래 코드에서 myClass 는 인스턴스가 된 것이죠.

class MyClass {}

// 메모리를 할당받음. 인스턴스.
let myClass = MyClass()

 

인스턴스는 하나 이상의 참조자(소유자=owner) 가 있어야 메모리에 유지가 됩니다. 소유자가 없다면 즉시 메모리에서 제거가 됩니다. 이때 인스턴스를 참조하고 있는 소유자의 개수를 reference count 라고 합니다.

 

reference count > 0 이면 메모리에 살아있고, reference count = 0 이면 메모리에서 삭제됩니다.

 

그렇기 때문에, 더 이상 사용하지 않을 인스턴스의 reference coutn 가 0보다 크지 않도록 주의를 해야합니다.

 

이걸 지켜주지 못하면 메모니라 누수됬다고 한다. 그래서 약한참조를 쓰는 것인데  https://accoding.tistory.com/66

 

swift 캡처 참조 타입에 대해 배워보자(강한참조, 약한참조, 순환참조)

캡쳐 리스트캡쳐링을 하는 값의 참조 규칙을 캡쳐리스트를 통해서 정해줄 수 있습니다.클로저의 캡쳐리스트 내에 정의하면 캡쳐링할 때 본래의 타입에 맞게 캡쳐링되도록 사용되게 할 수 있

accoding.tistory.com

 

대략 코드로는 

 

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

 

이렇다고 보면 된다. 

잉 우리는 레퍼런스 참조 해준적이 없는데 머야 할 수도 있다 ~ 

그래서 아까 자바에서 사용하는 갈베지 컬렉션은 스위프트에서 ARC 내장 탑제 되어 있어서 알아서 카운트 해주고 지워준다 ~

우리도 모르는사이에 엄청 편한거지 근데 잘 지워야 하기도 한다. 안지우면 바로 메모리 누수가 발생하니~ 

어디서 누수가 발생하냐 위에처럼 순환참조 타입이 되면 누수가 발생한다 ~

 

메모리 관리에는 자동인 ARC 와 수동인 MRC 존재한다 

 

 🧑🏻‍💻 ARC 와 MRC 의 개념을 공부 해봅시다.

  • ARC = Automatic Reference Counting
  • MRC = Manual Reference Counting
  • ARC
    • ARC 는 Swift 의 메모리 관리 시스템. Java 에 GC 가 있다면 Swift 에는 ARC 가 있음.
    • Reference Count 를 자동으로 계산. (Automatic)
      • 객체가 생성될 때 RC 가 1 로 설정
      • 객체가 다른 변수나 속성에 할당되어 참조될때마다 RC 가 1 씩 증가
      • 객체에 대한 참조가 해제될때마다 RC 가 감소
      • RC 0 이 되면 더 이상 사용되지 않는 것으로 간주되어 메모리에서 해제.
  • MRC
    • MRC 는 Objective-C 에서 사용하는 메모리 관리 시스템.
    • Reference Count 를 개발자가 코드로 직접 계산. (Manual)
      • 객체가 생성될때 개발자가 명시적으로 메모리 할당
      • 객체를 다른 변수나 속성에 할당되어 참조될때마다 개발자가 명시적으로 RC 증가
      • 객체에 대한 참조가 해제될때마다 개발자가 명시적으로 RC 감소
      • RC 가 0 이되면 개발자가 명시적으로 메모리에서 해제.

 

아까도 말했지만 ARC 자동으로 메모리 관리한다고 모든게 자동이 아니다 ~

사용하지 않는 메모리를 지우지 않으면 메모리 누수가 발생하니 메모리 관리를 잘 해줘야한다. 

 

그래서 약참조와 강참조가 생기는데 아까 링크에 자세한것은 있으니 참고하면 되고 여기서는 간단하게만 말하겠다. 

 

약참조와 강참조의 개념에 대해 공부 해봅시다.

  • 약참조
    • Reference Count 를 증가시키지 않으면서 참조하는 것.
    • weak 키워드를 붙여서 약참조를 할 수 있다.
  • 강참조
    • Reference Count 를 증가시키면서 참조하는 것.
    • 일반적인 참조 방식을 말한다. 

요약하면 약참조는 레퍼런스를 증가시키지 않고 강참조는 증가시킨다고 보면된다. 

 

자 예시고 만약 클로저 안에 캡처링이 있다면??

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 ”클래스 소멸” 이 호출되는 것을 확인할 수 있습니다.

 

이렇게 강참 약참을 알아봤다. 

 

그럼 또 따른 참조가 있는가? 

 

순환참조라는게 또 존재한다 ~

 

 

이것 또한 이전 포스팅에 있으니 자세한것은 거기서 보면되고 

요약하면 클래스끼리 연결이 되면 참조를 서로하는 방향이 되서 연결 고리를 끈어줘야한다. 한쪽에서

 

예시로 보자면 

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

이게 순환참조 모습이다. 

즉 레퍼런스가 둘다 참조가 되는 현상이라 메모리 누수가 발생한다. 

 

그래서 개발자는 순환참조가 발생하지 않게 연결을 잘 끈어주고 메모리 관리에 힘을 써야한다.