GGURUPiOS

Swift 공식 문서 정리 - (26) Memory Safety 본문

Swift/공식문서 정리 ( 문법 )

Swift 공식 문서 정리 - (26) Memory Safety

꾸럽 2023. 5. 14. 15:54

Memory Safety ( 메모리 안정성 )

기본적으로 스위프트는 코드가 비정상적으로 동작하는 것을 막는 행위를 함

예를들면, 변수가 초기화 되기전에 사용된다던가, 메모리에서 해제된 값을 접근 하는 것을 막는다던가, 인덱스의 한계를 넘는지 확인하는 것 등등

스위프트에서는 또, 메모리의 같은 영역을 동시에 접근해서 충돌이 나지 않도록 해줌

메모리 접근 충돌이 발생할 수 있는 잠재적인 상황을 이해하고 피하는 코드를 어떻게 작성할 수 있는 지 이해하자.

메모리 접근 충돌의 이해

코드에서 메모리 접근은 아래 예와 같이 변수에 값을 할당하거나 접근할 때 발생함

// 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)!")

메모리 충돌은 메모리에 값을 할당하고 메모리 값을 접근하는 것을 동시에 할 때 발 생함

예를들어. 아래와 같이 특정 물건을 구매하고 구매 총 금액을 확인하는 경우 Before의 기본 상태에서 Total은 5$ 임

만약 TV와 T-shirt를 구매하는 동안 Total에 접근해 값을 가져왔다면 Total은 5$가 됨

하지만 실제 제대로된 값은 After의 320$ 여야 함

Characteristics of Memory Access

메모리 접근이 충돌 할 수 있는 상황의 성격은 3가지 경우가 있음

메모리를 읽거나 쓰는경우, 접근 지속시간 그리고 메모리가 접근되는 위치

구체적으로 메모리 충돌은 다음 3가지 조건중 2가지를 만족하면 발생

  • 최소 하나의 쓰기 접근 상황
  • 메모리의 같은 위치를 접근할 때
  • 기간이 겹침 : 접근 지속시간이 겹칠때 읽기 접근과 쓰기 접근의 차이는 분명함 쓰기 접근은 메모리의 위치를 변경, 읽기는 그렇지 않음 메모리의 위치는 무엇을 참조하고 있는지 나타냄 메모리 접근의 지속시간은 즉각적인 접근 과 장기 접근으로 구분할 수 있음 즉각적인 접근은 코드에서 메모리 접근이 시작되고 끝나기 전에 그 메모리에 대한 다른 접근이 시작될수 없음을 의미함 → 아래 예제에서 확인
func oneMore(than number: Int) -> Int {
      return number + 1
  }

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

위 코드에서는 메모리 접근의 충돌이 발생하지 않음

In-Out 파라미터에 액세스 충돌

메모리 충돌은 in-out 파라미터를 잘못 사용할 때 발생할 수 있음

var stepSize = 1

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

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

increment의 파라미터로 inout Int의 number를 사용함

함수 내부에서 인자로 사용한 number를 변경함

이 경우 인자로 number를 넣고, 또 number를 읽어 그 number에 stepSize를 추가해 다시 할당하는 쓰기와 읽기가 동시에 발생 → 액세스 충돌

 

( 그림이 잘 안보이네요 ㅠ )

이 충돌을 해결하는 한가지 방법은 stepSize를 명시적으로 복사하는 것임

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

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

위의 코드와 같이 stepSize를 복사한 copyOfStepSize를 사용하면 하나의 메모리를 읽고 쓰는 행위를 동시에 하지 않게 돼 접근 충돌을 피할 수 있음

또 다른 in-out 파라미터의 장기 접근으로 인한 충돌은 다음과 같은 상황에서 발생 가능

balance라는 함수에 inout 파라미터 2개를 입력하는데 그 입력한 파라미터를 갖고 읽고 쓰는 것과 관련한 연산을 함

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, &playeTwoScore) 와 같이 인자를 넣으면 시간이 겹치는 두개의 쓰기 액세스가 있지만 메모리의 다른 위치에 액세스 함.

하지만 balance(&playerOneScore, &playerOneScore) 와 같이 값을 전달하면 동일한 위치에 대해 동시의 두번의 쓰기 액세스를 시도하기 때문에 충돌이 발생

메소드 안에서의 self 접근 충돌

메소드 안에서 self에 접근할 때 충돌이 발생할 수도 있음

예를 들어보자.

아래와 같이 Player 구조체 선언

struct Player {
    var name: String
    var health: Int
    var energy: Int

    static let maxHealth = 10
    mutating func restoreHealth() {
        health = Player.maxHealth
    }
}

이 구조체를 확장해 Player 간에 체력을 공유하는 shareHealth 함수 선언

Player 타입의 teammate는 inout 파라미터로 지정하고 동작은 현재 Player와

입력한 teammate Player 간에 balance함수를 실행

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 와 maria 둘 다 다른 구조체 인스턴스 이기 때문에 체력을 공유해도 아래와 같이 문제 없음

( 그림이 잘 안보임 https://docs.swift.org/swift-book/images/memory_share_health_maria~dark@2x.png )

하지만 만약 oscar를 자신과 같은 인스턴스인 oscar와 체력을 공유한다고 실행하면 어떻게 될까?

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

체력을 읽어오고 들어온 체력을 변경하는 동작을 한 메모리 위치에서 동시에 수행하게 돼서 충돌이 발생함

(https://docs.swift.org/swift-book/images/memory_share_health_oscar~dark@2x.png)

프로퍼티에 대한 액세스 충돌

구조체, 튜플 및 열거형과 같은 형식은 구조체의 프로퍼티나 튜플의 요소와 같은 개별 구성 값으로 구성됨

이들은 값 유형이기 때문에 값의 일부를 변경하면 전체 값이 변경됨

즉, 프로퍼티중 하나에 대한 읽기 또는 쓰기 액세스에는 전체 값에 대한 읽기 또는 쓰기 액세스가 필요함

예를들어 튜플의 요소에 대한 쓰기 액세스가 중복되면 충돌이 발생함

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

읽기 쓰기를 동시에 수행하는 balance를 실행할 때 충돌에러가 발생함

아래와 같이 Player의 경우에도 전역변수로 선언돼 있어서 충돌 에러가 발생

지역변수는 에러가 발생하지 않는다

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

구조체에서 프로퍼티를 접근하는데 오버래핑 접근으로부터 안전한 상황은 다음과 같음

  • 구조체 인스턴스에서 저장 프로퍼티에만 접근하고 계산된 프로퍼티 혹은 클래스 프로퍼티를 접근하지 않을 때
  • 구조체가 전역변수가 아니라 지역변수 일 때
  • 구조체가 어떤 클로저로부터 캡쳐하지 않거나 nonescaping 클로저에서만 획득된 경우 만약 컴파일러가 접근이 안전하다고 판단하지 못하면 접근이 불가능함