티스토리 뷰
이번 포스트에서는 Swift 개발 시 고려할 수 있는 메모리 안정성에 대한 주제를 다뤄보도록 하겠습니다.
Swift에서는 메모리 접근 충돌을 막기 위한 작업이 이루어집니다. 그렇기 때문에 Swift 개발자는 Memory Safety에 대해 크게 고려하지 않아도 개발할 수 있습니다. 하지만 Memory Access Conflict가 발생할 수 있는 상황을 이해하고 Memory Access Conflict를 방지할 수 있는 코드를 어떻게 작성할 수 있는지 이해하는 것은 중요합니다.
Memory Access Conflict는 변수에 값을 쓰는 작업과 읽는 작업이 동시에 이루어질 경우 주로 발생합니다.
var num1 = 1
var num2 = 2
print("num1 : \(num1), num2 : \(num2)")
위 코드는 num1에 1을 넣고 num2에 2를 넣은 후 두 값을 출력하는 코드입니다.
출력 시 print 메소드는 num1에 접근하고 num2에 접근하게 됩니다.
Memory Conflict는 메모리에 값을 할당하고 메모리 값에 동시에 접근할 때 발생합니다. 예를 들어 만약, 위와 같이 특정 물건(Eggs, Cookies)을 구매하고 총 구매 금액을 확인하는 경우 Before의 기본 상태에서의 Total은 $5 입니다. 만약 TV와 T-shirt 를 구매하는 동안(During) Total에 접근해 값을 가져 왔다면 Total은 $5가 됩니다. 하지만 실제 제대로 된 값을 After의 $320 이어야 할 것 입니다.
NOTE
만약 Concurrency 코드나, Multi-Threading 코드를 작성한적이 있다면 이 메모리 접근 충돌 문제는 익숙한 문제일 것 입니다. 하지만 이 접근 충돌 문제는 싱글 쓰레드에서 발생할 수 있는 문제이고 Concurrency와 Multi-Threading과는 관련이 없습니다.
Memory Access가 이루어지는 경우
Memory Access Conflict가 발생할 수 있는 경우는 3가지가 존재합니다. 메모리를 읽거나 쓰는 경우, 접근 지속시간 그리고 메모리가 접근되는 위치입니다.
구체적으로 Memory Access Conflict는 다음 3가지 조건 중 2가지를 만족하면 발생합니다.
1. 최소 하나의 쓰기 접근 상황
2. 메모리의 같은 위치를 접근할 때
3. 접근 지속시간이 겹칠 때
Read Access와 Write Access의 차이점은 분명합니다. Write Access는 Memory 공간의 위치를 변경하거나 값을 변경하고 Read Access는 그렇지 않습니다. (단순히 읽기만 합니다.)
Memory의 위치는 무엇을 참조하고 있는지 나타내는데 대표적으로 Variable과 Constant, Property가 그것입니다.
Memory Access의 지속 시간은 Instantaneous 접근과 Long-Term 접근으로 구분할 수 있습니다.
어떤 변수가 한 Memory 공간에 접근하고 있을 때 이 접근이 종료되기 전에 다른 코드를 실행할 수 없는 경우를 Instantaneous 접근이 이루어집니다. 그 특성상 두 가지 Instantaneous Access는 동시에 발생할 수 없습니다. 대부분의 Memory Access는 Instanteneous합니다.
아래 예제는 즉각적인 접근의 예입니다.
func oneMore(than number: Int) -> Int {
return number + 1
}
var myNumber = 1
myNumber = oneMore(than: myNumber)
print(myNumber)
// Prints "2"
하지만 다른 코드를 실행 할 수 있는 Long-Term Access라고 불리는 메모리에 접근하는 몇 가지 방법이 존재합니다.
Instantaneous Access와 Long-Term Access의 차이점은 메모리의 접근이 시작되고 접근이 종료되기 전에 다른 코드가 실행될 수 있다는 것입니다. Long-Term Access는 Other Long-Term Access와 Instantaneous Access와 겹칠 수 있습니다.
Overlapped Memory Access는 주로 Function/Method에서 inout Parameter를 사용하거나 구조체의 Method를 변경하는 코드에서 나타납니다. Long-Term Access를 사용하는 특정 종류의 코드를 살펴보겠습니다.
In-out Parameter의 접근 충돌
Function은 모든 In-out Parameter에 대하여 Long-Term Write Access Permission을 갖습니다. In-out Parameter에 대한 Write Access는 모든 In-out이 아닌 Parameter가 평가(Evaluation)된 후에 시작되어 해당 Function 호출의 전체 기간 동안 지속됩니다.
입력 매개 변수가 여러 개인 경우 매개 변수가 나타나는 순서대로 Write Access가 시작됩니다.
Long-Term Write Access의 결과 중 하나는 In-Out으로 처리된 Original 변수는 접근할 수 없다는 것입니다. Scoping Rule이나 Access Control이 접근할 수 있도록 허가하더라도 해당 변수에 대하여 접근이 이루어진다면 그 즉시 Conflict가 발생합니다.
예를 들면 다음과 같습니다.
var stepSize = 1
func increment(_ number: inout Int) {
number += stepSize
}
increment(&stepSize)
// Error: conflicting accesses to stepSize
위의 코드에서 stepSize는 전역 변수이며 일반적으로 increment (_ :) 내에서 액세스 할 수 있습니다. 그러나 stepSize에 대한 Read Access는 숫자에 대한 쓰기 액세스와 겹칩니다. 아래 그림에 표시된 것처럼 number와 stepSize는 모두 메모리에서 동일한 위치를 나타냅니다. 읽기 및 쓰기 액세스는 동일한 메모리를 참조하고 중복되어 Conflict가 발생합니다.
이 문제를 해결하는 대표적인 방법은 stepSize를 복제하여 사용하는 방법입니다.
// Make an explicit copy.
var copyOfStepSize = stepSize
increment(©OfStepSize)
// Update the original.
stepSize = copyOfStepSize
// stepSize is now 2
copyOfStepSize라는 변수를 만들어 사용한다면 동시에 접근하는 일이 발생하지 않아 Memory Access Conflict가 발생하지 않습니다.
increment (_ :)를 호출하기 전에 stepSize를 복사하면 copyOfStepSize의 값이 현재 단계 크기만큼 증가한다는 것이 분명합니다. 쓰기 액세스가 시작되기 전에 읽기 액세스가 종료되므로 충돌이 없습니다.
입력 매개 변수에 대한 Long-Term Write Access의 또 다른 결과는 동일한 함수의 여러 입력 매개 변수에 대한 인수로 단일 변수를 전달하면 충돌이 발생한다는 것입니다. 예를 들면 다음과 같습니다.
func balance(_ x: inout Int, _ y: inout Int) {
let sum = x + y
x = sum / 2
y = sum - x
}
var playerOneScore = 42
var playerTwoScore = 30
balance(&playerOneScore, &playerTwoScore) // OK
balance(&playerOneScore, &playerOneScore)
// Error: conflicting accesses to playerOneScore
위의 balance (_ : _ :) 함수는 두 매개 변수를 수정하여 전체 값을 두 값 사이에 고르게 나눕니다. playerOneScore 및 playerTwoScore를 인수로 호출하면 충돌이 발생하지 않습니다. 시간에 겹치는 두 개의 쓰기 액세스가 있지만 메모리의 다른 위치에 액세스합니다. 반대로 playerOneScore를 두 매개 변수의 값으로 전달하면(같은 변수를 두 개의 매개 변수로 전달) 메모리의 동일한 위치에 동시에 두 번의 쓰기 액세스를 수행하려고하기 때문에 충돌이 발생합니다.
NOTE.
연산자는 함수이므로 입력 매개 변수에 장기적으로 액세스 할 수도 있습니다.
예를 들어, balance (_ : _ :)가 <^>라는 연산자 함수 인 경우 playerOneScore <^> playerOneScore를 작성하면 balance (& playerOneScore, & playerOneScore)와 동일한 충돌이 발생합니다.
Method 내에서 자기 자신에 대한 Access Conflict
구조체 에서의 Mutating Method는 메소드 호출 기간 동안 자기 자신에 대한 쓰기 액세스 권한을 갖습니다. 예를 들어, 각 플레이어의 체력이 손상되면 감소하고 에너지 양은 특수 능력을 사용할 때 감소하는 게임을 가정합니다.
struct Player {
var name: String
var health: Int
var energy: Int
static let maxHealth = 10
mutating func restoreHealth() {
health = Player.maxHealth
}
}
위의 restoreHealth() 메소드에서 self에 대한 쓰기 액세스는 메소드 시작시 시작하여 메소드가 리턴 될 때까지 지속됩니다. 이 경우 restoreHealth() 안에 Player 인스턴스의 속성에 겹치는 액세스 권한을 가질 수 있는 다른 코드는 없습니다. 아래의 shareHealth (with :) 메소드는 다른 Player 인스턴스를 입력 매개 변수로 사용하여 액세스가 서로 중복될 가능성을 만듭니다.
extension Player {
mutating func shareHealth(with teammate: inout Player) {
balance(&teammate.health, &health)
}
}
var oscar = Player(name: "Oscar", health: 10, energy: 10)
var maria = Player(name: "Maria", health: 5, energy: 10)
oscar.shareHealth(with: &maria) // OK
위의 코드에서 oscar라는 이름의 Player가 maria라는 이름의 Player와 Health 값을 공유할 수 있도록 shareHealth (with: ) 메소드를 호출하더라도 Conflict는 발생하지 않습니다. 메소드 호출 중에 oscar에 대한 Write Access 권한이 있습니다. oscar는 mutating method에서 self의 값이므로 maria라는 Player가 In-Out Parameter로 전달되었기 때문에 같은 기간 동안 maria에 대한 쓰기 Access 권한이 존재합니다. 아래 그림과 같이 메모리의 다른 위치에 접근하게 됩니다. 따라서 두 번의 Write Access는 시간은 겹치지만 Conflict가 발생하지는 않습니다. 하지만 oscar를 Parameter로 전달하게 된다면 Conflict가 발생합니다.
oscar.shareHealth(with: &oscar)
// Error: conflicting accesses to oscar
Mutating Method는 메소드 지속 시간 동안 self에 대한 쓰기 액세스가 필요하며 입력 매개 변수는 동일한 지속 시간 동안 teammate에게 쓰기 액세스가 필요합니다. 이 방법에서, 자기 자신과 팀원 모두 아래 그림과 같이 메모리에서 동일한 위치를 나타냅니다. 두 번의 쓰기 액세스는 동일한 메모리를 나타내며 겹치므로 충돌이 발생합니다.
프로퍼티에 대한 Access Conflict
Structure, Tuple, Enum과 같은 유형은 Struct의 Property 또는 Tuple의 Element와 같은 개별 구성 요소 값으로 구성됩니다. 이들은 값 유형이므로 값의 일부를 변경하면 전체 값이 변경됩니다. 즉, Properties 중 하나에 대한 읽기 또는 쓰기 액세스는 전체 값에 대한 읽기 또는 쓰기 액세스가 필요합니다. 예를 들어, Tuple Element에 대한 쓰기 액세스가 겹치면 충돌이 발생합니다.
var playerInformation = (health: 10, energy: 20)
balance(&playerInformation.health, &playerInformation.energy)
// Error: conflicting access to properties of playerInformation
위의 예에서, 튜플 요소에서 balance (_ : _ :)를 호출하면 playerInformation에 대한 쓰기 액세스가 겹치기 때문에 충돌이 발생합니다. playerInformation.energy 및 playerInformation.health는 모두 인-아웃 매개 변수로 전달됩니다. 즉, 함수 호출 기간 동안 balance (_ : _ :)에 쓰기 액세스 권한이 필요합니다. 두 경우 모두, Tuple Element 대한 쓰기 액세스는 전체 튜플에 대한 쓰기 액세스가 필요합니다. 이는 재생 시간이 겹치는 playerInformation에 두 번의 쓰기 액세스가 있어 충돌을 일으킨다는 것을 의미합니다.
아래 코드는 전역 변수에 저장된 Struct의 Property에 대한 쓰기 액세스가 겹치는 경우 동일한 오류가 나타나는 것을 보여줍니다.
var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy) // Error
실제로, Struct의 Property에 대한 대부분의 액세스는 안전하게 중복될 수 있습니다. 예를 들어, 위 예제에서 holly 변수가 전역 변수 대신 로컬 변수로 변경되면 컴파일러는 구조체의 저장된 Property에 대한 중복 액세스가 안전하다는 것을 증명할 수 있습니다.
func someFunction() {
var oscar = Player(name: "Oscar", health: 10, energy: 10)
balance(&oscar.health, &oscar.energy) // OK
}
위의 예시코드에서 oscar의 health값과 energy 값은 balance(_: _) 함수에 2개의 입력 매개 변수로 전달됩니다. 컴파일러는 지정된 두 속성이 어떤식으로든 상호 작용을 하지 않기 때문에 Memory Safety가 유지된다는 것을 증명할 수 있습니다.
메모리의 안정성을 유지하기 위해 구조체의 Property에 대한 Duplicate Access에 대한 제한이 항상 필요한 것은 아닙니다. 메모리 안정성은 바람직한 Guarantee이지만, 독점적인 Access는 메모리 안정성보다 더욱 더 엄격한 요구 사항입니다. 즉, 일부 코드는 메모리에 대한 독점적인 Access를 위반하더라도 메모리 안정성을 유지합니다.
컴파일러가 메모리에 대한 비 배타적 액세스가 여전히 안전하다는 것을 증명할 수 있는 경우 Swift는 이 메모리 안전 코드를 허용합니다.
특히 다음 조건이 적용되는 경우 구조체의 Property에 대한 중복 액세스가 안전하다는 것을 증명할 수 있습니다.
구조체 인스턴스에서 저장프로퍼티에만 접근하고 계산된 프로퍼티 혹은 클래스 프로퍼티를 접근하지 않을 때
구조체가 전역변수가 아니라 지역변수 일때
구조체가 어떤 클로저로부터도 캡쳐(capturing)하지 않거나 nonescaping 클로저에서만 획득된 경우
만약 컴파일러가 접근이 안전하다고 판단하지 못하면 접근이 불가능 합니다.
이번 포스트에서는 Swift 개발 시 고려해야 할 메모리 안정성에 대해 살펴보았습니다.
'프로그래밍언어 > Swift' 카테고리의 다른 글
Swift 클로저 (Closure) (0) | 2020.02.23 |
---|---|
Swift 함수 (0) | 2020.02.23 |
Swift 제어문 (조건문, 반복문) (0) | 2020.02.14 |
Swift Collection Types (컬렉션 타입) (0) | 2020.02.10 |
Swift 문자열과 문자 (0) | 2020.02.09 |
- Total
- Today
- Yesterday
- SwiftUI
- Rxjava
- ios
- watchos
- CloudComputing
- 아이폰
- Kotlin
- java
- Notissu
- 컬렉션
- Reactive programming
- XCode
- apple
- Apple Watch
- Swift
- 알고리즘
- 스위프트
- databinding
- 함수형
- 애플워치
- android
- Elliotable
- 함수형프로그래밍
- 오토레이아웃
- 상속
- retrofit
- 코틀린
- C++
- 안드로이드
- Auto Layout
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |