# 메모리 관리 개념

구조체 등의 스택에 쌓이는 데이터들은 스택프레임 제거와 함께 메모리 관리가 자동으로 이루어진다. 반면 클래스, 클로저와 같이 힙 메모리에 저장되는 참조 타입 데이터들은 ARC모델에 의해 메모리 관리가 이루어지고 있었다. (Auto Reference Counting)

힙 메모리 상에 올린 데이터 동적 할당이 해제되지 않았을때의 상황을 메모리 누수라고 한다.

# ARC(Automatic reference Counting)

가비지 컬렉터 모델은 힙 메모리를 자동으로 스캔한 뒤 불필요하게 차지하는 공간을 비워준다.

ARC는 데이터 참조숫자를 카운팅하여 자동으로 메모리 정리를 해준다. 개발자가 해야할 일은 메모리 참조 형태를 잘 결정해야 한다는 것이다. (많이 참조하도록 코드를 짜거나 .. 하는 등)

// 개발자 코드
class Point{
    var x, y: Double
    func draw(){ ... }
}

let point1 = Point(x: 0, y: 0)
let point2 = point1
point2.x = 5
// 내부 코드
class Point{
    // generated!
    var refCount: Int
    var x, y: Double
    func draw(){ ... }
}

let point1 = Point(x: 0, y: 0) // RC + 1
let point2 = point1
retain(point2) // retain은 RC를 하나 올려준다. RC + 1
point2.x = 5

release(point1) // RC - 1
release(point2) // RC - 1

클래스를 통해 인스턴스를 생성하게 되면 RC를 관리하는 속성이 하나 함께 추가된다. 이때 스택을 통해 힙에 할당된 메모리를 참조시키면 (변수에 참조 타입의 데이터를 할당하면) RC가 하나 증가하게 되는 것이다.

위의 코드에서 Point(x: 0, y: 0)이라는 인스턴스가 0x123이라는 메모리 주소를 가지고 힙 메모리에 동적 할당 되었다고 가정하면 point1이라는 변수가 스택에서 0x123을 참조하므로 RC가 하나 증가하고, 이후 point2라는 변수가 한번 더 0x123이라는 주소를 참조하므로 RC가 하나 더 증가하게 되는 것이다.

class Dog {
    var name: String
    var weight: Double

    init(name: String, weight: Double) {
        self.name = name
        self.weight = weight
    }

    deinit {
        print("\(name) 메모리 해제")
    }
}


var choco: Dog? = Dog(name: "초코", weight: 15.0)  //retain(choco)   RC: 1
var bori: Dog? = Dog(name: "보리", weight: 10.0)   //retain(bori)    RC: 1
// 여기까지 실행하면 소멸자 실행이 안됨!
print("remain....")


// 아래 코드 실행을 통해 참조 수를 release해야 소멸자 실행됨!
choco = nil   // RC: 0
bori = nil    // RC: 0

메모리 누수가 발생할 수 있는 상황을 알고 있는 것이 중요하다.

# 강한 참조 사이클

힙 메모리 상에 올라간 인스턴스 간에 참조를 하게 되는 경우 스택상의 변수를 nil로 초기화 하더라도 이미 스택에서 한번 참조한 경우 서로 RC를 한번씩 증가시키기 때문에 이들의 RC를 릴리즈 할 수가 없다. (이미 스택상의 변수는 nil로 인스턴스 메모리 주소를 더 이상 참조하지 않기 때문)

class Dog {
    var name: String
    // 주의!
    var owner: Person?

    init(name: String) {
        self.name = name
    }

    deinit {
        print("\(name) 메모리 해제")
    }
}


class Person {
    var name: String
    // 주의!
    var pet: Dog?

    init(name: String) {
        self.name = name
    }

    deinit {
        print("\(name) 메모리 해제")
    }
}
var bori: Dog? = Dog(name: "보리") // Dog 인스턴스 RC + 1
var gildong: Person? = Person(name: "홍길동") // Person 인스턴스 RC + 1

// 주의!
bori?.owner = gildong // Person 인스턴스 RC + 1
gildong?.pet = bori // Dog 인스턴스 RC + 1

// 아래 코드로 인스턴스간의 참조를 끊어줘야함
// bori?.owner = nil
// gildong?.pet = nil


bori = nil // Dog 인스턴스 RC - 1
gildong = nil // Dog 인스턴스 RC - 1

// 소멸자 실행이 안됨!

bori 인스턴스와 gildong 인스턴스가 서로를 참조함.

메모리 누수 예방을 위해 bori?.owner = nil과 같은 코드 입력으로 인스턴스 간 참조를 직접 끊어줄 수도 있지만 이는 휴먼 에러의 가능성이 매우 높다.

이때 사용하는 약한 참조(Weak Reference), 비소유 참조(Unowned Reference) 방법이 있다.

# 메모리 누수 해결 방안

약한참조와 비소유참조는 참조하는 인스턴스의 RC 숫자를 올라가지 않도록 한다.

# 1. 약한참조 weak

class Dog {
    var name: String
    weak var owner: Person?     // weak 키워드 ==> 약한 참조

    init(name: String) {
        self.name = name
    }

    deinit {
        print("\(name) 메모리 해제")
    }
}

class Person {
    var name: String
    weak var pet: Dog?         // weak 키워드 ==> 약한 참조

    init(name: String) {
        self.name = name
    }

    deinit {
        print("\(name) 메모리 해제")
    }
}

var bori: Dog? = Dog(name: "보리")
var gildong: Person? = Person(name: "홍길동")


// 강한 참조 사이클이 일어나지 않음
bori?.owner = gildong
gildong?.pet = bori

bori = nil
gildong?.pet // nil

gildong = nil

위 코드에서 bori?.owner = gildong 코드로 인해 원래라면 강한 참조 사이클이 이루어져야 하지만 weak 키워드로 선언한 Dog 클래스 owner속성으로 인해 RC값이 인스턴스 간 참조로 증가하지 않게 된다.

위 코드에서 bori = nil이라는 코드는 Dog클래스로 생성된 bori 인스턴스의 RC값을 하나 감소시키게 되는데, 이후로 RC값을 0이 되므로 ARC모델에 따라 힙 메모리에서 해당 인스턴스는 사라지게 된다.

따라서 기존에 bori 인스턴스를 참조하던 gildong?.pet의 경우에도 nil로 다시 초기화 되는 것을 알 수 있다.

# 2. 비소유 참조(Unowned Reference)

class Dog1 {
    var name: String

    // unowned
    unowned var owner: Person1?

    init(name: String) {
        self.name = name
    }

    deinit {
        print("\(name) 메모리 해제")
    }
}

class Person1 {
    var name: String

    // unowned
    unowned var pet: Dog1?

    init(name: String) {
        self.name = name
    }

    deinit {
        print("\(name) 메모리 해제")
    }
}
var bori1: Dog1? = Dog1(name: "보리1")
var gildong1: Person1? = Person1(name: "홍길동1")


bori1?.owner = gildong1
gildong1?.pet = bori1

bori1 = nil
print(gildong1?.pet) // FATAL ERROR!

비소유 참조는 약한 참조와 다르게 RC값이 0이 되면서 인스턴스가 사라짐과 동시에 nil로 초기화 되지 않는다. 실제 메모리를 뒤져보고 메모리 주소가 비어있음을 인지하고 에러를 발생시킨다.

다음 문서는 (opens new window) 시간날때 꼭 다시 찬찬히 읽어보기

상수/변수 선언 가능 여부 구분하기

  1. strong (기본 변수)
    • let / var
    • Optional / Non-optional 넷다 상관 없음
  2. weak
    • let (불가능) / var (가능) - 인스턴스 소멸 시점에 nil로 재초기화 해야하기 때문
    • Optional(가능) / Non-Optional (불가능) - 인스턴스 소멸 시점에 nil로 재초기화 해야하기 때문
  3. unowned
    • let / var 둘다 가능 - 인스턴스 소멸되더라도 nil로 초기화 안됨
    • Optional / Non-optional - 둘다 가능

# 클로저와 메모리 관리

클로저 메모리 구조 (opens new window) 내용 정리본을 다시 복습하고 오자

var num = 1

let valueCaptureClosure = {
    print("밸류값 출력(캡처): \(num)")
}
valueCaptureClosure() // 밸류값 출력(캡처): 1

num = 7
valueCaptureClosure() // 밸류값 출력(캡처): 7

num = 1
valueCaptureClosure() // 밸류값 출력(캡처): 1

valueCaptureClosure라는 변수는 먼저 스택에 쌓인다. let valueCaptureClosure의 우항에 전달된 클로저는 함수이며, print("밸류값 출력")에 해당하는 코드 영역의 명령어 묶음이 힙 메모리에 저장되고 해당 메모리 주소를 valueCaptureClosure라는 변수가 참조하는 형태가 된다.

이때 valueCaptureClosure가 참조하는 클로저는 외부 변수 num을 사용해야 하기 때문에 클로저가 힙 메모리에 저장될 때에 클래스의 인스턴스 형태처럼 num변수의 참조값을 함께 저장하게 된다. 이를 값타입의 참조를 캡처한다고 한다.

따라서 num값에 변화를 주고 valueCaptureClosure가 참조하는 함수 호출을 하면 변화된 num값을 그대로 반영하여 출력하게 되는 것이다. 이것이 클로저의 캡처 현상이다.

# 값 타입의 캡처리스트 사용

먼저 캡처리스트 적용 문법 형태는 아래와 같다.

// 파라미터가 없을때
let capture1 = {[캡처리스트] in
    // 캡처리스트 활용 동작
}

let capture2 = {[캡처리스트](파라미터) -> 리턴타입 in
    // 캡처리스트 활용 동작
}

위의 valueCaptureClosure클로저를 캡처리스트 적용 형태로 바꾸면 아래와 같은 코드가 작성된다.

var num = 1
let valueCaptureListClosure = {[num] in
    print("밸류값 출력 캡처리스트: \(num)")
}

valueCaptureListClosure() // 1

num = 2
valueCaptureListClosure() // 1

valueCaptureListClosure라는 클로저는 여전히 힙 메모리에 함수 내부 명령어 묶음을 저장하고 있고 num이라는 변수도 함께 클로저에 저장하게 되지만 num이라는 변수의 참조를 저장하지 않고 클로저 생성 당시에 num 변수 내에 저장되어 있던 값을 그대로 복사하여 저장하게 된다.

따라서 위의 코드에서 num = 2로 수정했음에도 초기 클로저 실행 단계에서 값이 1로 복사되어 클로저 내에 저장되었기 때문에 1값을 그대로 사용하게 된다.

# 참조 타입의 캡처리스트 사용

class SomeClass {
    var num = 0
}


var x = SomeClass()
var y = SomeClass()

let refTypeCapture = { [x] in
    print("참조 출력값(캡처리스트):", x.num, y.num)
}

위 코드에서 refTypeCapture 클로저의 캡처리스트에는 SomeClass 인스턴스를 저장하는 x변수만 등록되어 있다. C언어 학습 과정에서 배우는 더블포인터 개념을 상기하면 좋다.

변수 x는 그 자체로 참조값을 갖는 포인터 변수인 것이고 캡처리스트에 등록됨에 따라 refTypeCapture클로저에 변하지 않는 형태로 인스턴스의 주소를 저장하게 된다.

반대로 변수 y의 경우 역시 그 자체로 참조값을 갖는 포인터 변수이지만 클로저 내에서는 참조타입 y변수를 다시 참조하는 메모리 주소값을 클로저 내에 저장하게 되는 것이다.

두 변수 x와 y가 클로저 내에서 참조하는 형태의 차이점을 정리하면 x변수는 클로저에서 힙 내부의 SomeClass 인스턴스를 직접 참조하는 형태이고, y변수는 클로저에서 스택에 저장된 y변수를 한번 참조한 뒤 y변수 내에 저장되어 있는 SomeClass인스턴스 메모리 주소로 참조를 한번 더 하는 형태인 것이다.

힙 메모리 내에서 다른 힙 메모리의 인스턴스를 참조하는 상황은 결국 강한참조 사이클을 유발할 수 있는 상황인 것이다! (물론 위 예시가 매번 나쁜 상황은 아니다. 클로저 생명주기 이상으로 인스턴스를 살려놓아야 하는 경우 RC값을 증가시켜놔야함.)

위 예시 코드는 클로저 내의 x변수가 SomeClass인스턴스를 참조하고 있기는 하지만 SomeClass 인스턴스에서 다시 클로저 내의 x변수를 참조하지 않기 때문에 강한 참조 사이클을 유발하는 예시는 아니다.

캡처리스트 내에서 참조타입 변수에 대해 약한참조 및 비소유참조 선언하는 방법은 아래와 같다.

let s = SomeClass()

// 캡처리스트에서 바인딩 가능
let refTypeCapture1 = { [weak z = s] in
    print("참조 출력값(캡처리스트):", z?.num)
}

// 캡처리스트에서 바인딩 가능
let refTypeCapture2 = { [unowned z = s] in
    print("참조 출력값(캡처리스트):", z.num)
}

# 일반적인 클로저 사용

class Dog {
    var name = "초코"

    func doSomething() {
        DispatchQueue.global().async {
            print("나의 이름은 \(self.name)입니다.")
        }
    }
}

var choco = Dog()
choco.doSomething()

// ===== 아래와 같이 작성해도 됨 =====
class Dog {
    var name = "초코"

    func doSomething() {
        // 이렇게 작성해도 됨
        DispatchQueue.global().async { [self] in
            print("나의 이름은 \(name)입니다.")
        }

        //  ===== 강한 참조 싸이클 방지를 위한 코드 =====
        DispatchQueue.global().async{ [weak self] in
            // 동작..
            // self?.name으로 접근!
        }
    }

    func doSomething2(){
        // self는 약한참조라서 nil로 초기화 되어 있을 수도 있다
        DispatchQueue.global().async{ [weak self] in
            // guard let을 통해 nil에 대해 early exit 처리
            guard let weakSelf = self else {return}

            print("\(weakSelf.name)!")
        }
    }
}

위 코드의 doSomething 메서드는 내부에서 DispatchQueue를 활용하여 클로저를 정의하고 있다. 이 클로저는 인스턴스의 name 속성에 접근하고 있는데 이때 주의할 점은 인스턴스 메서드 내의 클로저에서 자기 자신 인스턴스 속성에 접근하기 위해서는 self 키워드를 반드시 붙여야 한다는 것이다. (구조체는 self 생략 가능)

위 코드가 실행된 후 choco 변수에는 Dog클래스에 대한 인스턴스 참조값이 저장된다.

choco가 참조하는 인스턴스의 doSomething메서드가 호출되면 DispatchQueue에서 쓰레드 하나를 할당하여 스택 하나를 추가한다.

이때 DispatchQueue에서 생성하는 클로저는 힙 메모리에 저장되고 클로저의 self속성은 choco가 참조하는 인스턴스를 참조하게 된다.

새로 생성된 스택에서 DispatchQueue에서 생성한 클로저가 실행된 후 클로저 내부 print("나의 이름은 ~ ")코드가 실행되고, 이후 스택 프레임 pop out과 함께 클로저까지 종료된다.

새로운 스택의 모든 스택 프레임 pop out 이후에 클로저도 함께 힙 메모리에서 deallocation된다.

# 메모리 누수 사례

class Dog {
    var name = "초코"

    var run: (() -> Void)?

    func walk() {
        print("\(self.name)가 걷는다.")
    }

    func saveClosure() {
        // 클로저를 인스턴스의 변수에 저장
        run = {
            print("\(self.name)가 뛴다.")
        }
    }

    deinit {
        print("\(self.name) 메모리 해제")
    }
}

func doSomething() {
    let choco: Dog? = Dog()
    choco?.saveClosure()
    // choco?.run = nil -> 주석 풀면 메모리 해제됨
    // 1. saveClosure()함수 실행
    // 2. 클로저 print("self.name가 뛴다")가 힙 메모리에 저장
    // 3. 해당 클로저는 self -> 자기 자신 인스턴스를 참조 (name속성에 접근하고 있기 때문)
    // 4. self 인스턴스의 run 속성은 다시 클로저를 참조
}
doSomething()

위 코드에 직접 달아둔 주석을 다시 복습해보자! (메모리 구조 머리속으로 상상해보기)

choco인스턴스의 run속성을 nil로 초기화하면 클로저의 RC가 0이되어 인스턴스가 사라지고 이에 따라 다시 self를 참조하는 클로저가 nil이므로 self도 RC가 0이 된다.

또는 saveClosure 코드를 아래와 같이 수정해도 된다.

    func saveClosure() {
        // 클로저를 인스턴스의 변수에 저장
        run = { [weak self] in
            print("\(self?.name)가 뛴다.")
        }
    }

실제로 사용될만한 예시 코드는 아래와 같다.

class ViewController1: UIViewController {

    var name: String = "뷰컨"

    func doSomething() {
        // 강한 참조 사이클이 일어나지 않지만, 굳이 뷰컨트롤러를 길게 잡아둘 필요가 없다면
        // weak self로 선언
        DispatchQueue.global().async { [weak self] in
            guard let weakSelf = self else { return }
            sleep(3)
            print("글로벌큐에서 출력하기: \(self?.name)")
        }
    }

    deinit {
        print("\(name) 메모리 해제")
    }
}


func localScopeFunction1() {
    let vc = ViewController1()
    vc.doSomething()
}

localScopeFunction1()
print("HELLO..")

위 코드를 실행하면 HELLO...가 먼저 실행된 후 3초가 지난 뒤에 "글로별큐에서 출력하기"가 실행된다.

위 코드에서 강한 참조 싸이클이 인스턴스와 클로저 서로를 참조하는 형태로 이루어지는 것이 아니라 멀티쓰레드 환경에서 RC값이 줄지 않아 메모리가 불필요하게 힙 공간을 차지하는 것이 문제이다.

약한 참조로 클로저의 인스턴스 참조가 RC값을 증가시키지 않게 된다면 localScopeFunction1함수가 스택프레임에서 pop out됨과 동시에 인스턴스는 소멸되고 RC값이 0이 된다. 이후 클로저에서 참조하는 self도 nil이 되지만, 클로저에서 guard let을 통해 클로저 내에 새로운 참조를 할당하면 RC값을 다시 증가시키기 때문에 메모리 해제가 안되는 것은 동일하다.

# 메모리 안전

멀티쓰레드 환경에서 동일한 데이터에 동시에 접근하여 race-condition이 발생할 수 있는 상태를 Thread-safe 하지 않다고 한다.

이 상태는 멀티쓰레드 환경에서만 발생하는 것이 아니다. 싱글 쓰레드에서도 한 데이터에 대한 참조가 지속적으로 이루어지고 있는 상황에서 동일 쓰레드 내의 다른 시점에 참조가 이루어질때 발생한다.

이러한 상황은 몇가지 존재한다.

// 상황 1. 자기 자신을 참조하여 읽으면서 동시에 쓰고있음
var stepConflict = 1

func increment(_ number: inout Int) {
    number += stepConflict    // number = number + stepConflict
}

increment(&stepConflict) // 에러

stepConflict는 자기 자신을 참조하여 읽으면서 쓰고 있다. 주소값 참조를 통해 원본 값을 변경할때 함수 내에서 동작이 이루어진다면 값을 복사하여 사용하자.

// 상황 2. 동일 함수에 대해 파라마터에 동일한 변수를 전달할때
func balance(_ x: inout Int, _ y: inout Int) {  // 평균값 설정하는 함수
    let sum = x + y
    x = sum / 2
    y = sum - x
}

var playerOneScore = 42

// 입출력 파라미터로 동일한 변수를 전달하고 있음
// 메모리 하나에 동시에 참조
balance(&playerOneScore, &playerOneScore)   // 에러 발생 ⭐️
// 상황 3. 튜플 동시접근
var tuple = (prop1: 10, prop2: 20)

// 튜플에 대한 동시접근 문제
play(&tuple.prop1, &tuple.prop2) // 에러

튜플과 동일하게 인스턴스에 대해서도 동시접근이 이루어지면 에러가 발생한다. 전역이 아닌 지역변수로 만들어 사용하는 것은 괜찮다.

func someFunction() {
    var yosi2 = Player(name: "요시2", health: 10, energy: 10)
    balance(&yosi2.health, &yosi2.energy) // 지역변수라 괜찮음
}

너무 궁금해하지는 말자

# Reference

  1. 인프런 - 앨런 swift 문법 마스터 스쿨 (opens new window)
  2. Side Table in Swift (opens new window)