본문 바로가기
FRONT-END/iOS

Swift 메모리 관리(ARC)

by 랄라J 2023. 8. 6.

메모리의 구조는 코드, 데이터, 힙, 스택으로 구성되어있다.

코드 영역에는 컴파일된 코드가 들어있다.

데이터 영역에는 전역 변수 혹은 타입 변수 중 클래스와 구조체가 들어있다. 공통으로 공유하기 위한 데이터가 저장되는 영역이다.

힙 영역에는 동적 할당되는 크기가 크고, 관리할 필요가 있는 데이터가 저장되는 영역이다.

스택영역에는 함수가 실행되기 위한 임시적인 공간이다. 스코프가 종료되면 자동으로 제거되기에 관리가 필요없는 영역이다.

 

Swift에서 값 형식은 필요 시 항상 메모리의 값이 복사되어 저장되는데 stack 영역에 저장된다.

그리고 값이 들어있는 스코프가 종료되면 메모리에서 자동으로 제거된다.

 

하지만, 참조 형식은 필요 시 항상 메모리의 주소를 저장하고 값은 heap 영역에 주소는 stack 영역에 저장된다.

그리고 RC(Reference Counting)을 통해 메모리를 관리한다. RC는 Swift에서 사용하는 ARC 모델이다.

heap 영역에 할당되는 데이터는 관리를 해줘야 메모리에서 해제된다. 할당이 해제되지 않으면 메모리 누수(Memory leak)가 발생한다

  

다른 언어의 메모리 관리 모델과 비교해보자.

Java에서는 GC(Gabage collector)을 사용하는데, 이는 런타임에 주기적으로 실행되어 사용되지 않는 힙 영역의 메모리를 자동으로 해제한다. 그래서 개발자가 별도로 관리할 필요가 없다. 단, 이러한 모델 등으로 자바라는 언어는 속도가 느리다는 단점이 있다.

SwiftARC(Automatic Reference Counting)를 사용한다. 참조 숫자를 세는 것을 통해 메모리 관리 및 컴파일시 메모리 해제 시점을 결정한다. 이것을 수동으로 하는 모델이 MRC(Manual RC) 자동으로 하는 것이 ARC 모델이다. 

Object-C에서는 MRC 모델을 사용할 수 있다. C언어 계열도 다 수동으로 관리해줘야 한다. 개발자가 직접 하기 때문에 휴먼에러 즉, 실수할 가능성이 높은 단점이 있다. 이러한 단점을 개선하기 위해 현대적 언어들은 대부분 자동 메모리 관리 모델을 사용한다.

 

Swift에서는 Class와 같이 힙에 저장되는 데이터를 만들 때, refCount라는 변수를 자동으로 하나 더 생성한다. 그 후 인스턴스를 생성할 때 해당 변수의 카운터를 하나 더한다. retain() 함수가 레퍼런스 카운팅을 하나 높이는 역할이다. release() 함수는 레퍼런스 카운팅을 하나 줄이는 역할이다. 이는 스위프트의 컴파일러가 실제로 retain() 할당하고 release() 해제 코드를 삽입한다.

 

ARC 모델의 기반은 소유정책과 참조카운팅이다. 소유정책은 인스턴스가 하나 이상의 소유자가 있는 경우 메모리에 유지되고 반대로 소유자가 없으면 메모리에서 제거된다는 것이다. 참조카운팅은 인스턴스(나)를 가리키는 소유자 수를 카운팅하는 것이다. 즉, 인스턴스를 가리키고 있는 참조 카운트가 1 이상이면 메모리에 유지되고, 0이되면 메모리에서 제거되는 것이다.

 

메모리 누수란  메모리 해제가 되지 않고, 메모리를 해제할 방법도 없는 경우를 의미한다. 이러한 메모리 누수는 강한 참조 사이클의 경우 발생한다. 코드가 아닌 문장으로 설명해보자면 Dog라는 클래스, Person이라는 클래스 각각의 인스턴스를 생성 후 Dog로 만들어진 인스턴스의 owner 속성에 Peson으로 만들어진 인스턴스를 넣고, Person으로 만들어진 인스턴스의 pet 속성에 Dog로 만들어진 인스턴스를 넣는 경우 코드 상에서 각각의 인스턴스에 nil을 선언하더라도 레퍼런스 카운트가 1로 여전히 남아있어 메모리에서 해제되지 않는다. 이런 경우를 강한 참조 사이클이라고 한다.

 

이러한 메모리 누수 문제를 해결하기 위한 방법Weak Reference(약한 참조) 방식과  Unowned Reference(비소유 참조) 방식이 있다. 이 두가지 방식의 공통점은 가리키는 인스턴스의 RC 숫자를 올라가지 않게 하는 것이다. 즉, 인스턴스의 강한 참조를 제거하는 것이다. 위 두가지 방식의 키워드로 선언된 변수를 통해 인스턴스 접근은 가능하지만, 인스턴스를 유지시키는 것은 불가능하다.

Weak Reference(약한 참조) 방식의 경우 weak 키워드를 사용한다. 약한 참조의 경우 가리키고 있던 인스턴스가 사라지면 nil로 초기화된다. 소유자에 비해 보다 짧은 생명주기를 가진 인스턴스를 참조할 때 주로 사용한다. weak으로 선언하는 변수 키워드는 변할 수 있어야 하기 때문에 var로 선언해줘야 한다. 또한 nil을 할당 할 수 있어야 하므로 옵셔널 타입으로 선언되어야한다. 

Unowned Reference(비소유 참조) 방식은 unowned 키워드를 사용한다. 참고로 Swift 5.3 이전 버전에서는 unowned 키워드를 붙이는 경우 해당의 타입이 옵셔널 타입이라면 선언이 되지 않았다. 이후 버전에는 옵셔널 타입이 선언될 수 있도록 변경되었다. Weak Reference와 달리 가리키고 있던 인스턴스가 사라져도 nil로 초기화되지 않는다. 소유자보다 인스턴스의 생명주기가 더 길거나 같은 경우 사용한다.

[참고] unowned 사용 시 하나 더 고민할 점이 있기 때문에 실제로는 weak 키워드를 사용해 약한 참조를 많이 사용한다. 참고로 이론적으로는 unowned가 조금 더 빠르다.

 

클로저와 메모리관리를 살펴보자면 캡처리스트라는 개념이 필요하다.

우선 캡처현상은 클로저는 클로저의 주기동안 사용이 필요 없어질 때까지 Heap 영역에 존재해야하고, 클로저 내부에서 외부에 존재하는 변수를 계속 사용하기 때문에 발생하는 현상을 의미한다.

캡쳐리스트의 사용법은 아래와 코드와 같다.

// 파라미터가 있는 경우
{[캡처리스트] in 
	print("a")
}

// 파라미터가 없는 경우
{[캡처리스트] (파라미터) -> 리턴형 in 
	print("a")
}

캡처리스트를 사용하는 이유는 2가지가 있다.

  1. 값 타입은 값을 복사/캡쳐해 외부적인 요인에 의한 값 변경 방지를 하고자 할 때 사용한다. 캡처리스트를 사용하지 않는 경우에는 힙에 저장되는 클로저의 내부 변수 정보로 변수의 주소를 저장해 외부에서 값을 변경하면 변경된다. 하지만 이를 막기 위해 캡쳐리스트에 해당 변수를 지정하는 경우 값 자체를 복사해 저장하기 때문에 외부에서 값을 변경하는 경우에도 값이 변하지 않는다.
  2. 참조 타입은 캡처리스트 내에서 weak, owned 키워드 선언이 가능해 강한 참조 현상 해결이 가능하다. 캡처리스트를 사용하지 않는 경우에는 힙에 저장되는 클로저의 내부에는 인스턴스가 저장된 변수의 주소가 저장되어 참조가 변수 주소로 이동한 후 변수에 저장된 인스턴스 주소를 통해 인스턴스에 접근하게 된다. 캡처리스트를 사용하는 경우에는 힙에 저장되는 클로저  내부에는 인스턴스가 저장된 변수의 주소가 아닌 인스턴스 주소를 복사해 담게 된다. 이 방식은 RC가 올라가는 단점으로 강한 참조가 발생할 가능성이 생기기 때문에 RC가 올라가지 않게 하기위해 weak, owned 키워드를 함께 사용해 이 문제를 방지한다. 위 키워드 중 weak 키워드와 함께 사용하는 경우 nil이 할당될 수 있어야 하기 때문에 옵셔널 타입으로 변경된다는 점을 기억해야한다. 

[참고] 캡처리스트는 바인딩도 가능하다. 

 

객체 내에서 클로저를 사용하는 경우가 일반적이다. 클로저 내부에서 객체의 속성 및 메서드에 접근하기 위해서는 self 키워드를 반드시 사용해야한다. 강한 참조를 하고 있음을 나타내는 용도이다. 

이렇게 객체 내에서 사용되는 클로저는 클로저가 실행되는 동안 오랫동안 객체를 사용해야 하므로 힙 영역에 클로저가 저장되고, 사용할 객체의 주소를 함께 보관한다.  

class Dog {
    var name: String = "bori"

    // Swift 5.3 이전 방식, 구조체도 붙여줘야했음
    func dosomething() {
    	DispatchQueue.global().async {
        	print("이름은 \(self.bori) 입니다.")
        }
    }
    
    // Swift 5.3 이후 방식, 구조체에서는 self 생략 가능
    func dosomething() {
    	DispatchQueue.global().async { [self] in
        	print("이름은 \(  bori) 입니다.")
        }
    }

}
반응형

댓글