ARC

박형석·2021년 12월 13일
0

Swift

목록 보기
19/20
post-thumbnail

메모리 관리 기법

참조 타입은 하나의 인스턴스가 참조를 통해 여러 곳에서 접근하기 때문에 언제 메모리에서 해제가 되는지가 중요한 문제이다. 메모리가 해제되지 않을시, 메모리 자원 낭비 및 성능저하로 이어지기 때문. 스위프트는 프로그램의 메모리 사용을 관리하기 위하여 메모리 관리 기법인 ARC를 사용한다. 당연히 ARC는 참조 타입인 클래스에만 적용된다.

MRC vs ARC
Objective-C에서는 개발자가 직접 참조 관리를 했다. 그래서 Manual Reference Counting(MRC)라고 한다. 레퍼런스 증가에는 alloc, new, copy, mutableCopy, retain 등을 사용, 감소에는 release 등을 사용했다고 한다. 개발자가 적절한 곳에 증가 감소 코드를 사용해서 관리. 하지만 ARC는 컴파일 단계에서 자동으로 구문을 분석해서 해당 코드를 삽입해준다.

ARC vs Garbage Collection
가장 큰 차이는 참조를 계산하는 시점. ARC는 인스턴스가 메모리에서 해제되어야 하는 시점은 컴파일과 동시에 결정한다. GC는 런타임에 동적으로 감시하고 있다가 더이상 사용할 필요가 없다고 여겨지는 것을 해제한다.
ARC의 장점은 컴파일 당시 이미 인스턴스의 해제 시점이 정해져 있기 때문에, 메모리 해제 시점이 비교적 분명하고 메모리 관리를 위해 시스템 자원을 추가할 필요가 없다. 하지만 작동 규칙을 모르고 사용하면 인스턴스가 메모리에서 영원히 해제되지 않을 가능성이 있다.
GC의 장점은 상호 참조와 같은 복잡한 상황에서도 특별한 규칙없이 인스턴스를 적절히 해제할 수 있다는 것이다. 하지만 메모리 감시를 위한 추가 시스템 자원이 필요하고 언제 메모리에서 해제될지 예측하기 어렵다.

ARC란?

자동으로 메모리를 관리해주는 방식. 더이상 필요하지 않은 클래스의 인스턴스를 메모리에서 해제하는 방식으로 작동한다. 하지만 ARC가 '이 필요'를 어떻게 알까? ARC는 이 상황을 '참조 횟수(Reference Counting)'로 판단한다. 이 참조 횟수로 참조 여부를 계속 추적하고 0이 되었을시 메모리에서 해제한다. 그렇기 때문에 개발자는 ARC가 참조를 증가 감소하는 일련의 규칙을 잘 알아야 한다. 그래야 예상치 못한 메모리 누수 문제를 방지할 수 있다.

강한 참조

인스턴스가 계속해서 메모리에 남아있어야 하는 명분을 만들어주는 것이 강한 참조다. 강한 참조는 해당 인스턴스를 다른 인스턴스의 프로퍼티나 변수, 상수 등에 할당할 때 별도의 식별자 없이 기본적으로 사용되는 방식이다. RC += 1. 강한 참조를 사용하는 프로퍼티에 nil을 할당하면 RC -= 1 된다. 지역변수 전역변수의 영향을 받아 함수가 해제되는 시점에 지역변수도 해제된다.

강한 참조 순환 문제(Strong Reference Cycle)

인스턴스끼리 서로가 서로를 강한 참조할 때 일어난다. 두 인스턴스 모두가 생성시 += 1, 다른 인스턴스에 사용시 += 1. 때문에, 기존 인스턴스에 nil을 할당(-= 1)하더라도 0이되지 않기 때문에 메모리에서 해제되지 않는다. 또 더 접근할 방법이 없기 때문에 영원히 해제되지 않는다. (좀비, 메모리릭)

약한 참조

강한 참조와 달리 자신이 참조하는 인스턴스의 참조 횟수를 증가시키지 않는다. (weak 키워드 사용) 약한 참조를 사용하는 경우는 자신이 참조하는 인스턴스가 메모리에서 해제될 수도 있다는 것을 예상하고 사용해야 한다.

약함 참조는 nil을 보장하고 이에 따른 값의 변화를 예상하기 때문에 옵셔널 변수만 사용할 수 있다.

미소유 참조

동일하게 참조 횟수를 증가시키지 않고 참조할 수 있다. 하지만 약한 참조와는 다르게 참조하는 인스턴스가 항상 메모리에 존재할 것이라 전제한다. 즉 자신이 참조하는 인스턴스가 메모리에서 해제되더라도 nil을 할당하지 않는다. 그렇기 때문에 굳이 옵셔널 변수일 필요도 없다. 만약 미소유 참조를 한 인스턴스가 해제가 된 상태고 그 인스턴스에 접근한다면 런타임 오류가 발생한다. 신용카드와 사람(명의자)의 관계 같은 경우에 사용할 수 있다.

미소유 옵셔널 참조

unowned var nextSubject: Subject?

이 경우는 약한 참조처럼 사용할 수 있다. 하지만 유효한 객체를 가리키지 않을 때는 nil을 직접 할당해줘야 한다. 만약 RC가 1인 과목이 위의 미소유 참조와 연결된 참조라면 해당 인스턴스를 해제할 때는 위의 nextSubject에도 nil을 할당해야 한다. 그래야 참조시 오류가 발생하지 않는다.

미소유 참조와 암시적 추출 옵셔널 프로퍼티

서로 참조해야 하는 프로퍼티에 값이 꼭 있어야 하면서도 한번 초기화되면 그 이후에는 nil을 할당할 수 없는 조건을 갖추어야 하는 경우. (강한 참조 순환을 해결하면서도....)

class Company {
 // 암시적 추출 옵셔널 프로퍼티 (강한 참조)
 var ceo: CEO!
}

class CEO {
 // 미소유 참조 상수 프로퍼티 (미소유 참조)
 unowned let company: Company
}

회사 창립에 ceo는 꼭 있어야 함(!). 또 CEO는 회사가 없다면 최고경영자가 아님(unowned). 즉 회사를 초기화 할 때 CEO 인스턴스가 생성되면서 프로퍼티로 할당되어야 하고, 회사가 존재하는 동안 ceo는 항상 존재해야 한다. 한편 CEO는 회사가 있는 경우에만 초기화할 수 있음(회사를 운영해야만 ceo기 때문) 또 회사가 사라지면 CEO도 없기 때문에 미소유 참조..

문제가 되는건 CEO 인스턴스를 생성할 때 초기화된 회사를 받아야만 하는데 이 회사를 만들기 위해서는 또 ceo를 초기화해야 하는 이상한 상황이 벌어진다. 자신을 초기화해야 self를 호출할 수 있는 2단계 초기화 조건 때문에 그냥 구현은 어려운 상황. 그래서 위처럼 미소유 참조와 암시적 추출 옵셔널 프로퍼티를 함께 사용하는 것.

클로저의 강한참조 순환

인스턴스 사이에서 일어나는 것 외에 클로저가 인스턴스의 프로퍼티 일 때나 값 획득 특성 때문에도 발생한다. 예를 들어, 클로저 내부에서 self.something으로 접근할 때 self를 획득하므로 강한 참조 순환이 발생한다.

전에도 이야기했던 것처럼 클로저 내부의 value capture 자체는 복사이지만, 클로저는 참조 타입이다. 클로저를 프로퍼티에 할당하면 참조가 할당된다. 때문에 강한 참조 순환이 발생되는 것이다.

어떻게 발생되는가?

클래스 인스턴스 안에 있는 클로저를 호출하면 그 때 클로저는 자신 안에 있는 참조 타입 변수 등을 capture한다. 문제는 여기서 시작된다. 클로저는 자신이 호출되면 언제든지 자신 내부의 참조들을 사용할 수 있도록 참조 횟수를 증가시켜 메모리에서 해제되는 것을 방지하는데, 이때 자신을 프로퍼티로 갖는 클래스 인스턴스의 참조 횟수도 증가시킨다. 클래스 인스턴스는 클로저를 강한 참조로 가지고, 클로저는 클래스 인스턴스를 강한 참조로 가지게 되는 순환 참조가 일어나게 되는 것이다.

어떻게 해결할까? 획득 목록(Captrue List)

획득목록이란 클로저 내부에서 참조 타입을 획득하는 규칙을 제시할 수 있는 기능이다. 예를 들어, self 참조를 약한 참조, 미소유 참조, 강한 참조로 지정할 수 있다. 위의 문제는 이전처럼 self를 약한 참조나 미소유 참조로 해결하면 되기 때문에 해당 self를 획득 목록을 통해 약한 참조 규칙을 적용시키면 해결된다.

획득 목록에 명시한 요소가 참조 타입이 아니라면, 해당 요소들은 클로저가 생성될 때 초기화된다.

var a = 0
var b = 0

let closure = { [a] in
 print(a, b)
 b = 20
}

a = 10
b = 10

closure() // 0 10
print(b) // 20
  • a와 b는 값 타입이다.
  • a는 값 획득목록을 통해 값을 획득했다. b는 획득하지 않았다.
  • 이후에 a와 b 값을 변경한 후 클로저를 실행하면, a는 획득한 값을 가지지만 b는 변경된 값을 사용한다.
  • 왜? 클로저가 생성됨과 동시에 획득목록 내에서 다시 a라는 이름의 상수로 초기화 된 것이기 때문. 외부의 a와 클로저 내부의 a는 별개. 하지만 b는 아님.
class SimpleClass {
 var value: Int = 0
}

var x = SimpleClass()
var y = SimpleClass()

let closure = { [x] in
 print(x.value, y.value)
}

x.value = 10
y.value = 10

closure() // 10 10
  • x와 y는 참조 타입임
  • x는 획득 목록을 통해 값을 획득, y는 별도 명시되지 않음
  • 하지만 결과는 같음.
  • 다만 획득 목록에서 어떤 방식으로 참조할 것인지 결정할 수 있음. 강한 획득, 약한 획득, 미소유 획득

self 프로퍼티를 미소유 참조로 획득하는 경우가 있는데, 이는 해당 인스턴스가 존재하지 않는다면 프로퍼티도 호출할 수 없기 때문이다. 그 내부적으로는 문제가 없지만 만약 프로퍼티로 사용하는 클로저를 다른 곳에서 참조하게 된다면 문제가 생길 수도 잇다.

profile
IOS Developer

0개의 댓글