티스토리 뷰

[Swift/iOS] 메모리 안전성 Memory Safety From 공식문서(1)

[Swift/iOS] 메모리 안전성 Memory Safety From 공식문서(2)

! 공식문서를 참고한 글입니다.

 

메서드에서 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() 메서드는 자기 자신의 health 프로퍼티에 대한 쓰기 접근이 메서드 시작부터 끝까지 유지됩니다.

접근 충돌이 일어날 수 있는 코드가 존재하지 않습니다.

 

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메서드를 maria를 인자로 호출하는 것은 충돌을 발생시키지 않습니다.

oscar는 mutating 메서드의 self 값이기 때문에, 메서드 호출 동안 oscar에 대한 쓰기 접근이 있습니다.

그리고 inout 파라미터의 인자로 maria가 있기 때문에, maria도 같은 기간 동안 쓰기 접근이 있습니다.

아래 그림에서 보이는 것처럼 서로 다른 메모리 위치에 대한 접근이기 때문에,
같은 시간에 쓰기 접근을 하더라도 충돌을 발생시키지 않습니다.

 

그러나, oscar가 전달인자로 oscar를 전달한다면 충돌이 발생합니다.

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

 

mutating 메서드는 메서드 지속시간 동안 self에 대해 쓰기 접근을 필요로 하는데,
inout 파라미터 역시 같은 지속시간 동안 쓰기 접근을 필요로 합니다.

메서드 안에서 self와 teammate가 같은 메모리 위치를 참조하게 됩니다.

아래 그림처럼, 두 개의 쓰기 접근이 같은 메모리를 참고하고 중첩되면서 충돌이 발생합니다.

 

프로퍼티 접근 충돌
Conflicting Access to Properties

구조체, 튜플, 그리고 열거형 같은 타입은 각각의 구성요소 값으로 이루어져 있습니다.

값들은 값(Struct) 타입이기 때문에, 값의 작은 일부 조각을 바꾸는 것은 전체 값을 바꾸는 것입니다.

이것은 프로퍼티중 하나(일부)에 대해 쓰기나 읽기 접근이 전체, 값의 쓰기나 읽기 접근을 필요로 한다는 것을 의미합니다.

예를 들어, 튜플의 한 요소에 중첩한 쓰기 접근은 충돌을 발생시킵니다.

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

(balance 메서드의 첫 번째 전달인자와 두 번째 전달인자가 같은 메모리위치를 참고하고 동시에 접근해서 충돌 발생)

 

위의 예에서, 한 튜플의 element들을 balance에서 호출하는 것은 충돌을 일으키게 됩니다.

playerInformation에 대한 쓰기 접근이 중첩되어 일어나기 때문입니다.

playerInformation.health와 playerInformation.energy가 둘 다 inout 파라미터에 전달되었는데,
이것은 balance 메서드가 메서드의 지속시간 동안 그 전달인자들에 쓰기 접근을 필요로 한다는 것을 의미합니다.

이 경우에서 튜플의 element에 대한 하나의 쓰기 접근은 튜플 전체에 대한 하나의 쓰기 접근을 필요로 합니다.

이것은 playerInformation이 대해 지속기간 동안 중첩된 두 개의 쓰기 접근이 있다는 것을 의미하고, 충돌을 일으키게 됩니다.

 

아래 코드는 한 구조체의 프로퍼티들에 대한 중첩된 쓰기 접근으로 같은 에러가 발생하는 것을 확인할 수 있습니다.

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

 

전역 변수 대신 로컬 변수를 사용한다면, 안전하게 사용할 수 있습니다.

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

전역 변수는 프로그램 어디에서나 접근할 수 있기 때문에, 동시 접근을 제어하기 위한 엄격한 규칙을 적용합니다.
로컬 변수는 해당 변수가, 함수 내에서만 사용되고 다른 곳에서 접근할 수 없기 때문에,
동시 접근을 안전하다고 판단하여 이를 허용합니다.

 

위의 예제에서는 두 저장된 속성이 상호작용하지 않기 때문에 컴파일러가 메모리 안전성이 지켜졌다는 것을 입증하게 됩니다.

 

구조체의 프로퍼티 중복 접근에 대한 제한은 메모리 안전성을 위해 항상 필요한 것은 아닙니다.

메모리 안전성은 desired guarantee지만, 배타적 접근은 메모리 안전성보다 엄격한 필요사항입니다.

이것은 어떤 코드가 메모리에 대한 배타적 접근을 위반하지만 메모리 안전성을 지키는 것을 의미합니다.

Swift는 만약 컴파일러가 메모리에 대한 비배타적 접근이 여전히 안전하다는 것을 증명하면 이 메모리 안전 코드를 허용하게 됩니다.

특히 다음 조건이 적용된다면, 구조체의 프로퍼티들에 대한 중첩 접근이 안전하다고 증명할 수 있습니다.

  • 인스턴스의 저장된 속성만 접근하고 연산프로퍼티나 class 프로퍼티는 접근하지 않는 것
  • 구조체가 전역변수가 아닌 지역 변수인 것
  • 구조체가 클로저에 의해 캡처되지 않았거나 nonescaping 클로저에 의해 캡처됐을 것

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

 

 

 

 

댓글