[최종 수정일 : 2018.09.11]

원문 : 애플 개발 문서 Swift 4.2 Language Guide - Memory Safety

메모리 안전성(Memory Safety)

기본적으로, Swift는 코드로부터 발생하는 안전하지 않은 동작을 방지합니다. 예를 들어, Swift는 변수가 사용되기 전에, 초기화 되는 것을 보장하며, 메모리는 해제된 후에는 접근되지 않고, 배열 인덱스는 범위를 벗어난 오류가 있는지 검사합니다.

또한 Swift는 메모리에 단독으로 접근하기 위해, 메모리의 위치를 수정하는 코드를 요구해서, 메모리의 동일한 영역에 대해 다중 접근이 충돌하지 않도록 합니다. Swift는 자동으로 메모리를 관리하기 때문에, 대부분의 경우에 메모리에 접근하는 것에 대해 생각할 필요가 없습니다. 하지만, 잠재적인 충돌이 발생할 수 있는 곳에 대해 이해하고 있는 것이 중요하며, 따라서 메모리 접근이 충돌하는 코드를 작성하는 것을 피할 수 있습니다. 코드에 충돌이 있는 경우에, 컴파일시나 런타임시에 오류가 발생합니다.

메모리 접근이 충돌하는것을 이해하기(Understanding Conflicting Access to Memory)

변수에 값을 설정하거나 함수에 인자를 넘겨주는 일을 할때, 코드에서 메모리에 접근할 일이 발생합니다. 예를 들어, 다음 코드는 읽기 접근과 쓰기 접근 모두 포함되어 있습니다.

// A write access to the memory where one is stored.
var one = 1

// A read access from the memory where one is stored.
print("We're number \(one)!")

코드의 다른 부분이 같은 시간에 메모리의 동일한 위치에 접근할수 있도록 시도할때 충돌하는 메모리 접근이 발생 할 수 있습니다. 동시에 메모리에 있는 곳에 대한 다중 접속은 예측하지 못하거나 모순된 행동이 생기게 될수 있습니다. Swift에서 몇줄의 코드에 있는 값을 수정하는 방법이 있으며, 수정하는 작업 중간에 값에 접근하려고 할 수 있습니다.

종이에 작성된 예산(budget)을 업데이트 하는 방법에 대해 생각해보면 비슷한 문제를 볼수 있습니다. 예산은 2단계 과정으로 업데이트됩니다: 첫번째 항목의 이름과 가격을 추가하고, 현재 목록에 있는 항목들을 반영해서 전체 금액을 변경합니다. 아래 그림에서 볼수 있는 것처럼, 업데이트 전과 후에, 예산(budget)에서 모든 정보를 읽을수 있고, 정확한 답을 얻을 수 있습니다.

예산(budget)에 항목을 추가하는 동안에, 일시적으로, 전체금액이 새로 추가된 항목이 반영되도록 업데이트 되지 않았으므로, 유효하지 않는 상태가 됩니다. 전체 금액을 읽는 동안에 항목을 추가하는 과정에서 잘못된 정보가 제공됩니다.

또한, 이 예제는 메모리에 충돌하는 접근을 고칠때, 발생할 수 있는 일을 보여줍니다: 충돌을 고치는 여러가지 방법이 있고, 어떤 답이 옳은지 항상 분명(obvious) 하지는 않습니다. 이 에제에서, 원래 전체 금액이나 업데이트 된 전체 금액중에 원하는 금액에 따라, $5 또는 $320이 정답이 될 수 있습니다. 충돌 접근을 해결하기 전에, 의도된 바를 결정해야 합니다.

주의
동시(concurrent) 또는 다중스레드(multithreaded) 코드를 작성한 경우에, 메모리 접근이 충돌하는 것은 익숙한 문제일수 있습니다. 하지만, 충돌하는 접근은 단일 스레드에서도 발생할 수 있고, 동시 또는 멀티스레드 코드는 필요하지 않습니다.

단일 스레드에서 메모리 접근이 충돌하는 경우, Swift는 컴파일시나 런타임시에 오류가 발생하는 것을 보장합니다. 멀티스레드 코드에 대해서는, 스레드 처리(Thread Sanitizer)를 사용해서 스레드간 충돌하는 접근을 탐지합니다.

메모리 접근의 특징(Characteristics of Memory Access)

접근이 충돌하는 컨텍스트(context)에서 고려해야 할 메모리 접근의 3가지 특징이 있습니다: 접근이 읽기인지 쓰기인지, 접근 기간, 접근되는 메모리 위치. 구체적으로, 다음에 오는 조건들을 모두 충족하는 두가지 접근의 경우에 충돌이 발생합니다.

  • 적어도 하나는 쓰기 접근입니다.
  • 메모리에 같은 위치를 접근합니다.
  • 기간이 중복됩니다(overlap).

읽고 쓰기 접근의 차이점은 보통 분명합니다: 쓰기 접근은 메모리상의 위치를 변경하지만, 읽기 접근은 그렇지 않습니다. 메모리에서 위치는 접근하는 대상이 됩니다 - 예를들어, 변수, 상수, 프로퍼티입니다. 메모리 접근의 지속시간은 순간적이거나 장기적입니다.

접근이 시작된 후에 종료되기 전에 다른 코드를 실행할수 없는 경우에, 접근은 즉각적(instantaneous)입니다. 본질적으로, 동시에 두가지 즉각적인 접근이 발생할 수 없습니다. 대부분의 메모리 접근은 즉각적으로 이루어집니다. 예를 들어, 코드 목록에 있는 읽기와 쓰기 접근은 즉각적으로 발생합니다.

func oneMore(than number: Int) -> Int {
    return number + 1
}

var myNumber = 1
myNumber = oneMore(than: myNumber)
print(myNumber)
// Prints "2"

하지만, 다른 코드의 실행 범위를 확장하는 장기(long-term) 접근이라고 하는 메모리에 접근하는 몇가지 방법이 있습니다. 순간 접근과 장기 접근간에 차이점은 장기(long-term) 접근이 시작했지만 종료되기 전에 다른 코드를 실행할수 있다는 것이며, 이를 중복(overlap)이라고 합니다. 장기(long-term) 접근은 다른 장기 접근과 즉각적인 접근이 중복(될) 될수 있습니될.

오버랩(다ping) 접근은 주로 코드에서 함수나 메소드, 구조체의 변경가능한(mutating) 메소드에in-ou매개변수로 나타납니다. 특정 종류의 Swift 코드는 장기 접근을 사용하며 아래 섹션에서 논의됩니다.

In-Out 매개변수에 대해 충돌하는 접근(Conflicting Access to In-Out Parameters)

함수는 모든 in-out 매개변수에 장기(long-term) 쓰기 접근을 가지고 있습니다. in-out 매개변수에 대한 쓰기 접근은 in-out 매개변수가 아닌 모든 매개변수가 처리된 뒤에서 시작하고 함수가 호출되는 동안 지속됩니다. 여러개의 in-out 매개변수가 있는 경우에, 쓰기 접근은 매개변수가 나타나는 순서와 동일한 순서로 시작됩니다.

범위 지정 규칙(scoping rules)과 접근 제어(access control)가 다르게 허용되더라도, 장기(long-term) 쓰기 접근의 한가지 결과는 in-out으로 전달된 원래 변수에 접근할 수 없습니다. - 원본에 접근하면 충돌이 발생합니다. 예를 들어:

var stepSize = 1

func increment(_ number: inout Int) {
    number += stepSize
}

increment(&stepSize)
// Error: conflicting accesses to stepSize

위 코드에서, stepSize는 전역변수이고, 일반적으로 increment(_:)에서 접근할 수 있습니다. 하지만, stepSize에 대한 읽기 접근은 number쓰기 접근과 중복되게(overlaps) 됩니다. 아래 그림에서 보는 것처럼, number와 stepSize 모두 메모리에서 같은 위치를 참조합니다. 읽기와 쓰기 접근은 같은 메모리를 참조하고, 중복되며(overlap), 충돌이 일어납니다.

이 충돌을 해결하는 한가지 방법은 stepSize의 명시적인 복사본을 만드는 것입니다.

// Make an explicit copy.
var copyOfStepSize = stepSize
increment(&copyOfStepSize)

// Update the original.
stepSize = copyOfStepSize
// stepSize is now 2

increment(_:) 호출하기 전에 stepSize의 복사본을 만들때, copyOfStepsize의 값은 현재 단계 보다 증가되는 것이 확실합니다. 쓰기 접근이 시작하기 전에 읽기 접근이 끝나며, 충돌이 나지 않습니다.

in-out 매개변수에 대한 장기(long-term) 쓰기 접근의 또 다른 결과는 동일한 함수의 여러개의 in-out 매개변수에 대한 인자처럼 단일 변수를 전달하면 충돌이 발생합니다. 예를들어:

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와 playTwoScore을 인자로 호출해도 충돌이 발생하지 않습니다 - 시간적으로 중복되는 두개의 쓰기 접근이 있지만, 메모리에서 서로 다른 곳을 접근합니다. 대조적으로(in contrast), 모든 매개변수에 대한 값으로 playerOneScore 전달은 같은 시간에 메모리의 같은 위치에 두개의 쓰기 접근이 수행이 시도되기 때문에, 충돌이 발생합니다.

주의
연산자는 함수이기 때문에, in-out 매개변수에 장기 접근할 수 있습니다. 예를 들어, balance(_:_:)가 <^>라는 연잔자 함수인 경우에, playerOneScore <^> playerOneScore 작성하는 것은 balance(&playerOneScore, &playerOneScore)와 동일한 충돌이 발생합니다.

메소드에서 self 에 충돌하는 접근(Conflicting Access to self in Methods)

구조체에서 변경가능(mutating) 메소드는 메소드를 호출하는 동안에 self에 대한 쓰기 접근을 가지고 있습니다. 예를들어, 각 플레이어가 건강 점수을 가지며, 손상을 입었을때 감소하고, 특수 능력을 사용하면 감소하는 에너지 점수가 있는게임을 생각해봅니다.

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 인스턴스의 프로퍼티에 중복되어(어)게ping) 접근할수 있는 다른 코드가 없습니다. 
아래 sharehealth(with:) 메소드는 다른 Player인스턴스를 in-out 매개변수처럼 사용하며, 중복되어(overlapping) 접근할 가능성이 있습니다.

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) 플레이어에 대한 shareHealth(with:) 메소드 호출은 마리아(Maria) 플레이어와 건강을 공유하더라도 충돌이 발생하지 않습니다. 이는 oscar는 변경가능한 메소드에서의 self의 값이기 때문에, 메소드 호출하는 동안에 oscar에 대한 쓰기 접근을하고, 같은 기간동안 maria는 in-out 매개변수로 전달되었기 때문에, maria 쓰기 접근을 합니다. 아래 그림에서 보는 것처럼, 메모리의 다른 곳에 접근합니다. 시간적으로 두개의 쓰기 접근이 중복되지만, 충돌이 되지는 않습니다.

하지만, osscar를 shareHealth(with:)메소드에 인자로 넘기면, 충돌이 발생합니다.

oscar.shareHealth(with: &oscar)
// Error: conflicting accesses to oscar

변경가능한 메소드(mutating method)는 메소드 지속시간동안 self에 대한 쓰기 접근이 필요하고, in-out 매개변수는 같은 시간동안 teammate에 쓰기 접근이 필요합니다. 이 메소드에서, self와 teammate모두 메모리에서 같은 위치를 참조합니다 - 아래 그램에서 보여집니다. 2개의 쓰기 접근은 동일한 메모리를 참조하고 중첩되며, 충돌이 발생합니다.

프로퍼티에 충돌하는 접근(Conflicting Access to Properties)

구조체, 튜플, 열거형과 같은 타입은 구조체의 프로퍼티나 튜플의 요소처럼, 개별 구성 값으로 만들어집니다. 이것들은 값 타입 이기 때문에, 어떤 부분의 값을 변경하면 전체 값이 변경하며, 프로퍼티 하나에 대한 읽기나 쓰기 접근은 전체 값에 대한 읽기나 쓰기 접근이 필요합니다. 예를들어, 튜플의 요소에 중첩 쓰기는 충돌이 발생합니다.

var playerInformation = (health: 10, energy: 20)
balance(&playerInformation.health, &playerInformation.energy)
// Error: conflicting access to properties of playerInformation

위 예제에서, 튜플의 요소에 대한 balance(_:_:) 호출은 playerInformation에 대한 중첩 쓰기 접근이기 때문에, 충돌이 발생합니다. playerInformation.health와 playerInformation.energy모두 함수 호출하는 동안에 balance(_:_:)가 쓰기 접근이 필요하다는 것을 의미하는 in-out 매개변수로 전달됩니다., 두 경우 모두, 튜플 요소에 대한 쓰기 접근은 전체 튜플에 대한 쓰기 접근이 필요합니다. 이는 중복되는 동안 playerInformation에 대한 두개의 쓰기 접근을 의미하며, 충돌이 발생합니다.

아래 코드는 전역 변수로 저장되는 구조체의 프로퍼티에 쓰기 접근이 중첩되는 동안에도 동일한 오류가 발생하는 것을 보여줍니다.

var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy)  // Error

실제로, 구조체의 프로퍼티에 대한 대부분의 접근은 안전하게 중첩될수 있습니다. 예를 들어, 위 예제에서 holly변수가 전역 변수 대신에 지역변수로 변경되는 경우, 컴파일러는 구조체의 저장 프로퍼에 대한 중복 접근이 안전하다는 것을 증명할 수 있습니다.

func someFunction() {
    var oscar = Player(name: "Oscar", health: 10, energy: 10)
    balance(&oscar.health, &oscar.energy)  // OK
}

위 예제에서, 오스카(Oscar)의 health와 energy는 balance(_:_:)에 2개의 in-out 매개변수로 전달됩니다. 2개의 저장 프로퍼티는 어떤 방법으로도 상호작용하지 않기 때문에, 컴파일러는 메모리 안전성이 유지하는(preserved)것을 검증할 수있습니다.

구조체의 프로퍼티에 대한 중복 접근에 대한 제한은 항상 메모리 안전성을 유지할 필요는 없습니다. 메모리 안전성을 원하지만, 단독(exclusive) 접근은 메모리 안전성보다 요구사항이 엄격합니다. - 일부코드에서 메모리에 단독으로 접근하는 것을 위반하더라도 메모리 안전성을 유지하는 것을 의미합니다, Swift는 컴파일러가 단복이 아닌 메모리 접근이 안전하다는 것을 증명할수 있는 경우에, 메모리 안전(Memory-safe) 코드를 허용합니다. 특히, 다음에 오는 조건들이 적용되는 경우에 구조체의 프로퍼티에 대한 중복 접근이 안전하다는 것을 증명할 수 있습니다.

  • 계산 프로퍼티나 클래스 프로퍼티가 아닌, 인스턴스의 저장 프로퍼티만 접근합니다.
  • 구조체는 저녁 변수가 아닌, 지역변수 값입니다.
  • 구조체는 어떤 클로져에 의해 캡쳐되지 않거나, 탈출하지않는(nonescaping) 클로져에 의해서만 캡쳐됩니다.

    컴파일러가 접근이 안전하다는 것을 증명할 수 없다면, 접근을 허용하지 않습니다.


'Swift > Language Guide' 카테고리의 다른 글

Advanced Operators  (0) 2018.09.18
Access Control  (0) 2018.09.18
Memory Safety  (0) 2018.09.18
Automatic Reference Counting  (0) 2018.09.18
Generics  (0) 2018.09.18
Protocols  (0) 2018.09.18
Posted by 까칠코더