학습 노트/Swift (2021)

152 ~ 158. Memory, Value Type and Reference Type (메모리, 값형식과 참조형식)

걔랑계란 2021. 9. 14. 18:41

Memory Basics

메모리는 0과 1을 저장하는 반도체이다.
전기가 통하면 1, 전기가 통하지 않으면 0을 저장할 수 있다.

Bit

0과 1을 저장할 수 있는 가장 작은 단위를 Bit(비트)라고 부른다.

Bit
0 or 1

Byte

비트 8개를 묶어서 Byte(바이트)라고 부른다.
컴퓨터 데이터 처리의 기본 단위로 사용되며,
양수만 저장 할 때는 0^8개의 경우인 0~255까지,
음수와 양수를 동시에 저장하면 -128~127까지의 범위를 저장할 수 있다.

Byte
               
MSB             LSB

8개의 비트 중 가장 왼쪽의 비트를 MSB(Most Significant Bit)라고 부르며,
가장 오른쪽의 비트를 LSB(Least Significant Bit)라고 부른다.

데이터 저장 방식

Byte의 양수 저장

컴퓨터에 저장되는 모든 데이터는 형식에 상관없이 2진수로 변환되어 저장된다.
예를 들어 30을 저장한다고 가정하자.

양수를 2진수로 변활 할 때는 1이 나올 때까지 2로 나눈 뒤, 최종의 1과 나머지를 역순으로 배열한다.

2 값(몫) 나머지
2 30 0
2 15 1
2 7 1
2 3 1
  1  

따라서 10진수 30의 2진수는 00011110이다

이것을 메모리에 저장하면 다음과 같이 된다.

Byte
0 0 0 1 1 1 1 0
Data Bit

바이트 내에서 실질적으로 데이터가 저장되는 공간을 Data Bit라고 부른다.

Byte의 음수 저장

음수와 양수를 구분하기 위해, 컴퓨터는 바이트를 다음과 같은 형태로 재구성한다.

Byte
               
SB Data Bit

가장 왼쪽의 비트 하나를 SB(Sign Bit)로 취급하여,
0이면 양수, 1이면 음수로 인식한다.

따라서 실질적인 Data Bit는 하나가 줄어 7개가 된다.

구세대 컴퓨터들은 해당 Data Bit에 기존의 양수와 같은 방식으로 저장해 왔지만,
여러 문제가 있어 현행 컴퓨터들은 음수를 저장할 때 '2의 보수'방식을 사용한다.

2의 보수화는 기존의 2진수의 0과 1을 반전시킨 뒤, 1을 더해 완성한다.
위에서 저장했던 30을 2의 보수화 하여 -30을 만드는 과정은 다음과 같다.

과정 Byte
원본 0 0 0 1 1 1 1 0
반전 1 1 1 0 0 0 0 1
+1 1 1 1 0 0 0 1 0

따라서 메모리엔 다음과 같이 저장된다.

Byte
1 1 1 0 0 0 1 0
SB Data Bit

메모리 단위

단위 비교
Bit  
Byte 8 Bit
Kilobyte 1024 Byte
Megabyte 1024 Kilobyte
Gigabyte 1024 Megabyte
Terabyte 1024 Gigabyte
Petabyte 1024 Terabyte
Exabyte 1024 Petabyte
Zettabyte 1024 Exabyte
Yottabyte 1024 Zettabyte

여러 단위가 존재하지만 일반적으로 사용하는 단위는 Terabyte까지이다.
Bit와 Byte는 8배가 차이 나지만, 이외의 단위들은 1024배 차이이다.

메모리 주소

메모리는 1Byte마다 주소를 가지고 있다.

CPU는 자신 내의 MAR(Memory Address Register)를 사용해 메모리의 주소에 접근한다.
또한 이 MAR의 크기에 따라 32bit, 64bit CPU가 구분된다.
이는 각각 최대 4GB, 16EB의 가짓수를 저장할 수 있다.

메모리 공간

운영체제는 프로그램이 실행될 때마다 프로그램이 사용할 메모리 공간을 할당한다.
메모리 공간은 용도에 따라 크게 Code, Data, Heap, Stack 으로 나뉜다.

  • Code
    기계어로 번역된 프로그램 코드가 저장된다.
  • Data
    정적 변수와 전역 변수가 저장된다.
    프로그램이 시작될 때 생성되었다가 프로그램이 끝나면 삭제된다.
  • Heap
    동적으로 할당된 데이터가 저장된다. 따라서 저장공간의 크기를 예측할 수 없다.
    힙에 저장된 데이터는 생성시점과 저장 시점이 정해져 있지 않다. 따라서, 직접 관리를 해야 하는 번거로움이 있다.
    메모리를 정리하지 않는다면 프로그램이 종료될 때까지 유지되고, 이러한 경우를 Memory Leak(메모리 누수)라고 부른다.
  • Stack
    지역변수, 파라미터, 반환 값 등이 저장된다.
    함수를 실행하면 함수에서 사용하는 모든 값을 저장하는 메모리 공간이 생성되며, 이 공간은 Stack Frame(스택 프레임)이라고 부른다.
    스택 프레임은 함수의 실행이 종료되면 자동으로 삭제된다.
    스택은 말 그대로 스택 프레임을 쌓는 구조로 메모리를 저장하기 때문에 삭제할 때는 마지막에 저장된 스택 프레임부터 순차적으로 삭제된다. 이를 LIFO(Last In First Out)이라고 부른다.
분류 Value TypeReference Type
Code    
Data    
Heap   Actual Data
Stack Actual Data
Heap Address

값 형식은 스택에 실제 데이터가 저장된다.
따라서 자동으로 메모리를 관리하게 된다.

참조 형식은 실제 데이터는 힙에 저장되고, 스택에는 힘의 주소가 저장된다.
두 가지 데이터가 다른 곳에 저장되기 때문에 메모리 관리에 신경 써야 한다.

 

Value Type vs Reference Type

Value Type Reference Type
Struct
Enumeration
Tuple
Class
Closure

값 형식에는 구조체, 열거형, 튜플이 해당되고, 참조 형식에는 클래스, 클로저가 포함된다.
가장 사용 빈도가 많은 구조체와 클로저의 메모리 저장 방식에 집중하면서 살펴보자.

struct SizeValue {
	var width = 0.0
	var height = 0.0
}

class SizeObject {
	var width = 0.0
	var height = 0.0
}

두 구조체와 클래스는 동일한 두 개의 인스턴스를 가진다.

struct SizeValue {
	var width = 0.0
	var height = 0.0
}

//-----

var value = SizeValue()

위와 같이 새로운 인스턴스를 생성하면 메모리 구조는 다음과 같이 변한다.

Stack
 
width : 0.0
height : 0.0
 
 
 
struct SizeValue {
	var width = 0.0
	var height = 0.0
}

var value = SizeValue()

//-----

var value2 = SizeValue()

위와 같이 다른 인스턴스를 새로 생성하면 메모리 구조는 다음과 같이 변한다.

Stack
 
width : 0.0
height : 0.0
 
width : 0.0
height : 0.0
 

둘 다 동일한 값을 저장하고 있지만, 개별적인 메모리 구조를 가지고 있다.

struct SizeValue {
	var width = 0.0
	var height = 0.0
}

var value = SizeValue()
var value2 = SizeValue()

//-----

value2.width = 1.0
value2.height = 2.0

위와 같이 value2의 값을 변경하면 메모리 구조는 다음과 같이 변한다.

Stack
 
width : 0.0
height : 0.0
 
width : 1.0
height : 2.0
 

value와 calue2는 별도의 메모리 구조를 가지고 있기 때문에
서로에게 미치는 영향이 없다.

struct SizeValue {
	var width = 0.0
	var height = 0.0
}

var value = SizeValue()


var value2 = value
value2.width = 1.0
value2.height = 2.0

//-----

print(value)
print(value2)
결과

SizeValue(width: 0.0, height: 0.0)
SizeValue(width: 1.0, height: 2.0)

이는 실제로 확인해 봐도 동일한 결과를 볼 수 있다.
단, Swift는 Copy on Write 최적화를 사용하고 있다.
해당 최적화 알고리즘에 따라 메모리 변형 과정은 다를 수 있으며, 알고리즘의 영향을 제외한 일반적인 경우에 한한다.

class SizeObject {
	var width = 0.0
	var height = 0.0
}

var object = SizeObject()

이번엔 클래스를 사용하여 새로운 인스턴스를 생성했다.

Heap Stack
   
width : 0.0
height : 0.0
←0x1234
   
   
   

클래스는 참조 형식이기 때문에 실제 값은 Heap에 저장되고, Stack에는 저장된 Heap의 주소가 저장된다.
따라서 데이터에 바로 접근할 수 없고, Stack을 거쳐야만 접근이 가능하다.

class SizeObject {
	var width = 0.0
	var height = 0.0
}

var object = SizeObject()

//-----

var object2 = object

새로운 인스턴스를 생성하면 메모리 구조가 다음과 같이 변경된다.

Heap Stack
   
width : 0.0
height : 0.0
←0x1234
  ↖︎0x1234
   
   

이번엔 Heap에 새로운 데이터가 저장되지 않고, 기존의 Heap의 주소를 가리키는 Stack이 하나 더 생성된다.
따라서 object나 object2 어느 것을 통하든지 항상 같은 Heap에 접근하게 된다.

class SizeObject {
	var width = 0.0
	var height = 0.0
}

var object = SizeObject()
var object2 = object

//-----

object2.width = 1.0
object2.height = 2.0

이번엔 onject2를 통해 값을 변경했다.
메모리는 다음과 같이 변경된다.

Heap Stack
   
width : 1.0
height : 2.0
←0x1234
  ↖︎0x1234
   
   

object2를 통해 Heap의 값을 변경했다.
하지만 같은 값을 가르키는 object도 이 변화에 영향을 받는다.

class SizeObject {
	var width = 0.0
	var height = 0.0
}

var object = SizeObject()
var object2 = object

object2.width = 1.0
object2.height = 2.0

//-----

object
object2
결과

width 1, height 2
width 1, height 2

object2를 통해 값을 변경했지만, 어느 인스턴스를 통해 접근해도 같은 값을 반환한다.
클래스를 사용하다 접하는 '참조를 전달하다.'. '참조를 복사하다.'라는 표현에서의 참조는 Stack에 저장된 메모리 주소를 의미한다.

이러한 특징은 상수로 선언했을 때 차이를 만들어 낸다.

class SizeObject {
	var width = 0.0
	var height = 0.0
}

let o = SizeObject()

o.width = 1.0
o.height = 2.0

let으로 선언되어 값을 변경하지 못해야 하지만, 이는 Stack에 대한 조건이다.
따라서 Stack에 저장 된 메모리 주소를 바꾸지 못한다는 의미이지, Heap에 저장 된 실제 데이터를 바꾸는 데에는 문제가 없다.
따라서 데이터가 변경된다.

struct SizeValue {
	var width = 0.0
	var height = 0.0
}

//-----

let v = SizeValue()

v.height = 3.0
결과

//error

단, 값 형식의 경우 제한이 실제 저장된 스택에 걸리기 때문에
상수로 선언하면 값을 바꿀 수 없다.

타입에 따른 비교 연산자, 항등 연산자

Value TypeReference Type

Value Type Reference Type
==
!=
==
!=
===
!==

항등 연산자는 값 형식을 비교할 때 Stack에 저장된 값을 비교한다.
반면 참조 형식의 경우 Heap에 저장된 값을 비교한다.
또한, 주소를 따로 저장하기 때문에 스택에 저장 된 주소를 비교할 때는 항등 연산자를 사용한다.

 

ARC (Automatic Reference Counting/메모리 관리 모델)

자동적으로 관리되는 Stack과 다르게 Heap에 저장되는 데이터는 직접 관리해 줘야 한다.
이때 사용하는 것이 ARC이며, 클래스 인스턴스의 메모리를 관리한다.

Objective C Swift
MRC
ARC
ARC

정확히 말하자면 Swift에서 사용하는 Cocoa에서 사용하는 메모리 관리 모델 두 종류 중에 ARC만 사용할 수 있다.

두 모델을 이해하기 위해선 Ownership Policy(소유자 정책)와 Reference Count(참조 카운트)를 이해해야 한다.

인스턴스는 하나 이상의 소유자가 있는 경우 메모리에 유지된다. 즉, 소유자가 없다면 메모리에서 제거된다.
이것을 구분하기 위해 참조 카운트를 사용한다.
따라서 인스턴스는 참조 카운트가 1 이상이면 메모리에 유지되고, 0이면 삭제된다.

class Name {

}

var name = Name()

위와 같이 변수에 인스턴스를 저장하면 변수가 인스턴스의 소유자가 된다.
이때의 인스턴스의 참조 카운트는 1이 된다.

class Name {

}

var name = Name()

//-----

var name2 = name

이렇게 다른 변수가 참조하게 되면 카운트는 2가 된다.

이렇게 인스턴스를 소유하기 위해서는 특별한 메시지를 전달해야 하는데, retain 메소드이다.
인스턴스가 필요하지 않게 됐다면 소유권을 포기해야 하고, 이 때는 release 메소드를 전달한다.
소유자가 release를 전달하면 참조 카운트가 1씩 줄어들게 되고, 0이 되면 메모리에서 삭제된다.

MRC(Manual Reference Counting) 정책을 사용한다면 이러한 소유 정책에 대한 코드를 직접 구현해야 한다.
따라서 난이도와 오류 발생률이 높고, 디버깅이 어렵다는 단점이 존재한다.
이러한 단점을 해결하기 위해 ARC(Automatic Reference Counting)이 도입된다.
이름 그대로 소유 정책을 자동으로 처리하지만 소유 정책에 대한 작동 방식은 MRC와 동일하다.

ARC가 알아서 하기 때문에 내부적으로 깊게 알 필요는 없다.
참조 카운트에 대해 이해하고, ARC가 제공하는 세 가지 참조 방식만 이해하면 된다.

Strong Reference(강한 참조)

가장 기본이 되는 참조 방식이다.
대상을 소유할 때마다 참조카운트가 1 증가하고, 소유를 포기할 때 마다 1씩 감소한다.

class Person{
	var name = "K"
	
	deinit {
		print("person deinit")
	}
}

var p: Person?
var p2: Person?
var p3: Person?

p = Person()

 

위와 같이 Person 인스턴스를 저장할 변수 3개를 만들고,
p에 인스턴스를 저장한다.
이렇게 되면 메모리는 다음과 같이 변한다.

var instance
p→ Person
Instance
(count : 1)
p2
p3

변수 p가 Person 인스턴스를 참조하고, 인스턴스의 카운트가 1 증가한다.

class Person{
	var name = "K"
	
	deinit {
		print("person deinit")
	}
}

var p: Person?
var p2: Person?
var p3: Person?

p = Person()

//-----

p2 = p
p3 = p

이번엔 다른 변수들에도 p를 저장했다.
이때의 메모리 구조는 다음과 같이 변한다.

var instance
p→ Person
Instance
(count : 3)
p2→
p3→

이번에도 변수와 인스턴스는 강한 참조로 연결되고, 참조 카운트도 각각 1씩 증가한다.

class Person{
	var name = "K"
	
	deinit {
		print("person deinit")
	}
}

var p: Person?
var p2: Person?
var p3: Person?

//-----

p = nil
p2 = nil

이번에는 두 개의 변수에 nil을 저장해 소유를 포기했고, 따라서 카운트도 2 감소한다.

var instance
p Person
Instance
(count : 1)
p2
p3→

여전히 참조 카운트가 0이 아니기 때문에 인스턴스는 제거되지 않는다.

class Person{
	var name = "K"
	
	deinit {
		print("person deinit")
	}
}

var p: Person?
var p2: Person?
var p3: Person?

p = nil
p2 = nil

//-----

p3 = nil
결과

person deinit

이번엔 p3에 nil을 저장했다.

var instance
p Person
Instance
(count : 0)
p2
p3

이렇게 되면 최종 소유자인 p3가 소유권을 포기하게 되면서 참조 카운트가 0이 되고,
메모리에서 삭제된다. 따라서 소멸자가 호출되고, 콘솔에 person deinit이 출력된다.

 

Strong Reference Cycle (강한 참조 사이클)

class Person {
	var name = "John Doe"
	var car: Car?
	
	deinit {
		print("person deinit")
	}
}

class Car {
	var model: String
	var lessee: Person?
	
	init(model: String) {
		self.model = model
	}
	
	deinit {
		print("car deinit")
	}
}

Person 클래스는 Car 클래스 형식을 가지고 있다.
또한 Car 클래스는 Person 클래스 형식을 가지고 있다.

class Person {
	var name = "John Doe"
	var car: Car?
	
	deinit {
		print("person deinit")
	}
}

class Car {
	var model: String
	var lessee: Person?
	
	init(model: String) {
		self.model = model
	}
	
	deinit {
		print("car deinit")
	}
}

//-----

var person: Person? = Person()
var rentedCar: Car? = Car(model: "Porsche")

 

var instance
person→ Person
Instance
(count : 1)
retendCar→ Car
Instance
(count : 1)

Peroson 인스턴스와 Car 인스턴스를 각각의 변수에 저장하면,
이들이 강한 참조로 연결되고, 카운트가 1씩 증가한다.

class Person {
	var name = "John Doe"
	var car: Car?
	
	deinit {
		print("person deinit")
	}
}

class Car {
	var model: String
	var lessee: Person?
	
	init(model: String) {
		self.model = model
	}
	
	deinit {
		print("car deinit")
	}
}

var person: Person? = Person()
var rentedCar: Car? = Car(model: "Porsche")

//-----

person?.car = rentedCar

이번엔 person 인스턴스의 car 속성에 rentedCar를 저장한다.

var instance
person→ Person
Instance
(count : 1)
retendCar→ Car
Instance
(count : 2)

이렇게 되면 car 속성과 rentedCar 인스턴스와 강한 참조로 연결되고,
Car 인스턴스의 카운트가 하나 증가하게 된다.

class Person {
	var name = "John Doe"
	var car: Car?
	
	deinit {
		print("person deinit")
	}
}

class Car {
	var model: String
	var lessee: Person?
	
	init(model: String) {
		self.model = model
	}
	
	deinit {
		print("car deinit")
	}
}

var person: Person? = Person()
var rentedCar: Car? = Car(model: "Porsche")

person?.car = rentedCar

//-----

rentedCar?.lessee = person

이번엔 rentedCar의 lessee 속성에 person 인스턴스를 저장한다.
이번에도 마찬가지로 해당 속성이 인스턴스와 강한 참조로 연결되고 Person 인스턴스의 카운트가 하나 증가한다.

var instance
person→ Person
Instance
(count : 2)
retendCar→
Car
Instance
(count : 2)
class Person {
	var name = "John Doe"
	var car: Car?
	
	deinit {
		print("person deinit")
	}
}

class Car {
	var model: String
	var lessee: Person?
	
	init(model: String) {
		self.model = model
	}
	
	deinit {
		print("car deinit")
	}
}

var person: Person? = Person()
var rentedCar: Car? = Car(model: "Porsche")

person?.car = rentedCar
rentedCar?.lessee = person

//-----

person = nil

이번엔 person 인스턴스에 nil을 저장해 소유권을 포기하도록 만들었다.
따라서 Person 인스턴스의 참조 카운트도 1 감소한다.

var instance
  Person
Instance
(count : 1)
retendCar→
Car
Instance
(count : 2)
class Person {
	var name = "John Doe"
	var car: Car?
	
	deinit {
		print("person deinit")
	}
}
	
	class Car {
	var model: String
	var lessee: Person?
	
	init(model: String) {
		self.model = model
	}
	
	deinit {
		print("car deinit")
	}
}

var person: Person? = Person()
var rentedCar: Car? = Car(model: "Porsche")

person?.car = rentedCar
rentedCar?.lessee = person
person = nil

//-----

rentedCar = nil

이번엔 rentedCar 변수에 nil을 저장해 소유권을 포기하게 만들었다.
마찬가지로 Car 인스턴스의 카운트가 하나 감소하게 된다.

var instance
  Person
Instance
(count : 1)
 
Car
Instance
(count : 1)

이렇게 되면 문제가 발생한다.
인스턴스들을 참조했던 변수들이 삭제됐기 때문에 이제는 접근할 방법이 없는 상황이 된 것이다.
따라서 이들을 정상적으로 해제할 수 있는 방법이 없다.
이러한 문제를 Strong Reference Cycle이라고 부른다.

이러한 문제는 Weak Reference(약한 참조))와 Unowned Reference(비소유 참조)로 해결한다.
두 가지 참조 모두 인스턴스 사이의 강한 참조를 제거하는 것으로 문제를 해결한다.
이들은 참조 카운트를 증가시키거나 감소시키지 않는다.
따라서 인스턴스에 접근할 수는 있지만 유지시키는 것은 불가능하다.

Weak Reference (약한 참조)

약한 참조는 인스턴스를 참조하지만 소유하진 않는다. 따라서 참조 카운트도 변하지 않는다.
따라서 인스턴스를 참조하고 있는 동안 해당 인스턴스는 언제든지 사라질 수 있다.
소유자에 비해 짧은 생명주기를 가진 인스턴스를 참조할 때 주로 사용된다.

Synyax

weak var name: Type?

 

약한 참조는 항상 Optional 형식으로 생성되며, weak 키워드를 사용한다.

class Person {
	var name = "John Doe"
	var car: Car?
	
	deinit {
		print("person deinit")
	}
}

class Car {
	var model: String
	weak var lessee: Person?
	
	init(model: String) {
		self.model = model
	}
	
	deinit {
		print("car deinit")
	}
}

var person: Person? = Person()
var rentedCar: Car? = Car(model: "Porsche")

person?.car = rentedCar

이전에 강한 참조 사이클이 발생한 코드이다.
이번엔 lessee 속성을 약한 참조로 설정한 상태이다.
현재까지의 참조 관계는 아래와 같다.

var instance
person→ Person
Instance
(count : 1)
retendCar→ Car
Instance
(count : 2)
class Person {
	var name = "John Doe"
	var car: Car?
	
	deinit {
		print("person deinit")
	}
}

class Car {
	var model: String
	weak var lessee: Person?
	
	init(model: String) {
		self.model = model
	}
	
	deinit {
		print("car deinit")
	}
}

var person: Person? = Person()
var rentedCar: Car? = Car(model: "Porsche")

person?.car = rentedCar

//-----

rentedCar?.lessee = person

다시 rentedCar의 lessee 속성에 person 인스턴스를 저장했다.

var instance
person→ Person
Instance
(count : 1)
retendCar→
Car
Instance
(count : 2)

이번에도 Car 인스턴스의 속성이 다른 인스턴스를 참조하지만, 약한 참조로 선언되어 있기 때문에 참조 카운트는 증가하지 않는다.

class Person {
	var name = "John Doe"
	var car: Car?
	
	deinit {
		print("person deinit")
	}
}

class Car {
	var model: String
	weak var lessee: Person?
	
	init(model: String) {
		self.model = model
	}
	
	deinit {
		print("car deinit")
	}
}

var person: Person? = Person()
var rentedCar: Car? = Car(model: "Porsche")

person?.car = rentedCar
rentedCar?.lessee = person

//-----

person = nil
결과

person deinit

person 변수에 다시 nil을 저장해 소유권을 포기하도록 한다.
이때의 참조 구조는 다음과 같이 변한다.

var instance
  Person
Instance
(count : 0)
retendCar→
Car
Instance
(count : 2)

person 변수가 소유권을 포기했으므로 Person 인스턴스의 참조 카운트가 0이 된다.
따라서 Person 인스턴스도 다음과 같이 삭제되고, person deinit이 출력된다.

var instance
   
retendCar→ Car
Instance
(count : 1)

이 때 Car 인스턴스를 참조하고 있던 Person 인스턴스가 사라졌으므로 Car 인스턴스의 참조 카운트도 1 감소한다.
이때, 약한 참조를 선언한 lessee에는 nil을 반환한다. 이것이 약한 참조를 Optional로 선언하게 되는 이유이다.

class Person {
	var name = "John Doe"
	var car: Car?
	
	deinit {
		print("person deinit")
	}
}

class Car {
	var model: String
	weak var lessee: Person?
	
	init(model: String) {
		self.model = model
	}
	
	deinit {
		print("car deinit")
	}
}

var person: Person? = Person()
var rentedCar: Car? = Car(model: "Porsche")

person?.car = rentedCar
rentedCar?.lessee = person

person = nil

//-----

rentedCar = nil
결과

person deinit
car deinit

마지막으로 rentedCar 변수에 nil을 저장해 소유권을 포기하도록 하면 참조 관계는 다음과 같이 변한다.

var instance
   
   

Car 인스턴스를 참조하던 rentedCar 변수가 사라졌으므로 Car 인스턴스의 참조 카운트가 하나 줄어 0이 된다.
따라서 Car 인스턴스는 메모리에서 삭제된다. 또한 car deinit이 출력된다.
모든 인스턴스가 제거되었다.

Unowned Reference (비소유 참조)

비소유 참조는 약한 참조와 동일한 방식으로 문제를 해결한다.

비소유 참조는 약한 참조와 다르게 nonOptional 형식이다.
참조 사이클을 해결하면서 속성이 nonOptional이어야 하는 경우 사용한다.
인스턴스의 생명주기가 소유자와 같거나 더 긴 경우에 사용한다.

Syntax

unowned var nametype

 

비소유 참조는 unowned 키워드로 선언한다.
Optional 형식이 아니기 때문에 참조하고 있는 인스턴스가 사라져도 nil로 초기화되지 않는다.
따라서 해제된 인스턴스에 접근하려는 경우 오류가 발생한다.

class Person {
	var name = "John Doe"
	var car: Car?
	
	deinit {
		print("person deinit")
	}
}

class Car {
	var model: String
	unowned var lessee: Person
	
	init(model: String, lessee: Person) {
		self.model = model
		self.lessee = lessee
	}
	
	deinit {
		print("car deinit")
	}
}

var person: Person? = Person()
var rentedCar: Car? = Car(model: "Porsche", lessee: person!)

person?.car = rentedCar

person = nil
rentedCar = nil
결과

person deinit
car deinit

이번엔 lessee 속성을 비소유 참조로 변경했다.
이후 인스턴스끼리 서로 참조하도록 동일하게 구성후 동일하게 nil을 저장하면
정상적으로 소멸자를 호출하고 콘솔에 출력하는 것을 볼 수 있다.

 

Unowned Optional Reference

위에서 정리한 비소유 참조는 Optional 형식을 취할 수 없다고 했는데, swift5부턴 지원한다.

System

unowned var nameType?

 

문법은 Optional 선언을 제외하곤 동일하다.

class Person {
	var name = "John Doe"
	var car: Car?
	
	deinit {
		print("person deinit")
	}
}

class Car {
	var model: String
	unowned var lessee: Person?
	
	init(model: String, lessee: Person) {
		self.model = model
		self.lessee = lessee
	}
	
	deinit {
		print("car deinit")
	}
}

따라서 이전 코드에서 Optional 선언만 추가했다.
이렇게 되면 약한 참조와의 차이가 모호해지는데,
둘 다 참조 카운트를 증가시키지 않는다는 것은 동일하다.
하지만 참조하던 인스턴스가 사라졌을 때의 결과는 많이 다르다.

class Person {
	var name = "John Doe"
	var car: Car?
	
	deinit {
		print("person deinit")
	}
}

class Car {
	var model: String
	weak var lessee: Person?
	
	init(model: String, lessee: Person) {
		self.model = model
		self.lessee = lessee
	}
	
	deinit {
		print("car deinit")
	}
}

var person: Person? = Person()
var rentedCar: Car? = Car(model: "Porsche", lessee: person!)

person?.car = rentedCar

person = nil

rentedCar?.lessee
결과

person deinit
-----
nil

Person 인스턴스가 해제되면서 person deinit이 출력되고,
참조하고 있던 rentedCar의 lessee 속성은 nil로 초기화된다.

class Person {
	var name = "John Doe"
	var car: Car?
	
	deinit {
		print("person deinit")
	}
}

class Car {
	var model: String
	unowned var lessee: Person?
	
	init(model: String, lessee: Person) {
		self.model = model
		self.lessee = lessee
	}
	
	deinit {
		print("car deinit")
	}
}

var person: Person? = Person()
var rentedCar: Car? = Car(model: "Porsche", lessee: person!)

person?.car = rentedCar

person = nil
결과

//error

Optional 비소유 참조로 다시 바꾸게 되면 해당 부분에서 오류가 발생한다.
자동으로 초기화되지 않는 것은 일반 비소유 참조와 동일하기 때문에 사라진 주소로 접근하게 되고,
충돌이 발생한다.
따라서 참조 대상이 사라지게 된다면 반드시 직접 nil로 초기화해야 충돌을 막을 수 있다.

class Person {
	var name = "John Doe"
	var car: Car?
	
	deinit {
		print("person deinit")
	}
}

class Car {
	var model: String
	unowned var lessee: Person?
	
	init(model: String, lessee: Person) {
		self.model = model
		self.lessee = lessee
	}
	
	deinit {
		print("car deinit")
	}
}

var person: Person? = Person()
var rentedCar: Car? = Car(model: "Porsche", lessee: person!)

person?.car = rentedCar

person = nil
rentedCar?.lessee = nil

rentedCar?.lessee
결과

person deinit
-----
il

Closure Capture List

클로저에서도 인스턴스와 마찬가지로 강한 참조 사이클 문제가 생긴다.
마찬가지로 약한 참조와 비소유 참조로 문제를 해결하게 된다.

class Car {
	var totalDrivingDistance = 0.0
	var totalUsedGas = 0.0
	
	lazy var gasMileage: () -> Double = {
		return self.totalDrivingDistance / self.totalUsedGas
	}
	
	func drive() {
		self.totalDrivingDistance = 1200.0
		self.totalUsedGas = 73.0
	}
	
	deinit {
		print("car deinit")
	}
}

gasMileage 속성은 지연 저장 속성으로 선언되어 있고, Double을 반환하는 클로저가 저장되어 있다.
그리고 클로저는 'self.'를 통해 인스턴스에 접근하고 있다.
따라서 클로저는 실행이 종료될 때까지 스스로를 강한 참조로 capture 한다.
따라서 클로저가 종료될 때 까지 'self.'가 가리키는 인스턴스는 메모리에서 제거되지 않는다.
또한 클로저는 인스턴스 속성에 저장되어 있다. 따라서 인스턴스는 속성에 저장된 클로저를 강한 참조 한다.
결과적으로 클로저와 인스턴스는 서로 강한 참조 관계이다.

class Car {
	var totalDrivingDistance = 0.0
	var totalUsedGas = 0.0
	
	lazy var gasMileage: () -> Double = {
		return self.totalDrivingDistance / self.totalUsedGas
	}
	
	func drive() {
		self.totalDrivingDistance = 1200.0
		self.totalUsedGas = 73.0
	}
	
	deinit {
		print("car deinit")
	}
}

var c: Car? = Car()
c?.drive()
c = nil
결과

car deinit

아직까지는 클로저가 실행되지 않았고, 따라서 강한 참조 사이클이 생기지 않았다.
따라서 해당 시점에 변수에 nil을 저장하면 정상적으로 소멸자를 호출해 car deinit이 출력된다.

class Car {
	var totalDrivingDistance = 0.0
	var totalUsedGas = 0.0
	
	lazy var gasMileage: () -> Double = {
		return self.totalDrivingDistance / self.totalUsedGas
	}
	
	func drive() {
		self.totalDrivingDistance = 1200.0
		self.totalUsedGas = 73.0
	}
	
	deinit {
		print("car deinit")
	}
}

var c: Car? = Car()
c?.drive()

c?.gasMileage()
결과

16.43835616438356

이번엔 c에 저장된 클로저를 호출했다. 호출과 동시에 클로저는 c를 capture 한다.
매문에 클로저가 c를 강한 참조하게 되고, 강한 참조 사이클이 발생한다.
따라서 c에 nil을 전달해도 소멸자를 호출하지 않는다.

class Car {
	var totalDrivingDistance = 0.0
	var totalUsedGas = 0.0
	
	lazy var gasMileage: () -> Double = {
		return self.totalDrivingDistance / self.totalUsedGas
	}
	
	func drive() {
		self.totalDrivingDistance = 1200.0
		self.totalUsedGas = 73.0
	}
	
	deinit {
		print("car deinit")
	}
}

var c: Car? = Car()
c?.drive()

c?.gasMileage()

c = nil
결과


이를 해결하기 위해 Closure Capture List를 사용한다.

Syntax

{ [list] (parameters) -> ReturnType in
    Code
}
{ [list] in
    Code
}

 

 

클로저 캡처 리스트는 클로저의 파라미터 앞에 '[ ]' 사이에 캡쳐할 값들을 ','로 구별하여 나열한다.
일반 클로저들은 문법 최적화 규칙에 의해 in 키워드를 삭제할 수 있었지만 클로저 캡쳐 리스트를 사용하는 동안엔 불가능하다.

클로저 캡쳐 리스트는 세 가지 형태로 작성될 수 있다.

Value Type

Syntax

{ [valueName] in
    Code
}

 

값 형식의 경우 값의 이름을 나열한다.

var a = 0
var b = 0
let c = { print(a, b)}

c 상수에 저장된 클로저는 a와 b를 캡처하고 있다.
클로저가 값을 캡처할 때는 복사본이 아닌 참조가 전달된다.
따라서 다음과 같이 변수의 값을 바꾼 뒤 클로저를 실행하면 값이 변화한다.

var a = 0
var b = 0
let c = { print(a, b)}

//-----

a = 1
b = 2
c()
결과

1 2

이 상태에서 클로저 캡처 리스트를 추가한다.

var a = 0
var b = 0
let c = { [a] in print(a, b)}

//-----

a = 1
b = 2
c()
결과

0 2

이번엔 클로저 캡처 리스트에서 작성한 a 변수의 값이 변하지 않았다.
클로저 캡쳐 리스트에 추가하게 되면 참조 대신 실제 데이터를 복사하게 된다.

Reference Type

Syntax

{ [weak instanveName, unowned instanceName] in
    statements
}

 

참조 형식에 클로저 캡처 리스트를 사용할 때는 반드시 대상 앞에 waek나 unwoned 키워드를 추가해야 한다.
각각은 약한 참조와 비소유 참조를 캡처한다.

class Car {
	var totalDrivingDistance = 0.0
	var totalUsedGas = 0.0
	
	lazy var gasMileage: () -> Double = { [weak self] in
		guard let hardSelf = self else {return 0.0}
		return hardSelf.totalDrivingDistance / hardSelf.totalUsedGas
	}

	func drive() {
		self.totalDrivingDistance = 1200.0
		self.totalUsedGas = 73.0
	}
	
	deinit {
		print("car deinit")
	}
}

var car: Car? = Car()
car?.drive()

car?.gasMileage()

car = nil
결과

car deinit

위에서 작성된 강한 참조 사이클이 발생했던 클로저의 코드를 수정했다.
클로저 캡처 사이클을 사용해 self를 약한 참조로 전환하고,
약한 참조은 Optional의 형식을 취하고 있기 때문에 지금처럼 unwrapping 해서 사용하거나 optional chaining으로 멤버에 접근해야 한다.
클로저의 실행이 완료되지 않은 상황에서 참조 대상이 사라진다면 약한 참조를 사용한다.
만약 대상이 사라졌다면 코드를 종료하고 nil을 반환하기 때문에 optional binding에 실패하고 코드가 종료된다.
따라서 약한 참조로 캡처하는 경우 참조 대상이 사라진 경우도 함께 고려해야 한다.

class Car {
	var totalDrivingDistance = 0.0
	var totalUsedGas = 0.0
	
	lazy var gasMileage: () -> Double = { [unowned self] in
		return self.totalDrivingDistance / self.totalUsedGas
	}
	
	func drive() {
		self.totalDrivingDistance = 1200.0
		self.totalUsedGas = 73.0
	}
	
	deinit {
		print("car deinit")
	}
}

이번엔 약한 참조가 아닌 비소유 참조로 캡처했다.
약한 참조와는 달리 기존의 클로저 형식을 그대로 사용할 수 있는 것을 볼 수 있다.
비소유 참조로 캡처된 대상은 클로저 종료 전에 해체될 수 있다.
따라서 해제된 대상에 접근할 경우 오류가 발생하기 때문에 주의가 필요하다.
때문에, 대상의 생명주기가 클로저와 같거나 더 긴 경우에 사용한다.

 

Explicit Strong Capture

swift5.3부터 제공되는 클로저 내부에서 self에 접근하는 코드를 더 단순하게 만드는 방법이다.

struct PersonValue {
	let name: String = "Jane Doe"
	let age: Int = 0
	
	func doSomething() {
		DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
			print(name, age)
		}
	}
}
결과

//error

xcode 11까지는 위와 같이 자기 자신의 속성에 접근하려면 'self.'를 사용해야만 했다.

struct PersonValue {
	let name: String = "Jane Doe"
	let age: Int = 0
	
	func doSomething() {
		DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
			print(self.name, self.age)
		}
	}
}

이렇게 되면 오류는 사라지고 이제부터 클로저는 self를 강하게 참조한다.
따라서 클로저의 실행이 완료될 때까지 self는 메모리에 상주하게 된다.

Explicit Strong Capture는 위와 같이 'self.'로 인한 코드의 번잡성을 줄여주는 문법이다.
기존의 방식은 Implicit Strong Capture라고 부른다.

struct PersonValue {
	let name: String = "Jane Doe"
	let age: Int = 0
	
	func doSomething() {
		DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
			print(name, age)
		}
		DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [self] in
			print(name, age)
		}
	}
}

swift5.3 이상의 버전에서는 같은 코드라도 오류가 발생하지 않는다.
강한 참조 사이클이 발생하지 않는다면 'self.'를 생략해도 된다.
또한 'self.'를 사용해야 한다면 매번 적는 것이 아닌 클로저 캡처 리스트에 한 번 등록하는 implicit strong capture를 구현하는 것 만으로 수고를 덜 수 있다.

class PersonObject {
	let name: String = "John Doe"
	let age: Int = 0
	
	func doSomething() {
		DispatchQueue.main.asyncAfter(deadline: .now() + 1) { 
			print(name, age)
		}
	}
}
결과

//error

하지만 클래스의 경우는 조금 다르다.
클래스는 강한 참조 사이클의 발생 가능성이 높기 때문에 해당 키워드 자체를 생략하는 건 불가능하다.
이 경우에도 implicit strong capture를 사용해 간단하게 해결할 수 있다.

 


Log

2021.09.14.
블로그 이전으로 인한 글 옮김 및 수정