개념정리
ARC
ARC란참조 타입(Reference Type)의 인스턴스 메모리를 자동으로 관리해주는 기능이다. 참조 타입의 인스턴스를 참조할 때마다 참조 카운트가 증가하며 이를strong참조라고 한다.
a. 인스턴스 생성과 메모리 할당
Reference Type의 인스턴스를 생성하면 메모리의 Heap 영역에 인스턴스의 공간을 할당하고 값을 저장b. 참조 카운트 관리
c. 메모리 해제
d. ARC 동작시기
Memory Leak란 사용이 끝난 인스턴스가 메모리에서 해제되지 않고 남아있는 현상이다. 이는 불필요한 메모리의 사용으로 성능의 저하를 초래하게 된다.
개발자는 메모리 누수를 피하기 위해 코드를 작성할 때 주의를 기울여야 한다. 메모리 누수가 발생하는 대표적인 원인으로는 강한순환참조가 있다.
Strong Reference Cycle란 클래스의 인스턴스에서 프로퍼티가 다른 타입을 참조하고 있는 상황에서 인스턴스가 해제되는 것이다. 즉, 참조카운트가 0이 되지 않았는데 인스턴스가 해제된 상황을 말한다.
import Foundation
class Apt {
var aptName: String
var resident: Person?
init(aptName: String) {
self.aptName = aptName
}
// 인스턴스 해제시 호출
deinit {
print("\(resident?.name)이 \(aptName)에서 퇴실하셨습니다.")
}
}
class Person {
var name: String
var age: Int
var home: Apt?
init(name: String, age: Int) {
self.name = name
self.age = age
}
// 인스턴스 해제시 호출
deinit {
print("\(name)이 \(home?.aptName)에서 퇴실했습니다.")
}
}
var person: Person? = Person(name: "crois", age: 20) // RC: 1
var apt: Apt? = Apt(aptName: "H") // RC: 1
// 순환참조 시작
person?.home = apt // RC: 2
apt?.resident = person // RC: 2
person = nil // RC: 1
apt = nil // RC: 1
// person과 apt 모두 RC가 1씩 남아있지만, 인스턴스가 해제되어 더는 변경할 수 없음
// 메모리 누수 발생
// 출력: crois이 H에서 퇴실하셨습니다.
// 출력: crois이 H에서 퇴실했습니다.
위와 같은 강한순환참조를 해결하려면 참조 카운트가 올라가지 않도록 설정해주면 된다. 참조 카운트를 올리지 않게 하려면 strong 참조가 아닌 weak, unowned를 사용하면 된다.
import Foundation
class Apt {
var aptName: String
weak var resident: Person? // 약한 참조
init(aptName: String) {
self.aptName = aptName
}
// 인스턴스 해제시 호출
deinit {
print("\(resident?.name)이 \(aptName)에서 퇴실하셨습니다.")
}
}
class Person {
var name: String
var age: Int
weak var home: Apt? 약한 순환참조
init(name: String, age: Int) {
self.name = name
self.age = age
}
// 인스턴스 해제시 호출
deinit {
print("\(name)이 \(home?.aptName)에서 퇴실했습니다.")
}
}
var person: Person? = Person(name: "crois", age: 20) // RC: 1
var apt: Apt? = Apt(aptName: "H") // RC: 1
// 순환참조 시작
person?.home = apt // RC: 1 - 참조 카운트가 올라가지 않음
apt?.resident = person // RC: 1 - 참조 카운트가 올라가지 않음
person = nil // RC: 0
apt = nil // RC: 0
// person과 apt 인스턴스가 해제되며 RC도 0이 되어 메모리누수가 발생하지 않음
// 출력: crois이 H에서 퇴실하셨습니다.
// 출력: crois이 H에서 퇴실했습니다.
strong,weak,unowned란 뭘까?
1. strong
- 강한 참조
- Default 값
- 객체를 참조 시작하거나 참조가 끝나면 참조카운트에 변화를 줌
2. weak
- 약한 참조
- 객체를 참조하거나 종료되어도 참조카운트가 변하지 않음
- nil을 허용
3. unowned
- 약한 참조
- 객체를 참조하거나 종료되어도 참조카운트가 변하지 않음
- nil이면 크래시 발생
개념정리
Closure
closure란 함수와 익명함수를 모두 포함하는 함수를 의미한다. 일반적으로func로 정의하는 클로저를 함수라고 하고, 이름을 붙이지 않고 사용하는 익명 함수를 클로저라고 한다.
함수의 파라미터로 전달된 클로저는 보통 함수의 내부에서만 사용할 수 있다. 하지만 @escaping 코드를 사용하여 함수가 종료된 뒤에도 클로저를 호출할 수 있다.
// 일반적인 클로저
func closureTest(closure: () -> Void) {
closure()
}
closureTest {
print("Hello World")
}
// 출력 - Hello World
// escaping 클로저가 필요한 경우
func closureTest(closure: () -> Void) {
// 내부 함수를 1초 뒤에 호출
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
closure() // Error!!
}
}
closureTest {
print("Hello World")
}
// 출력 - Hello World
위의 두번째 상황에서는 클로저를 함수가 호출되고 1초 뒤에 실행하도록 코드를 입력했다. 그러나 1초가 지나기 전에 함수가 끝나버리기 때문에 클로저를 호출할 수 없어서 아래와 같은 오류가 발생한다.
Escaping closure captures non-escaping parameter 'closure'
이 오류는 클로저가 escaping closure가 아니기 때문에 이런 상황에서는 사용할 없다는 오류이다. 클로저는 기본값이 non-escaping이기 때문에 함수가 끝난 뒤에도 클로저를 사용해야 하는 경우에는 반드시 @escaping코드로 클로저를 탈출 가능한 타입으로 선언해주어야 한다.
// escaping 클로저 사용
func closureTest(closure: @escaping () -> Void) {
// 내부 함수를 1초 뒤에 호출
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
closure()
}
}
closureTest {
print("Hello World")
}
// 1초 뒤에 출력 - Hello World
@escaping closure는 주로 비동기적인 작업 후 클로저를 호출할 때 사용한다.
Closure Capture란 클로저가 생성된 코드블록 내에 있는 변수나 상수에 접근할 수 있는 기능을 말한다. 클로저캡처는 참조 형식으로 값을 사용하기 때문에 값이 변경되면 캡처한 값도 똑같이 변경된다.
struct Person {
var name: String
var age: Int
}
func closureCapture() {
var person = Person(name: "crois", age: 20)
let closure: () -> Void {
// 함수 내의 변수인 person을 캡처
print(person.name)
print(person.age)
}
closure()
// 출력 - crois, 20
// person의 값을 변경
person.age = 25
closure()
// 출력 - crois, 25
// Person은 값 타입이지만, 클로저캡처는 참조 형식으로 값을 사용
// 캡처한 person의 값이 변동되면 변경사항이 똑같이 적용됨
}
// 클로저캡처는 클로저 사용 이후의 값을 캡처할 수 없다
func closureCapture() {
var test1 = 1
let closure: () -> Void {
print(test1)
print(test2) // Error
}
var test2 = 2
}
클로저캡처를 할 때 캡처할 대상을 선택할 수 있다. 이를 캡처 리스트라고 하며, 메모리관리를 명시적으로 제어할 수 있는 방법이다.
// class의 참조 방식 정하기
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age= age
}
}
func referenceCapture() {
var person = Person(name: "crois", age: 20)
let closure: () -> Void = { [person] in
print(person.age)
}
closure()
person.age = 25
closure()
}
referenceCapture()
// 출력: 20, 25
// 참조 방식으로 weak, unowned 사용 가능
// weak로 참조할 경우 옵셔널에 감싸진 값이 나옴
// struct의 참조 방식 정하기
struct Person {
var name: String
var age: Int
}
func valueCapture() {
var person = Person(name: "crois", age: 20)
var test = 1
let closure: () -> Void = { [person] in
print(person.age)
print(test)
}
closure()
person.age = 25
test = 2
closure()
}
referenceCapture()
// 출력: 20, 1, 20, 2
// person은 값 타입으로 복사했기 때문에 변경이 없음
// test는 참조캡처를 하여 값 변경
오늘은 스파르타에서 제공해주는 강의에서 ARC와 클로저에 대해 학습했다.
원래는 객체지향프로그래밍이나 타입캐스팅, 동시성 같은 개념도 배웠지만,
내용이 길어 다시 제대로 학습하고 다음번에 정리할 계획이다.
학습내용을 그저 받아적기만 해서는 공부에 도움이 안된다는데, 지금같은 TIL을 쓰는 것이 도움이 될 수 있을지 걱정이다.
앞으로는 배운 내용을 응용하고 활용하는 방향으로 TIL을 작성할 수 있도록 해보려고 한다.