iOS Swift의 메모리 관리

호씨·2024년 12월 2일
2

메모리와 디스크 기본 개념

메모리와 디스크는 모두 컴퓨터나 스마트폰에서 데이터의 저장 및 처리를 담당하지만 목적과 특성이 다르다.

▪️ 메모리

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

▪️ 디스크

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

Garbage Collector (GC)

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

💻 Garbage Collector

Garbage Collector 는 직역하면 쓰레기 청소부. 메모리에서 필요없는 것들을 정리해주는 역할을 한다.

사용하지 않는 데이터들이 메모리에 올라와 공간을 차지하고 있는 것은 매우 비효율적입니다. 따라서 좋은 개발자는 메모리 관리를 신경써서 잘 할 줄 알아야 한다.

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

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

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

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

일반적인 메모리 관리 개념의 근간이 되는 시스템이며, 개발자라면 필수 교양으로 알아야 하는 내용이며 내가 자바랑 자바스크립트로 일을 때문에 간단하게 예시로 말해보았다.


Reference Counting

💻 Swift 의 메모리 관리 시스템의 핵심이 되는 개념인 Reference Counting(RC)

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

class MyClass {}

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

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

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

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


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 는 자동으로 RC 카운트를 해서 메모리 관리를 해주는 좋은 시스템이니, 개발자는 메모리 관리에 대해 신경쓰지 않아도 됨?
→ Nope. 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 가 증한다.

위 예시를 따라해보면 adamdeinit 소멸자가 호출되지 않습니다. 클로저에서 값을 캡처해 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

따라서, 개발자는 개발할 때 순환참조가 발생하는 상황이 아닌지 점검할 줄 알아야 한다.

profile
이것저것 많이 해보고싶은 사람

0개의 댓글

관련 채용 정보